2&&D>2&&!e.hidden?(W=p,X=0):W=D>1&&X>1&&Q<6?d:0),f!==c&&($=innerWidth+c*g,M=innerHeight+c,l=-1*c,f=c),s=m[n].getBoundingClientRect(),(B=s.bottom)>=l&&(H=s.top)<=M&&(z=s.right)>=l*g&&(F=s.left)<=$&&(B||z||F||H)&&(i.loadHidden||Z(m[n]))&&(R&&Q<3&&!h&&(D<3||X<4)||Y(m[n],c))){if(at(m[n]),u=!0,Q>9)break}else!u&&R&&!a&&Q<4&&X<4&&D>2&&(L[0]||i.preloadAfterLoad)&&(L[0]||!h&&(B||z||F||H||"auto"!=m[n].getAttribute(i.sizesAttr)))&&(a=L[0]||m[n]);a&&!u&&at(a)}},et=function(t){var e,r=0,o=i.throttleDelay,s=i.ricTimeout,a=function(){e=!1,r=n.now(),t()},c=l&&s>49?function(){l(a,{timeout:s}),s!==i.ricTimeout&&(s=i.ricTimeout)}:C((function(){u(a)}),!0);return function(t){var i;(t=!0===t)&&(s=33),e||(e=!0,(i=o-(n.now()-r))<0&&(i=0),t||i<9?c():u(c,i))}}(tt),nt=function(t){var e=t.target;e._lazyCache?delete e._lazyCache:(G(t),m(e,i.loadedClass),y(e,i.loadingClass),v(e,it),b(e,"lazyloaded"))},rt=C(nt),it=function(t){rt({target:t.target})},ot=function(t){var e,n=t.getAttribute(i.srcsetAttr);(e=i.customMedia[t.getAttribute("data-media")||t.getAttribute("media")])&&t.setAttribute("media",e),n&&t.setAttribute("srcset",n)},st=C((function(t,e,n,r,o){var s,a,c,l,f,d;(f=b(t,"lazybeforeunveil",e)).defaultPrevented||(r&&(n?m(t,i.autosizesClass):t.setAttribute("sizes",r)),a=t.getAttribute(i.srcsetAttr),s=t.getAttribute(i.srcAttr),o&&(l=(c=t.parentNode)&&h.test(c.nodeName||"")),d=e.firesLoad||"src"in t&&(a||s||l),f={target:t},m(t,i.loadingClass),d&&(clearTimeout(P),P=u(G,2500),v(t,it,!0)),l&&p.call(c.getElementsByTagName("source"),ot),a?t.setAttribute("srcset",a):s&&!l&&(K.test(t.nodeName)?function(t,e){var n=t.getAttribute("data-load-mode")||i.iframeLoadMode;0==n?t.contentWindow.location.replace(e):1==n&&(t.src=e)}(t,s):t.src=s),o&&(a||l)&&w(t,{src:s})),t._lazyRace&&delete t._lazyRace,y(t,i.lazyClass),S((function(){var e=t.complete&&t.naturalWidth>1;d&&!e||(e&&m(t,i.fastLoadedClass),nt(f),t._lazyCache=!0,u((function(){"_lazyCache"in t&&delete t._lazyCache}),9)),"lazy"==t.loading&&Q--}),!0)})),at=function(t){if(!t._lazyRace){var e,n=V.test(t.nodeName),r=n&&(t.getAttribute(i.sizesAttr)||t.getAttribute("sizes")),o="auto"==r;(!o&&R||!n||!t.getAttribute("src")&&!t.srcset||t.complete||g(t,i.errorClass)||!g(t,i.lazyClass))&&(e=b(t,"lazyunveilread").detail,o&&T.updateElem(t,!0,t.offsetWidth),t._lazyRace=!0,Q++,st(t,e,o,r,n))}},ut=A((function(){i.loadMode=3,et()})),ct=function(){3==i.loadMode&&(i.loadMode=2),ut()},lt=function(){R||(n.now()-q<999?u(lt,999):(R=!0,i.loadMode=3,et(),a("scroll",ct,!0)))},{_:function(){q=n.now(),r.elements=e.getElementsByClassName(i.lazyClass),L=e.getElementsByClassName(i.lazyClass+" "+i.preloadClass),a("scroll",et,!0),a("resize",et,!0),a("pageshow",(function(t){if(t.persisted){var n=e.querySelectorAll("."+i.loadingClass);n.length&&n.forEach&&c((function(){n.forEach((function(t){t.complete&&at(t)}))}))}})),t.MutationObserver?new MutationObserver(et).observe(o,{childList:!0,subtree:!0,attributes:!0}):(o.addEventListener("DOMNodeInserted",et,!0),o.addEventListener("DOMAttrModified",et,!0),setInterval(et,999)),a("hashchange",et,!0),["focus","mouseover","click","load","transitionend","animationend"].forEach((function(t){e.addEventListener(t,et,!0)})),/d$|^c/.test(e.readyState)?lt():(a("load",lt),e.addEventListener("DOMContentLoaded",et),u(lt,2e4)),r.elements.length?(tt(),S._lsFlush()):et()},checkElems:et,unveil:at,_aLSL:ct}),T=(N=C((function(t,e,n,r){var i,o,s;if(t._lazysizesWidth=r,r+="px",t.setAttribute("sizes",r),h.test(e.nodeName||""))for(o=0,s=(i=e.getElementsByTagName("source")).length;o0&&void 0!==arguments[0]?arguments[0]:{};this.action="function"==typeof t.action?t.action:this.defaultAction,this.target="function"==typeof t.target?t.target:this.defaultTarget,this.text="function"==typeof t.text?t.text:this.defaultText,this.container="object"===r(t.container)?t.container:document.body}},{key:"listenClick",value:function(t){var e=this;this.listener=(0,a.default)(t,"click",(function(t){return e.onClick(t)}))}},{key:"onClick",value:function(t){var e=t.delegateTarget||t.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new o.default({action:this.action(e),target:this.target(e),text:this.text(e),container:this.container,trigger:e,emitter:this})}},{key:"defaultAction",value:function(t){return l("action",t)}},{key:"defaultTarget",value:function(t){var e=l("target",t);if(e)return document.querySelector(e)}},{key:"defaultText",value:function(t){return l("text",t)}},{key:"destroy",value:function(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:["copy","cut"],e="string"==typeof t?[t]:t,n=!!document.queryCommandSupported;return e.forEach((function(t){n=n&&!!document.queryCommandSupported(t)})),n}}]),e}(s.default);function l(t,e){var n="data-clipboard-"+t;if(e.hasAttribute(n))return e.getAttribute(n)}t.exports=c},function(t,e,n){"use strict";var r,i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},o=function(){function t(t,e){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};this.action=t.action,this.container=t.container,this.emitter=t.emitter,this.target=t.target,this.text=t.text,this.trigger=t.trigger,this.selectedText=""}},{key:"initSelection",value:function(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function(){var t=this,e="rtl"==document.documentElement.getAttribute("dir");this.removeFake(),this.fakeHandlerCallback=function(){return t.removeFake()},this.fakeHandler=this.container.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[e?"right":"left"]="-9999px";var n=window.pageYOffset||document.documentElement.scrollTop;this.fakeElem.style.top=n+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,this.container.appendChild(this.fakeElem),this.selectedText=(0,a.default)(this.fakeElem),this.copyText()}},{key:"removeFake",value:function(){this.fakeHandler&&(this.container.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(this.container.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function(){this.selectedText=(0,a.default)(this.target),this.copyText()}},{key:"copyText",value:function(){var t=void 0;try{t=document.execCommand(this.action)}catch(e){t=!1}this.handleResult(t)}},{key:"handleResult",value:function(t){this.emitter.emit(t?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function(){this.trigger&&this.trigger.focus(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function(){this.removeFake()}},{key:"action",set:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"copy";if(this._action=t,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function(){return this._action}},{key:"target",set:function(t){if(void 0!==t){if(!t||"object"!==(void 0===t?"undefined":i(t))||1!==t.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&t.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(t.hasAttribute("readonly")||t.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=t}},get:function(){return this._target}}]),t}();t.exports=u},function(t,e){t.exports=function(t){var e;if("SELECT"===t.nodeName)t.focus(),e=t.value;else if("INPUT"===t.nodeName||"TEXTAREA"===t.nodeName){var n=t.hasAttribute("readonly");n||t.setAttribute("readonly",""),t.select(),t.setSelectionRange(0,t.value.length),n||t.removeAttribute("readonly"),e=t.value}else{t.hasAttribute("contenteditable")&&t.focus();var r=window.getSelection(),i=document.createRange();i.selectNodeContents(t),r.removeAllRanges(),r.addRange(i),e=r.toString()}return e}},function(t,e){function n(){}n.prototype={on:function(t,e,n){var r=this.e||(this.e={});return(r[t]||(r[t]=[])).push({fn:e,ctx:n}),this},once:function(t,e,n){var r=this;function i(){r.off(t,i),e.apply(n,arguments)}return i._=e,this.on(t,i,n)},emit:function(t){for(var e=[].slice.call(arguments,1),n=((this.e||(this.e={}))[t]||[]).slice(),r=0,i=n.length;r";var r=document.createElement("div");r.appendChild(document.createTextNode(e)),n=n||"";var i=document.createElement("div");i.appendChild(document.createTextNode(n));var s=document.createElement("div");return s.appendChild(document.createTextNode(t)),s.innerHTML.replace(RegExp(o(r.innerHTML),"g"),e).replace(RegExp(o(i.innerHTML),"g"),n)}}},function(t,e,n){"use strict";t.exports={element:null}},function(t,e){var n=Object.prototype.hasOwnProperty,r=Object.prototype.toString;t.exports=function(t,e,i){if("[object Function]"!==r.call(e))throw new TypeError("iterator must be a function");var o=t.length;if(o===+o)for(var s=0;s was loaded but did not call our provided callback"),JSONPScriptError:o("JSONPScriptError","
-{{ end }}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/icon-link.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/icon-link.html
deleted file mode 100644
index dec9ae48b..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/icon-link.html
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/maintenance-pages-table.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/maintenance-pages-table.html
deleted file mode 100644
index f1fb6d059..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/maintenance-pages-table.html
+++ /dev/null
@@ -1,24 +0,0 @@
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/math.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/math.html
deleted file mode 100644
index b1eb5c8db..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/math.html
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-docs-mobile.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-docs-mobile.html
deleted file mode 100644
index a8fc27e21..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-docs-mobile.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{{ $currentPage := . }}
-{{ $menu := .Site.Menus.docs.ByWeight }}
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-docs.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-docs.html
deleted file mode 100644
index 61aa11dde..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-docs.html
+++ /dev/null
@@ -1,23 +0,0 @@
-{{ $currentPage := . }}
-
-
- {{ range .Site.Menus.docs.ByWeight }}
- {{ $post := printf "%s" .Post }}
-
-
-
- {{- if .HasChildren }}
-
- {{- end}}
-
- {{- end}}
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-global-mobile.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-global-mobile.html
deleted file mode 100644
index 6ad98923e..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-global-mobile.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{{ $currentPage := . }}
-{{ $menu := .Site.Menus.global }}
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links.html
deleted file mode 100644
index af3790b16..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links.html
+++ /dev/null
@@ -1,37 +0,0 @@
-{{ $currentPage := . }}
-{{ $.Scratch.Add "listlinkClasses" "f6 link primary-color-dark hover-white db brand-font ma0 w-100 pv3 ph4" }}
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-mobile.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-mobile.html
deleted file mode 100644
index b04866e52..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-mobile.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
- Menu
-
- Docs Menu
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-top.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-top.html
deleted file mode 100644
index f64111409..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-top.html
+++ /dev/null
@@ -1,16 +0,0 @@
-{{ $currentPage := . }}
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/get-featured-image.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/get-featured-image.html
deleted file mode 100644
index 79b315a44..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/get-featured-image.html
+++ /dev/null
@@ -1,24 +0,0 @@
-{{ $images := $.Resources.ByType "image" }}
-{{ $featured := $images.GetMatch "*feature*" }}
-{{ if not $featured }}{{ $featured = $images.GetMatch "{*cover*,*thumbnail*}" }}{{ end }}
-{{ if not $featured }}
- {{ $featured = resources.Get "/opengraph/gohugoio-card-base-1.png" }}
- {{ $size := 80 }}
- {{ $title := $.LinkTitle }}
- {{ if gt (len $title) 20 }}
- {{ $size = 70 }}
- {{ end }}
-
- {{ $text := $title }}
- {{ $textOptions := dict
- "color" "#FFF"
- "size" $size
- "lineSpacing" 10
- "x" 65 "y" 80
- "font" (resources.Get "/opengraph/mulish-black.ttf")
- }}
-
- {{ $featured = $featured | images.Filter (images.Text $text $textOptions) }}
-{{ end }}
-
-{{ return $featured }}
\ No newline at end of file
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/opengraph.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/opengraph.html
deleted file mode 100644
index 6d195ede6..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/opengraph.html
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
-{{- with $.Params.images -}}
-{{- range first 6 . }} {{ end -}}
-{{- else -}}
-{{- $featured := partial "opengraph/get-featured-image.html" . }}
-{{- with $featured -}}
-
-{{- else -}}
-{{- with $.Site.Params.images }} {{ end -}}
-{{- end -}}
-{{- end -}}
-
-{{- if .IsPage }}
-{{- $iso8601 := "2006-01-02T15:04:05-07:00" -}}
-
-{{ with .PublishDate }} {{ end }}
-{{ with .Lastmod }} {{ end }}
-{{- end -}}
-
-{{- with .Params.audio }} {{ end }}
-{{- with .Params.locale }} {{ end }}
-{{- with .Site.Params.title }} {{ end }}
-{{- with .Params.videos }}{{- range . }}
-
-{{ end }}{{ end }}
-
-{{- /* If it is part of a series, link to related articles */}}
-{{- $permalink := .Permalink }}
-{{- $siteSeries := .Site.Taxonomies.series }}
-{{ with .Params.series }}{{- range $name := . }}
- {{- $series := index $siteSeries ($name | urlize) }}
- {{- range $page := first 6 $series.Pages }}
- {{- if ne $page.Permalink $permalink }} {{ end }}
- {{- end }}
-{{ end }}{{ end }}
-
-{{- /* Facebook Page Admin ID for Domain Insights */}}
-{{- with site.Params.social.facebook_admin }} {{ end }}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/twitter_cards.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/twitter_cards.html
deleted file mode 100644
index 456f87b1c..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/twitter_cards.html
+++ /dev/null
@@ -1,22 +0,0 @@
-{{- with $.Params.images -}}
-
-
-{{ else -}}
-{{- $featured := partial "opengraph/get-featured-image.html" . }}
-{{- with $featured -}}
-
-
-{{- else -}}
-{{- with $.Site.Params.images -}}
-
-
-{{ else -}}
-
-{{- end -}}
-{{- end -}}
-{{- end }}
-
-
-{{ with site.Params.social.twitter -}}
-
-{{ end -}}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/page-edit.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/page-edit.html
deleted file mode 100644
index f41e5b791..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/page-edit.html
+++ /dev/null
@@ -1,3 +0,0 @@
-Improve this page
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/page-header.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/page-header.html
deleted file mode 100644
index dcc96242f..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/page-header.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{{ $currentPage := . }}
-{{ $currentURL := .RelPermalink }}
-
-
-
-
-
- News:
-
-
- {{ range $name, $taxonomy := .Site.Taxonomies.categories }}
- {{ $link := $name | printf "%s%s" "/categories/" | printf "%s/" }}
-
-
- {{ $name | humanize }}
-
-
- {{ end }}
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/pagelayout.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/pagelayout.html
deleted file mode 100644
index e6b644b2f..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/pagelayout.html
+++ /dev/null
@@ -1,36 +0,0 @@
-{{ $section_to_display := .section_to_display }}
-
-
-
-
- {{ partial "nav-links-docs.html" .context }}
-
-
-
-
-
- {{ with $.context.Data.Singular }}{{ . | humanize }}: {{ end }}{{ .context.Title }}
-
-
- {{ .context.Content }}
-
-
-
-
- {{ $interior_classes := .context.Site.Params.flex_box_interior_classes }}
-
- {{ $pages := $section_to_display }}
- {{ if in (slice "functions" "methods") $.context.Type }}
- {{ $pages = $.context.Pages }}
- {{ end }}
- {{ range $pages }}
- {{ partial "boxes-section-summaries.html" (dict "context" . "classes" $interior_classes "fullcontent" true) }}
- {{ end }}
-
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links-in-section-with-title.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links-in-section-with-title.html
deleted file mode 100644
index 71a14c0ef..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links-in-section-with-title.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{{ if or .PrevInSection .NextInSection }}
-{{/* this div holds these a tags as a unit for flex-box display */}}
-
-{{ end }}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links-in-section.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links-in-section.html
deleted file mode 100644
index af9f4aac1..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links-in-section.html
+++ /dev/null
@@ -1,16 +0,0 @@
-{{ if or .PrevInSection .NextInSection }}
-{{/* this div holds these a tags as a unit for flex-box display */}}
-
-{{ end }}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links.html
deleted file mode 100644
index cd43dd840..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{{if .Prev }}
-
- {{ partial "svg/ic_chevron_left_black_24px.svg" (dict "size" "30px") }} {{ .Prev.Title }}
-
-{{end}}
-
-{{if .Next }}
-
- {{ .Next.Title }} {{ partial "svg/ic_chevron_right_black_24px.svg" (dict "size" "30px") }}
-
-{{end}}
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/related.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/related.html
deleted file mode 100644
index ff7435668..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/related.html
+++ /dev/null
@@ -1,22 +0,0 @@
-{{- $heading := "See also" }}
-{{- $related := site.RegularPages.Related . | first 5 }}
-
-{{- if in (slice "functions" "methods") .Type }}
- {{- $related = slice }}
- {{- range .Params.action.related }}
- {{- with site.GetPage (lower .) }}
- {{- $related = $related | append . }}
- {{- else }}
- {{- errorf "The 'related' partial was unable to get page %s" . }}
- {{- end }}
- {{- end }}
-{{- end }}
-
-{{- with $related }}
- {{ $heading }}
-
-{{- end }}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/right-sidebar.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/right-sidebar.html
deleted file mode 100644
index ecdbeb33f..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/right-sidebar.html
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
- {{- partial "previous-next-links-in-section.html" . }}
-
-
- {{- /* Table of contents is visible if toc = true in front matter. */}}
- {{- if .Params.toc }}
-
-
On this page
- {{- .TableOfContents }}
-
- {{- end }}
-
- {{- /* Section menu for single pages is visible if showSectionMenu = true in top level section page. */}}
- {{- if .FirstSection.Params.showSectionMenu }}
- {{- with .CurrentSection.RegularPages }}
- In this section
-
-
-
- {{- end }}
- {{- end }}
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-footer.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-footer.html
deleted file mode 100644
index 09c013361..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-footer.html
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ with getenv "REPOSITORY_URL" -}}
-
- {{- end }}
-
-
-
-
- {{ partial "home-page-sections/sponsors.html" (dict "cx" . "gtag" "footer" "classes_section" "pb3 w-100" "classes_copy" "f7 w-90-ns") }}
-
-
-
-
-
-
The Hugo logos are copyright © Steve Francia 2013–{{ now.Year }}.
-
The Hugo Gopher is based on an original work by Renée French.
-
-
-
-
-
- {{- partial "nav-mobile.html" . -}}
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-manifest.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-manifest.html
deleted file mode 100644
index 54472ba16..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-manifest.html
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-nav.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-nav.html
deleted file mode 100644
index f387d66f3..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-nav.html
+++ /dev/null
@@ -1,38 +0,0 @@
-{{ $currentPage := . }}
-
-
-
-
-
-
- {{ range .Site.Menus.global }}
-
- {{/* TODO: Create an "Global" active class to show which site one is currently at */}}
-
-
- {{ end }}
-
-
-
- {{- partial "site-search.html" . -}}
-
-
-
- {{- partialCached "social-follow.html" . -}}
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-scripts.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-scripts.html
deleted file mode 100644
index 7dec9de18..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-scripts.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
-{{ $scripts := resources.Get "output/js/app.js" }}
-{{ $isDev := eq hugo.Environment "development" }}
-{{ if not $isDev }}
-{{ $scripts = $scripts | fingerprint }}
-{{ end }}
-{{ with $scripts }}
- {{ if $isDev }}
-
- {{ else }}
-
- {{ end }}
- {{ $.Scratch.Set "scripts" . }}
-{{end}}
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-search.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-search.html
deleted file mode 100644
index 8c97ac454..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-search.html
+++ /dev/null
@@ -1,6 +0,0 @@
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/social-follow.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/social-follow.html
deleted file mode 100644
index 7451c15d6..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/social-follow.html
+++ /dev/null
@@ -1,21 +0,0 @@
-
-gohugoio
-Star
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/summary.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/summary.html
deleted file mode 100644
index 0f140cf70..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/summary.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
{{ humanize .Section }}
-
-
- {{ .Summary }}
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/Twitter_Logo_Blue.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/Twitter_Logo_Blue.svg
deleted file mode 100644
index da9438414..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/Twitter_Logo_Blue.svg
+++ /dev/null
@@ -1 +0,0 @@
-Twitter_Logo_Blue
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/apple.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/apple.svg
deleted file mode 100644
index 6f3c20f76..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/apple.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/clipboard.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/clipboard.svg
deleted file mode 100644
index e1b170359..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/clipboard.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/clippy.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/clippy.svg
deleted file mode 100644
index e1b170359..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/clippy.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/cloud.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/cloud.svg
deleted file mode 100644
index 2ea15de87..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/cloud.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/content.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/content.svg
deleted file mode 100644
index bc696b90b..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/content.svg
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/design.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/design.svg
deleted file mode 100644
index 9f9d71769..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/design.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/exclamation.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/exclamation.svg
deleted file mode 100644
index e69de29bb..000000000
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/facebook.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/facebook.svg
deleted file mode 100644
index 6e6af44a2..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/facebook.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/focus.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/focus.svg
deleted file mode 100644
index ed2c929b4..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/focus.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/freebsd.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/freebsd.svg
deleted file mode 100644
index 842be09a1..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/freebsd.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/functions.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/functions.svg
deleted file mode 100644
index 717a35686..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/functions.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/github-corner.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/github-corner.svg
deleted file mode 100644
index 29bc57ad3..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/github-corner.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/github-squared.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/github-squared.svg
deleted file mode 100644
index dabc741e0..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/github-squared.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gitter.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gitter.svg
deleted file mode 100644
index 9c2de7da2..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gitter.svg
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gme.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gme.svg
deleted file mode 100644
index 9ab114aa3..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gme.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/godoc-icon.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/godoc-icon.html
deleted file mode 100644
index 1a6b82159..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/godoc-icon.html
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-2.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-2.svg
deleted file mode 100644
index 961221f18..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-2.svg
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-front.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-front.svg
deleted file mode 100644
index 0f8fbe0d9..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-front.svg
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-homepage.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-homepage.svg
deleted file mode 100644
index 36d9f1c41..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-homepage.svg
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-side_path.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-side_path.svg
deleted file mode 100644
index 05cfb84d1..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-side_path.svg
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-small.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-small.svg
deleted file mode 100644
index bc1e5010c..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-small.svg
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher.svg
deleted file mode 100644
index 7f6ec255c..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher.svg
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/hugo-h-only.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/hugo-h-only.svg
deleted file mode 100644
index ea72a6f51..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/hugo-h-only.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/hugo.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/hugo.svg
deleted file mode 100644
index 58d025596..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/hugo.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_down.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_down.svg
deleted file mode 100644
index 3ba28c3f5..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_down.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_up.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_up.svg
deleted file mode 100644
index 8ec2eb766..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_up.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_chevron_left_black_24px.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_chevron_left_black_24px.svg
deleted file mode 100644
index da37757cf..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_chevron_left_black_24px.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_chevron_right_black_24px.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_chevron_right_black_24px.svg
deleted file mode 100644
index 47689a91e..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_chevron_right_black_24px.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/idea.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/idea.svg
deleted file mode 100644
index 5c2ccc2f4..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/idea.svg
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/instagram.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/instagram.svg
deleted file mode 100644
index ae915113b..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/instagram.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/javascript.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/javascript.svg
deleted file mode 100644
index b0e2f5b0d..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/javascript.svg
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/json.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/json.svg
deleted file mode 100644
index d2ba6d0fc..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/json.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/link-ext.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/link-ext.svg
deleted file mode 100644
index ba9400b7f..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/link-ext.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/link-permalink.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/link-permalink.svg
deleted file mode 100644
index f5de52d02..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/link-permalink.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/md.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/md.svg
deleted file mode 100644
index f1a794565..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/md.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/mdsolid.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/mdsolid.svg
deleted file mode 100644
index d0d9ae938..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/mdsolid.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
- Svg Vector Icons : http://www.onlinewebfonts.com/icon
-
-
\ No newline at end of file
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/newlogo.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/newlogo.svg
deleted file mode 100644
index 83b706383..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/newlogo.svg
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/sass.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/sass.svg
deleted file mode 100644
index da3d9cfcf..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/sass.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/search.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/search.svg
deleted file mode 100644
index 181789b54..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/search.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/twitter.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/twitter.svg
deleted file mode 100644
index 247ca9062..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/twitter.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/website.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/website.svg
deleted file mode 100644
index 2bdcf5f94..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/website.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/windows.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/windows.svg
deleted file mode 100644
index fe3bf0296..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/windows.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/yaml.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/yaml.svg
deleted file mode 100644
index 59eeb71c2..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/yaml.svg
+++ /dev/null
@@ -1 +0,0 @@
-icon
\ No newline at end of file
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/tags.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/tags.html
deleted file mode 100644
index 59e3e51a0..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/tags.html
+++ /dev/null
@@ -1,37 +0,0 @@
-{{ $currentPageUrl := .RelPermalink }}
-{{ if and .Params.tags .Site.Taxonomies.tags }}
- {{ $name := index .Params.tags 0 }}
- {{ $name := $name | urlize }}
- {{ $tags := index .Site.Taxonomies.tags $name }}
-
-
-
- {{ with .Params.tags }}
-
- Tags:
-
- {{ range .}}
-
-
- {{ . }}
-
-
- {{ end }}
- {{ end }}
- {{ range $i, $e := $tags.Pages }}
- {{ if eq $i 1 }}
-
- Related entries:
-
- {{ end }}
- {{ if ne .RelPermalink $currentPageUrl }}
-
-
- {{ .LinkTitle }}
-
-
- {{ end }}
- {{end}}
-
-
-{{end}}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/robots.txt b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/robots.txt
deleted file mode 100644
index 25b9e9a0d..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/robots.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-User-agent: *
-# robotstxt.org - if ENV production variable is false robots will be disallowed.
-{{ if eq (getenv "HUGO_ENV") "production" }}
- Disallow: admin/
- Disallow:
-{{ else }}
- Disallow: /
-{{ end }}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/articlelist.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/articlelist.html
deleted file mode 100644
index 2755b1e2d..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/articlelist.html
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
- Title
- Author
- Date
-
-
-
- {{ range $ind, $art := $.Site.Data.articles.article }}
-
- {{$art.title | markdownify }}
- {{ $art.author | markdownify }}
- {{ $art.date }}
-
- {{ end }}
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/chroma-lexers.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/chroma-lexers.html
deleted file mode 100644
index 2e10c3dee..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/chroma-lexers.html
+++ /dev/null
@@ -1,6 +0,0 @@
-
- {{ range .Site.Data.docs.chroma.lexers }}
- {{ .Name }}
- {{ with .Aliases }}{{ delimit . ", " }}{{ end }}
- {{ end }}
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/code-toggle.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/code-toggle.html
deleted file mode 100644
index d1131132d..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/code-toggle.html
+++ /dev/null
@@ -1,101 +0,0 @@
-{{- /*
- Renders syntax-highlighted configuration data in JSON, TOML, and YAML formats.
-
- @param {string} [config] The section of site.Data.docs.config to render.
- @param {bool} [copy=false] If true, display a copy to clipboard button.
- @param {string} [file] The file name to display above the rendered code.
- @param {bool} [fm=false] If true, render the code as front matter.
- @param {bool} [skipHeader=false] If false, omit top level key(s) when rendering a section of site.Data.docs.config.
-
- @returns {template.HTML}
-*/}}
-
-{{- /* Initialize. */}}
-{{- $config := "" }}
-{{- $dataKey := "" }}
-{{- $copy := false }}
-{{- $file := "" }}
-{{- $fm := false }}
-{{- $skipHeader := false }}
-
-{{- /* Get parameters. */}}
-{{- $config = .Get "config" }}
-{{- $dataKey = .Get "dataKey" }}
-{{- $file = .Get "file" }}
-{{- if in (slice "false" false 0) (.Get "copy") }}
- {{- $copy = false }}
-{{- else if in (slice "true" true 1) (.Get "copy") }}
- {{- $copy = true }}
-{{- end }}
-{{- if in (slice "false" false 0) (.Get "fm") }}
- {{- $fm = false }}
-{{- else if in (slice "true" true 1) (.Get "fm") }}
- {{- $fm = true }}
-{{- end }}
-{{- if in (slice "false" false 0) (.Get "skipHeader") }}
- {{- $skipHeader = false }}
-{{- else if in (slice "true" true 1) (.Get "skipHeader") }}
- {{- $skipHeader = true }}
-{{- end }}
-
-{{- /* Define constants. */}}
-{{- $delimiters := dict "toml" "+++" "yaml" "---" }}
-{{- $langs := slice "yaml" "toml" "json" }}
-{{- $placeHolder := "#-hugo-placeholder-#" }}
-
-{{- /* Render. */}}
-{{- $code := "" }}
-{{- if $config }}
- {{- $file = $file | default "hugo" }}
- {{- $sections := (split $config ".") }}
- {{- $configSection := index $.Site.Data.docs.config $sections }}
- {{- $code = dict $sections $configSection }}
- {{- if $skipHeader }}
- {{- $code = $configSection }}
- {{- end }}
-{{- else if $dataKey }}
- {{- $file = $file | default $dataKey }}
- {{- $sections := (split $dataKey ".") }}
- {{- $code = index $.Site.Data.docs $sections }}
-{{- else }}
- {{- $code = $.Inner }}
-{{- end }}
-
-
- {{- with $file }}
-
- {{ . }}{{ if not $fm }}.{{ end }}
-
- {{- end }}
- {{- range $langs }}
-
- {{ . }}
-
-
- {{- end }}
-
-
- {{- range $langs }}
-
- {{- $hCode := $code | transform.Remarshal . }}
- {{- if and $fm (in (slice "toml" "yaml") .) }}
- {{- $hCode = printf "%s\n%s\n%s" $placeHolder $hCode $placeHolder }}
- {{- end }}
- {{- $hCode = $hCode | replaceRE `\n+` "\n" }}
- {{ highlight $hCode . "" | replaceRE $placeHolder (index $delimiters .) | safeHTML }}
-
- {{- if $copy }}
-
- {{- /* Functionality located within filesaver.js The copy here is located in the css with .copy class so it can be replaced with JS on success */}}
- {{- end }}
- {{- end }}
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/code.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/code.html
deleted file mode 100644
index dd21551cb..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/code.html
+++ /dev/null
@@ -1,35 +0,0 @@
-{{- /*
-Renders syntax highlighted code.
-
-@param {bool} [copy=false] If true, display a copy to clipboard button.
-@param {string} [file] The file name to display above the rendered code.
-@param {string} [lang] The code language of the inner content.
-
-@returns {template.HTML}
-*/}}
-
-{{- /* Get parameters. */}}
-{{- $copy := false }}
-{{- if in (slice "false" false 0) (.Get "copy") }}
- {{- $copy = false }}
-{{- else if in (slice "true" true 1) (.Get "copy")}}
- {{- $copy = true }}
-{{- end }}
-{{- $file := or (.Get "file") " " }}
-{{- $lang := or (.Get "lang") (path.Ext $file | strings.TrimPrefix ".") "text" }}
-
-{{- /* Use the go-html-template Chroma lexer for HTML. */}}
-{{- if eq $lang "html" }}
- {{- $lang = "go-html-template" }}
-{{- end }}
-
-{{- /* Render. */}}
-
-
{{ $file | htmlUnescape }}
- {{- if $copy }}
-
- {{- end }}
-
- {{- highlight (trim .Inner "\n\r") $lang }}
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/datatable-filtered.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/datatable-filtered.html
deleted file mode 100644
index ff3f299bd..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/datatable-filtered.html
+++ /dev/null
@@ -1,39 +0,0 @@
-{{ $package := (index .Params 0) }}
-{{ $listname := (index .Params 1) }}
-{{ $filter := split (index .Params 2) " " }}
-{{ $filter1 := index $filter 0 }}
-{{ $filter2 := index $filter 1 }}
-{{ $filter3 := index $filter 2 }}
-
-{{ $list := (index (index .Site.Data.docs $package) $listname) }}
-{{ $fields := after 3 .Params }}
-{{ $list := where $list $filter1 $filter2 $filter3 }}
-
-
-
- {{ range $fields }}
- {{ . }}
- {{ end }}
-
- {{ range $list }}
-
- {{ range $k, $v := . }}
- {{ $.Scratch.Set $k $v }}
- {{ end }}
- {{ range $k, $v := $fields }}
-
- {{ $tdContent := $.Scratch.Get . }}
- {{ if eq $k 3 }}
- {{ printf "%v" $tdContent |
- strings.ReplaceRE `\[` "" |
- strings.ReplaceRE `\s` " " |
- strings.ReplaceRE `\]` " " |
- safeHTML }}
- {{ else }}
- {{ $tdContent }}
- {{ end}}
-
- {{ end }}
-
- {{ end }}
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/datatable.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/datatable.html
deleted file mode 100644
index 12054f89d..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/datatable.html
+++ /dev/null
@@ -1,33 +0,0 @@
-{{ $package := (index .Params 0) }}
-{{ $listname := (index .Params 1) }}
-{{ $list := (index (index .Site.Data.docs $package) $listname) }}
-{{ $fields := after 2 .Params }}
-
-
-
-
- {{ range $fields }}
- {{ $s := . }}
- {{ if eq $s "_key" }}
- {{ $s = "Type" }}
- {{ end }}
- {{ $s }}
- {{ end }}
-
- {{ range $k1, $v1 := $list }}
-
- {{ range $k2, $v2 := . }}
- {{ $.Scratch.Set $k2 $v2 }}
- {{ end }}
- {{ range $fields }}
- {{ $s := "" }}
- {{ if eq . "_key" }}
- {{ $s = $k1 }}
- {{ else }}
- {{ $s = $.Scratch.Get . }}
- {{ end }}
- {{ $s }}
- {{ end }}
-
- {{ end }}
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/deprecated-in.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/deprecated-in.html
deleted file mode 100644
index 7219d7f54..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/deprecated-in.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{{ $_hugo_config := `{ "version": 1 }` }}
-
-{{ with .Get 0 }}
- {{ $version := printf "v%v" (strings.TrimLeft "vV" .) }}
- {{ $href := printf "https://github.com/gohugoio/hugo/releases/tag/%s" $version }}
-
-{{ else }}
- {{ errorf "The %q shortcode requires a single positional parameter indicating version. See %s" .Name .Position }}
-{{ end }}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/funcsig.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/funcsig.html
deleted file mode 100644
index 4e96504ab..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/funcsig.html
+++ /dev/null
@@ -1,4 +0,0 @@
-Syntax
-
- {{- .Inner -}}
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/gomodules-info.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/gomodules-info.html
deleted file mode 100644
index b56758ac3..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/gomodules-info.html
+++ /dev/null
@@ -1,17 +0,0 @@
-{{ $text := `
-Most of the commands for **Hugo Modules** require a newer version of Go installed (see https://golang.org/dl/) and the relevant VCS client (e.g. Git, see https://git-scm.com/downloads/ ). If you have an "older" site running on Netlify, you may have to set GO_VERSION to 1.12 in your Environment settings.
-
-For more information about Go Modules, see:
-
-* https://github.com/golang/go/wiki/Modules
-* https://blog.golang.org/using-go-modules
-` }}
-
-
-
- {{partial "svg/exclamation.svg" (dict "size" "20px" ) }}
-
-
- {{- $text | markdownify -}}
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/img.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/img.html
deleted file mode 100644
index 50d4da9ed..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/img.html
+++ /dev/null
@@ -1,379 +0,0 @@
-{{- /*
-Renders the given image using the given filter, if any.
-
-@param {string} src The path to the image which must be a remote, page, or global resource.
-@param {string} [filter] The filter to apply to the image (case-insensitive).
-@param {string} [filterArgs] A comma-delimited list of arguments to pass to the filter.
-@param {bool} [example=false] If true, renders a before/after example.
-@param {int} [exampleWidth=384] Image width, in pixels, when rendering a before/after example.
-
-@returns {template.HTML}
-
-@examples
-
- {{< img src="zion-national-park.jpg" >}}
-
- {{< img src="zion-national-park.jpg" alt="Zion National Park" >}}
-
- {{< img
- src="zion-national-park.jpg"
- alt="Zion National Park"
- filter="grayscale"
- >}}
-
- {{< img
- src="zion-national-park.jpg"
- alt="Zion National Park"
- filter="process"
- filterArgs="resize 400x webp"
- >}}
-
- {{< img
- src="zion-national-park.jpg"
- alt="Zion National Park"
- filter="colorize"
- filterArgs="180,50,20"
- >}}
-
- {{< img
- src="zion-national-park.jpg"
- alt="Zion National Park"
- filter="grayscale"
- example=true
- >}}
-
- {{< img
- src="zion-national-park.jpg"
- alt="Zion National Park"
- filter="grayscale"
- example=true
- exampleWidth=400
- >}}
-
- When using the text filter, provide the arguments in this order:
-
- 0. The text
- 1. The horizontal offset, in pixels, relative to the left of the image (default 20)
- 2. The vertical offset, in pixels, relative to the top of the image (default 20)
- 3. The font size in pixels (default 64)
- 4. The line height (default 1.2)
- 5. The font color (default #ffffff)
-
- {{< img
- src="images/examples/zion-national-park.jpg"
- alt="Zion National Park"
- filter="Text"
- filterArgs="Zion National Park,25,250,56"
- example=true
- >}}
-
- When using the padding filter, provide all arguments in this order:
-
- 0. Padding top
- 1. Padding right
- 2. Padding bottom
- 3. Padding right
- 4. Canvas color
-
- {{< img
- src="images/examples/zion-national-park.jpg"
- alt="Zion National Park"
- filter="Padding"
- filterArgs="20,50,20,50,#0705"
- example=true
- >}}
-
-*/}}
-
-{{- /* Initialize. */}}
-{{- $alt := "" }}
-{{- $src := "" }}
-{{- $filter := "" }}
-{{- $filterArgs := slice }}
-{{- $example := false }}
-{{- $exampleWidth := 384 }}
-
-{{- /* Default values to use with the text filter. */}}
-{{ $textFilterOpts := dict
- "xOffset" 20
- "yOffset" 20
- "fontSize" 64
- "lineHeight" 1.2
- "fontColor" "#ffffff"
- "fontPath" "https://github.com/google/fonts/raw/main/ofl/lato/Lato-Regular.ttf"
-}}
-
-{{- /* Get and validate parameters. */}}
-{{- with .Get "alt" }}
- {{- $alt = .}}
-{{- end }}
-
-{{- with .Get "src" }}
- {{- $src = . }}
-{{- else }}
- {{- errorf "The %q shortcode requires a file parameter. See %s" .Name .Position }}
-{{- end }}
-
-{{- with .Get "filter" }}
- {{- $filter = . | lower }}
-{{- end }}
-
-{{- $validFilters := slice
- "autoorient" "brightness" "colorbalance" "colorize" "contrast" "gamma"
- "gaussianblur" "grayscale" "hue" "invert" "none" "opacity" "overlay"
- "padding" "pixelate" "process" "saturation" "sepia" "sigmoid" "text"
- "unsharpmask"
-}}
-
-{{- with $filter }}
- {{- if not (in $validFilters .) }}
- {{- errorf "The filter passed to the %q shortcode is invalid. The filter must be one of %s. See %s" $.Name (delimit $validFilters ", " ", or ") $.Position }}
- {{- end }}
-{{- end }}
-
-{{- with .Get "filterArgs" }}
- {{- $filterArgs = split . "," }}
- {{- $filterArgs = apply $filterArgs "trim" "." " " }}
-{{- end }}
-
-{{- if in (slice "false" false 0) (.Get "example") }}
- {{- $example = false }}
-{{- else if in (slice "true" true 1) (.Get "example")}}
- {{- $example = true }}
-{{- end }}
-
-{{- with .Get "exampleWidth" }}
- {{- $exampleWidth = . | int }}
-{{- end }}
-
-{{- /* Get image. */}}
-{{- $ctx := dict "page" .Page "src" $src "name" .Name "position" .Position }}
-{{- $i := partial "inline/get-resource.html" $ctx }}
-
-{{- /* Resize if rendering before/after examples. */}}
-{{- if $example }}
- {{- $i = $i.Resize (printf "%dx" $exampleWidth) }}
-{{- end }}
-
-{{- /* Create filter. */}}
-{{- $f := "" }}
-{{- $ctx := dict "filter" $filter "args" $filterArgs "name" .Name "position" .Position }}
-{{- if eq $filter "autoorient" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 0) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $f = images.AutoOrient }}
-{{- else if eq $filter "brightness" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $filterArgs = apply $filterArgs "float" "." }}
- {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" -100 "max" 100) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $f = images.Brightness (index $filterArgs 0) }}
-{{- else if eq $filter "colorbalance" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 3) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $filterArgs = apply $filterArgs "float" "." }}
- {{- $ctx = merge $ctx (dict "argName" "percentage red" "argValue" (index $filterArgs 0) "min" -100 "max" 500) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $ctx = merge $ctx (dict "argName" "percentage green" "argValue" (index $filterArgs 1) "min" -100 "max" 500) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $ctx = merge $ctx (dict "argName" "percentage blue" "argValue" (index $filterArgs 2) "min" -100 "max" 500) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $f = images.ColorBalance (index $filterArgs 0) (index $filterArgs 1) (index $filterArgs 2) }}
-{{- else if eq $filter "colorize" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 3) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $filterArgs = apply $filterArgs "float" "." }}
- {{- $ctx = merge $ctx (dict "argName" "hue" "argValue" (index $filterArgs 0) "min" 0 "max" 360) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $ctx = merge $ctx (dict "argName" "saturation" "argValue" (index $filterArgs 1) "min" 0 "max" 100) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 2) "min" 0 "max" 100) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $f = images.Colorize (index $filterArgs 0) (index $filterArgs 1) (index $filterArgs 2) }}
-{{- else if eq $filter "contrast" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $filterArgs = apply $filterArgs "float" "." }}
- {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" -100 "max" 100) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $f = images.Contrast (index $filterArgs 0) }}
-{{- else if eq $filter "gamma" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $filterArgs = apply $filterArgs "float" "." }}
- {{- $ctx = merge $ctx (dict "argName" "gamma" "argValue" (index $filterArgs 0) "min" 0 "max" 100) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $f = images.Gamma (index $filterArgs 0) }}
-{{- else if eq $filter "gaussianblur" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $filterArgs = apply $filterArgs "float" "." }}
- {{- $ctx = merge $ctx (dict "argName" "sigma" "argValue" (index $filterArgs 0) "min" 0 "max" 1000) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $f = images.GaussianBlur (index $filterArgs 0) }}
-{{- else if eq $filter "grayscale" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 0) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $f = images.Grayscale }}
-{{- else if eq $filter "hue" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $filterArgs = apply $filterArgs "float" "." }}
- {{- $ctx = merge $ctx (dict "argName" "shift" "argValue" (index $filterArgs 0) "min" -180 "max" 180) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $f = images.Hue (index $filterArgs 0) }}
-{{- else if eq $filter "invert" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 0) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $f = images.Invert }}
-{{- else if eq $filter "opacity" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $filterArgs = apply $filterArgs "float" "." }}
- {{- $ctx = merge $ctx (dict "argName" "opacity" "argValue" (index $filterArgs 0) "min" 0 "max" 1) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $f = images.Opacity (index $filterArgs 0) }}
-{{- else if eq $filter "overlay" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 3) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $ctx := dict "src" (index $filterArgs 0) "name" .Name "position" .Position }}
- {{- $overlayImg := partial "inline/get-resource.html" $ctx }}
- {{- $f = images.Overlay $overlayImg (index $filterArgs 1 | float ) (index $filterArgs 2 | float) }}
-{{- else if eq $filter "padding" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 5) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $f = images.Padding
- (index $filterArgs 0 | int)
- (index $filterArgs 1 | int)
- (index $filterArgs 2 | int)
- (index $filterArgs 3 | int)
- (index $filterArgs 4)
- }}
-{{- else if eq $filter "pixelate" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $filterArgs = apply $filterArgs "float" "." }}
- {{- $ctx = merge $ctx (dict "argName" "size" "argValue" (index $filterArgs 0) "min" 0 "max" 1000) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $f = images.Pixelate (index $filterArgs 0) }}
-{{- else if eq $filter "process" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $f = images.Process (index $filterArgs 0) }}
-{{- else if eq $filter "saturation" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $filterArgs = apply $filterArgs "float" "." }}
- {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" -100 "max" 500) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $f = images.Saturation (index $filterArgs 0) }}
-{{- else if eq $filter "sepia" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $filterArgs = apply $filterArgs "float" "." }}
- {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" 0 "max" 100) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $f = images.Sepia (index $filterArgs 0) }}
-{{- else if eq $filter "sigmoid" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 2) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $filterArgs = apply $filterArgs "float" "." }}
- {{- $ctx = merge $ctx (dict "argName" "midpoint" "argValue" (index $filterArgs 0) "min" 0 "max" 1) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $ctx = merge $ctx (dict "argName" "factor" "argValue" (index $filterArgs 1) "min" -10 "max" 10) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $f = images.Sigmoid (index $filterArgs 0) (index $filterArgs 1) }}
-{{- else if eq $filter "text" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $ctx := dict "src" $textFilterOpts.fontPath "name" .Name "position" .Position }}
- {{- $font := or (partial "inline/get-resource.html" $ctx) }}
- {{- $fontSize := or (index $filterArgs 3 | int) $textFilterOpts.fontSize }}
- {{- $lineHeight := math.Max (or (index $filterArgs 4 | float) $textFilterOpts.lineHeight) 1 }}
- {{- $opts := dict
- "x" (or (index $filterArgs 1 | int) $textFilterOpts.xOffset)
- "y" (or (index $filterArgs 2 | int) $textFilterOpts.yOffset)
- "size" $fontSize
- "linespacing" (mul (sub $lineHeight 1) $fontSize)
- "color" (or (index $filterArgs 5) $textFilterOpts.fontColor)
- "font" $font
- }}
- {{- $f = images.Text (index $filterArgs 0) $opts }}
-{{- else if eq $filter "unsharpmask" }}
- {{- $ctx = merge $ctx (dict "argsRequired" 3) }}
- {{- template "validate-arg-count" $ctx }}
- {{- $filterArgs = apply $filterArgs "float" "." }}
- {{- $ctx = merge $ctx (dict "argName" "sigma" "argValue" (index $filterArgs 0) "min" 0 "max" 500) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $ctx = merge $ctx (dict "argName" "amount" "argValue" (index $filterArgs 1) "min" 0 "max" 100) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $ctx = merge $ctx (dict "argName" "threshold" "argValue" (index $filterArgs 2) "min" 0 "max" 1) }}
- {{- template "validate-arg-value" $ctx }}
- {{- $f = images.UnsharpMask (index $filterArgs 0) (index $filterArgs 1) (index $filterArgs 2) }}
-{{- end }}
-
-{{- /* Apply filter. */}}
-{{- $fi := $i }}
-{{- with $f }}
- {{- $fi = $i.Filter . }}
-{{- end }}
-
-{{- /* Render. */}}
-{{- if $example }}
- Original
-
- Processed
-
-{{- else -}}
-
-{{- end }}
-
-{{- define "validate-arg-count" }}
- {{- $msg := "When using the %q filter, the %q shortcode requires an args parameter with %d %s. See %s" }}
- {{- if lt (len .args) .argsRequired }}
- {{- $text := "values" }}
- {{- if eq 1 .argsRequired }}
- {{- $text = "value" }}
- {{- end }}
- {{- errorf $msg .filter .name .argsRequired $text .position }}
- {{- end }}
-{{- end }}
-
-{{- define "validate-arg-value" }}
- {{- $msg := "The %q argument passed to the %q shortcode is invalid. Expected a value in the range [%v,%v], but received %v. See %s" }}
- {{- if or (lt .argValue .min) (gt .argValue .max) }}
- {{- errorf $msg .argName .name .min .max .argValue .position }}
- {{- end }}
-{{- end }}
-
-{{- define "partials/inline/get-resource.html" }}
- {{- $r := "" }}
- {{- $u := urls.Parse .src }}
- {{- $msg := "The %q shortcode was unable to resolve %s. See %s" }}
- {{- if $u.IsAbs }}
- {{- with resources.GetRemote $u.String }}
- {{- with .Err }}
- {{- errorf "%s" }}
- {{- else }}
- {{- /* This is a remote resource. */}}
- {{- $r = . }}
- {{- end }}
- {{- else }}
- {{- errorf $msg $.name $u.String $.position }}
- {{- end }}
- {{- else }}
- {{- with .page.Resources.Get (strings.TrimPrefix "./" $u.Path) }}
- {{- /* This is a page resource. */}}
- {{- $r = . }}
- {{- else }}
- {{- with resources.Get $u.Path }}
- {{- /* This is a global resource. */}}
- {{- $r = . }}
- {{- else }}
- {{- errorf $msg $.name $u.Path $.position }}
- {{- end }}
- {{- end }}
- {{- end }}
- {{- return $r}}
-{{- end -}}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/imgproc.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/imgproc.html
deleted file mode 100644
index c09133ba8..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/imgproc.html
+++ /dev/null
@@ -1,37 +0,0 @@
-{{- /*
-Renders the given image using the given process specification.
-
-@param {string} (positional parameter 0) The path to the image, relative to the current page. The image must be a page resource.
-@param {string}} (positional parameter 1) The image processing specification.
-
-@returns template.HTML
-
-@example {{< imgproc "sunset.jpg" "resize 300x" />}}
-*/}}
-
-{{- with $.Get 0 }}
- {{- with $i := $.Page.Resources.Get . }}
- {{- with $spec := $.Get 1 }}
- {{- with $i.Process . }}
-
-
-
-
- {{- with $.Inner }}
- {{ . }}
- {{- else }}
- {{ $spec }}
- {{- end }}
-
-
-
- {{- end }}
- {{- else }}
- {{- errorf "The %q shortcode requires a positional parameter (1) containing the image processing specification. See %s" $.Name $.Position }}
- {{- end }}
- {{- else }}
- {{- errorf "The %q shortcode was unable to find %q. See %s" $.Name . $.Position }}
- {{- end }}
-{{- else }}
- {{- errorf "The %q shortcode requires a positional parameter (0) indicating the image path, relative to the current page. See %s" $.Name $.Position }}
-{{- end }}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/list-pages-in-section.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/list-pages-in-section.html
deleted file mode 100644
index 73e7f85a9..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/list-pages-in-section.html
+++ /dev/null
@@ -1,96 +0,0 @@
-{{- /*
-Renders a description list of the pages in the given section.
-
-Render a subset of the pages in the section by specifying a predefined filter,
-and whether to include those pages.
-
-Filters are defined in the data directory, in the file named page_filters. Each
-filter is an array of paths to a file, relative to the root of the content
-directory. Hugo will throw an error if the specified filter does not exist, or
-if any of the pages in the filter do not exist.
-
-The definition term elements (dt) have an id attribute derived from the title
-of the page. This is probably unique, because pages of the same title in the
-same section is unlikely.
-
-If you render a complete list on a page, then call the shortcode again to
-render a subset, you will generate duplicate element ids. In this case, set
-omitElementIDs to true for the subset.
-
-@param {string} path The path to the section.
-@param {string} [filter=""] The name of filter list.
-@param {string} [filterType=""] The type of filter, either include or exclude.
-@param {string} [omitElementIDs=false] Whether to omit dt element ids.
-@param {string} [titlePrefix=""] The string to prepend to the link title.
-
-@returns template.HTML
-
-@example {{< list-pages-in-section path=/methods/resources >}}
-@example {{< list-pages-in-section path=/functions/images filter=some_filter filterType=exclude >}}
-@example {{< list-pages-in-section path=/functions/images filter=some_filter filterType=exclude titlePrefix=foo >}}
-@example {{< list-pages-in-section path=/functions/images filter=some_filter filterType=exclude titlePrefix=foo omitElementIDs=true >}}
-*/}}
-
-{{- /* Initialize. */}}
-{{- $filter := or "" (.Get "filter" | lower)}}
-{{- $filterType := or (.Get "filterType") "none" | lower }}
-{{- $filteredPages := slice }}
-{{- $titlePrefix := or (.Get "titlePrefix") "" }}
-{{- $omitElementIDs := false }}
-
-{{- /* Get boolean parameters. */}}
-{{- if in (slice "false" false 0) (.Get "omitElementIDs") }}
- {{- $omitElementIDs = false }}
-{{- else if in (slice "true" true 1) (.Get "omitElementIDs")}}
- {{- $omitElementIDs = true }}
-{{- end }}
-
-{{- /* Build slice of filtered pages. */}}
-{{- with $filter }}
- {{- with index site.Data.page_filters . }}
- {{- range . }}
- {{- with site.GetPage . }}
- {{- $filteredPages = $filteredPages | append . }}
- {{- else }}
- {{- errorf "The %q shortcode was unable to find %q as specified in the page_filters data file. See %s" $.Name . $.Position }}
- {{- end }}
- {{- end }}
- {{- else }}
- {{- errorf "The %q shortcode was unable to find the %q filter in the page_filters data file. See %s" $.Name . $.Position }}
- {{- end }}
-{{- end }}
-
-{{- /* Render */}}
-{{- with $sectionPath := .Get "path" }}
- {{- with site.GetPage . }}
- {{- with .RegularPages }}
-
- {{- range $page := .ByTitle }}
- {{- if or
- (and (eq $filterType "include") (in $filteredPages $page))
- (and (eq $filterType "exclude") (not (in $filteredPages $page)))
- (eq $filterType "none")
- }}
- {{- $linkTitle := .LinkTitle }}
- {{- with $titlePrefix }}
- {{- $linkTitle = printf "%s%s" . $linkTitle }}
- {{- end }}
- {{- $idAttribute := "" }}
- {{- if not $omitElementIDs }}
- {{- $id := path.Join .File.Dir .File.ContentBaseName | replaceRE `[\|/]` ":" | lower }}
- {{- $idAttribute = printf " id=%q" $id }}
- {{- end }}
- {{ $linkTitle }}
- {{- $page.Description | $page.RenderString }}
- {{- end }}
- {{- end }}
-
- {{- else }}
- {{- warnf "The %q shortcode found no pages in the %q section. See %s" $.Name $sectionPath $.Position }}
- {{- end }}
- {{- else }}
- {{- errorf "The %q shortcode was unable to find %q. See %s" $.Name $sectionPath $.Position }}
- {{- end }}
-{{- else }}
- {{- errorf "The %q shortcode requires a 'path' parameter indicating the path to the section. See %s" $.Name $.Position }}
-{{- end }}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/module-mounts-note.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/module-mounts-note.html
deleted file mode 100644
index e8b2a7a7e..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/module-mounts-note.html
+++ /dev/null
@@ -1 +0,0 @@
-Also see [Module Mounts Config](/hugo-modules/configuration/#module-configuration-mounts) for an alternative way to configure this directory (from Hugo 0.56).
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/new-in.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/new-in.html
deleted file mode 100644
index e22a91f3d..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/new-in.html
+++ /dev/null
@@ -1,36 +0,0 @@
-{{- /*
-Renders a "new in" button indicating the version in which a feature was added.
-
-When comparing the current version to the specified version, the "new in"
-button will be hidden if any of the following conditions is true:
-
-- The major version difference exceeds the majorVersionDiffThreshold
-- The minor version difference exceeds the minorVersionDiffThreshold
-
-@param {string} version The semantic version string, with or without a leading v.
-@returns {template.HTML}
-
-@example {{< new-in 0.100.0 >}}
-*/}}
-
-{{- /* Set defaults. */}}
-{{- $majorVersionDiffThreshold := 0 }}
-{{- $minorVersionDiffThreshold := 30 }}
-{{- $displayExpirationWarning := true }}
-
-{{- /* Render. */}}
-{{- with $version := .Get 0 | strings.TrimPrefix "v" }}
- {{- $majorVersionDiff := sub (index (split hugo.Version ".") 0 | int) (index (split $version ".") 0 | int) }}
- {{- $minorVersionDiff := sub (index (split hugo.Version ".") 1 | int) (index (split $version ".") 1 | int) }}
- {{- if or (gt $majorVersionDiff $majorVersionDiffThreshold) (gt $minorVersionDiff $minorVersionDiffThreshold) }}
- {{- if $displayExpirationWarning }}
- {{- warnf "This call to the %q shortcode should be removed: %s. The button is now hidden because the specified version (%s) is older than the display threshold." $.Name $.Position $version }}
- {{- end }}
- {{- else }}
-
- New in v{{ $version }}
-
- {{- end }}
-{{- else }}
- {{- errorf "The %q shortcode requires a positional parameter (version). See %s" .Name .Position }}
-{{- end -}}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/note.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/note.html
deleted file mode 100644
index 99818114e..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/note.html
+++ /dev/null
@@ -1,7 +0,0 @@
-{{ $_hugo_config := `{ "version": 1 }` }}
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/quick-reference.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/quick-reference.html
deleted file mode 100644
index fc53c93bd..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/quick-reference.html
+++ /dev/null
@@ -1,39 +0,0 @@
-{{/*
-Renders the child sections of the given top-level section, listing each child's immediate descendants.
-
-@param {string} section The top-level section to render.
-@returns template.HTML
-
-@example {{% quick-reference section="functions" %}}
-*/}}
-
-{{ $section := "" }}
-{{ with .Get "section" }}
- {{ $section = . }}
-{{ else }}
- {{ errorf "The %q shortcodes requires a 'section' parameter. See %s" .Name .Position }}
-{{ end }}
-
-{{/* Do not change the markdown indentation, and do not remove blank lines. */}}
-{{ with site.GetPage $section }}
- {{ range .Sections }}
-
-## {{ .LinkTitle }}
-{{ .RawContent }}
-
- {{ range .Pages }}
- {{ $aliases := "" }}
- {{ if eq .Section "functions" }}
- {{ with .Params.action.aliases }}
- {{ $aliases = delimit . " or " }}
- {{ end }}
- {{ end }}
-
-[{{ .LinkTitle }}]({{ .RelPermalink }}) {{ with $aliases }}({{ . }}){{ end }}
-: {{ .Description }}
-
- {{ end }}
- {{ end }}
-{{ else }}
- {{ errorf "The %q shortcodes was unable to find the %q section. See %s" .Name $section .Position }}
-{{ end }}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/readfile.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/readfile.html
deleted file mode 100644
index de8083443..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/readfile.html
+++ /dev/null
@@ -1,29 +0,0 @@
-{{- $highlight := or (.Get "highlight") "" }}
-
-{{- $markdown := false }}
-{{- if in (slice "false" false 0) (.Get "markdown") }}
- {{- $markdown = false }}
-{{- else if in (slice "true" true 1) (.Get "markdown") }}
- {{- $markdown = true }}
-{{- end }}
-
-{{- with .Get "file" }}
- {{- if os.FileExists . }}
- {{- with os.ReadFile . }}
- {{- $content := trim . "\n\r" }}
- {{- if $markdown }}
- {{- $content | markdownify }}
- {{- else if $highlight }}
- {{- highlight $content $highlight }}
- {{- else }}
- {{- $content | safeHTML }}
- {{- end }}
- {{- else }}
- {{- errorf "The %q shortcode was unable to read %q. See %s" $.Name . $.Position }}
- {{- end }}
- {{- else }}
- {{- errorf "The %q shortcode was unable to find %q. See %s" $.Name . $.Position }}
- {{- end }}
-{{- else }}
- {{- errorf "The %q shortcode requires a 'file' parameter. See %s" $.Name $.Position }}
-{{- end }}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/todo.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/todo.html
deleted file mode 100644
index 50a099267..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/todo.html
+++ /dev/null
@@ -1 +0,0 @@
-{{ if .Inner }}{{ end }}
\ No newline at end of file
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/showcase/list.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/showcase/list.html
deleted file mode 100644
index bff52ad8d..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/showcase/list.html
+++ /dev/null
@@ -1,46 +0,0 @@
-{{ define "main" }}
-
-
-
- {{ .Title }}
-
-
- {{ .Content }}
-
-
-
- {{ range (.Paginate (.Pages | shuffle ) 20).Pages }}
- {{template "showcase_items" .}}
- {{ end }}
-
-
- {{/* pagination.html: https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/template_embedded.go#L117 */}}
- {{ template "_internal/pagination.html" . }}
-
-
The Showcase articles are copyrighted by their respective content authors. Any open source license will be attached.
-
-{{ end }}
-
-
-{{define "showcase_items"}}
-
-
- {{ $img := (.Resources.ByType "image").GetMatch "*featured*" }}
- {{ with $img }}
- {{ $big := .Fill "1024x512 top" }}
- {{ $small := $big.Resize "512x" }}
-
- {{end}}
-
{{/* the margin aligns to the bottom */}}
-
- {{- .Title -}}
-
-
-
-
-
-
-{{end}}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/showcase/single.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/showcase/single.html
deleted file mode 100644
index 5ae1e07a7..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/showcase/single.html
+++ /dev/null
@@ -1,106 +0,0 @@
-{{ define "title" }}
-Showcase: {{ .Title }}
-{{ end }}
-
-{{ define "main" }}
-
-
-
-
-
-
- {{template "sc-details" .}}
-
-
-
- {{template "sc-main-column" .}}
-
-
-
- {{template "sc-navigation" .}}
-
-
-
-
- {{/* bottom row */}}
- Last Update: {{ .Lastmod.Format "January 2, 2006" }}
- {{ partial "page-edit.html" . }}
-
- The Showcase articles are copyright the content authors. Any open source license will be attached.
-
-{{ end }}
-
-
-
-{{define "sc-main-column"}}
- {{ $img := (.Resources.ByType "image").GetMatch "*featured*" }}
- {{ with $img }}
- {{ $big := .Fill "1024x512 top" }}
- {{ $small := $big.Resize "512x" }}
-
- {{ end }}
-
- {{with .Params.byline }}
-
By {{ . | markdownify -}}
- {{ end }}
- {{with .Content}}
- {{- . -}}
- {{end}}
-
-
-{{end}}
-
-{{define "sc-details"}}
-
-
-
-
Previous/Next
- {{- partial "previous-next-links-in-section-with-title.html" . -}}
-
-
-{{end}}
-
-{{define "sc-navigation"}}
- {{$section := where .Site.RegularPages "Section" .Section}}
- {{$number_of_entries := $section | len}}
-
-{{end}}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/template-func/page.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/template-func/page.html
deleted file mode 100644
index 8b5f0da85..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/template-func/page.html
+++ /dev/null
@@ -1,55 +0,0 @@
-{{ $pkg := .Params.package}}
-{{ $funcs := index site.Data.docs.tpl.funcs $pkg }}
-
-{{ range $k, $v := $funcs }}
- {{ if $v.Description }}
- {{ $func := printf "%s.%s" $pkg $k }}
- {{ $id := $func | anchorize | safeURL }}
-
-
- {{ $func }}
-
- {{ with $v.Description }}
-
- {{ . | $.RenderString | safeHTML }}
-
- {{ end }}
-
- Syntax
-
-
- {{ $pkg }}.{{ $k }}
- {{ with $v.Args }}
-
- {{ delimit $v.Args ", "}}
-
- {{ end }}
-
-
- {{ if $v.Examples }}
-
- Examples
-
- {{ end }}
- {{ range $v.Examples }}
- {{ $input := index . 0 }}
- {{ $result := index . 1 }}
- {{ $example := printf "%s ---> %s" $input $result }}
-
- {{ highlight $example "go-html-template" "" }}
- {{ end }}
- {{ with $v.Aliases }}
-
- Aliases
-
-
- {{ delimit . ", "}}
-
- {{ end }}
- {{ end }}
-{{ end }}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/package.json b/docs/_vendor/github.com/gohugoio/gohugoioTheme/package.json
deleted file mode 100644
index 14d128910..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/package.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "name": "gohugo-default-styles",
- "version": "1.1.0",
- "description": "Default Theme for Hugo Sites",
- "main": "index.js",
- "repository": "",
- "author": "budparr",
- "license": "MIT",
- "scripts": {
- "build": "NODE_ENV=production webpack",
- "build-dev": "NODE_ENV=development webpack --progress --watch",
- "start": "npm run build-dev"
- },
- "devDependencies": {
- "clean-webpack-plugin": "^1.0.0",
- "clipboard": "^2.0.4",
- "css-loader": "^1.0.1",
- "docsearch.js": "^2.6.1",
- "file-loader": "^2.0.0",
- "glob-all": "^3.1.0",
- "lazysizes": "^5.3.2",
- "mini-css-extract-plugin": "^0.4.4",
- "postcss": "^7.0.5",
- "postcss-cssnext": "^3.1.0",
- "postcss-import": "^12.0.1",
- "postcss-loader": "^3.0.0",
- "purgecss-webpack-plugin": "^1.3.1",
- "scrolldir": "^1.4.0",
- "tachyons": "^4.7.0",
- "typeface-muli": "0.0.54",
- "webpack": "^4.25.1",
- "webpack-command": "^0.4.2"
- }
-}
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/apple-touch-icon.png b/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/apple-touch-icon.png
deleted file mode 100644
index ecf1fc020..000000000
Binary files a/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/apple-touch-icon.png and /dev/null differ
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/browserconfig.xml b/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/browserconfig.xml
deleted file mode 100644
index 62400c5f2..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/browserconfig.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
- #2d89ef
-
-
-
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/dist/app.bundle.js b/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/dist/app.bundle.js
deleted file mode 100644
index 6391e71e9..000000000
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/dist/app.bundle.js
+++ /dev/null
@@ -1,22 +0,0 @@
-!function(t){function e(r){if(n[r])return n[r].exports;var i=n[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var n={};e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:r})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=11)}([function(t,e,n){"use strict";var r=function(t){var e=document.createElement("a");return e.className="header-link",e.href="#"+t,e.innerHTML=' ',e},i=function(t,e){for(var n=e.getElementsByTagName("h"+t),i=0;i0&&p.parentNode.classList.add("expand")}}catch(t){a=!0,u=t}finally{try{!s&&l.return&&l.return()}finally{if(a)throw u}}}},function(t,e,n){"use strict";n(13)({apiKey:"167e7998590aebda7f9fedcf86bc4a55",indexName:"hugodocs",inputSelector:"#search-input",debug:!0})},function(t,e,n){"use strict";n(14),n(15)},function(t,e,n){"use strict";function r(){for(var t=this.dataset.target.split(" "),e=document.querySelector(".mobilemenu:not(.dn)"),n=document.querySelector(".desktopmenu:not(.dn)"),r=document.querySelector(".desktopmenu:not(.dn)"),i=0;i=0?function(){var t=window.pageYOffset;(t>=i-s||window.innerHeight+t>=document.body.offsetHeight)&&clearInterval(u)}:function(){window.pageYOffset<=(i||0)&&clearInterval(u)};var u=setInterval(a,16)},e=document.querySelectorAll("#TableOfContents ul li a");[].forEach.call(e,function(e){e.addEventListener("click",function(n){n.preventDefault();var r=e.getAttribute("href"),i=document.querySelector(r),o=e.getAttribute("data-speed");i&&t(i,o||500)},!1)})}}()},function(t,e,n){"use strict";function r(t){if(t.target){t.preventDefault();var e=t.currentTarget,n=e.getAttribute("data-toggle-tab")}else var n=t;window.localStorage&&window.localStorage.setItem("configLangPref",n);for(var r=document.querySelectorAll("[data-toggle-tab='"+n+"']"),i=document.querySelectorAll("[data-pane='"+n+"']"),a=0;a0&&void 0!==arguments[0]?arguments[0]:{};this.action=t.action,this.container=t.container,this.emitter=t.emitter,this.target=t.target,this.text=t.text,this.trigger=t.trigger,this.selectedText=""}},{key:"initSelection",value:function(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function(){var t=this,e="rtl"==document.documentElement.getAttribute("dir");this.removeFake(),this.fakeHandlerCallback=function(){return t.removeFake()},this.fakeHandler=this.container.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[e?"right":"left"]="-9999px";var n=window.pageYOffset||document.documentElement.scrollTop;this.fakeElem.style.top=n+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,this.container.appendChild(this.fakeElem),this.selectedText=(0,r.default)(this.fakeElem),this.copyText()}},{key:"removeFake",value:function(){this.fakeHandler&&(this.container.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(this.container.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function(){this.selectedText=(0,r.default)(this.target),this.copyText()}},{key:"copyText",value:function(){var t=void 0;try{t=document.execCommand(this.action)}catch(e){t=!1}this.handleResult(t)}},{key:"handleResult",value:function(t){this.emitter.emit(t?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function(){this.trigger&&this.trigger.focus(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function(){this.removeFake()}},{key:"action",set:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"copy";if(this._action=t,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function(){return this._action}},{key:"target",set:function(t){if(void 0!==t){if(!t||"object"!==(void 0===t?"undefined":i(t))||1!==t.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&t.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(t.hasAttribute("readonly")||t.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=t}},get:function(){return this._target}}]),t}();t.exports=s})},{select:5}],8:[function(e,n,r){!function(i,o){if("function"==typeof t&&t.amd)t(["module","./clipboard-action","tiny-emitter","good-listener"],o);else if(void 0!==r)o(n,e("./clipboard-action"),e("tiny-emitter"),e("good-listener"));else{var s={exports:{}};o(s,i.clipboardAction,i.tinyEmitter,i.goodListener),i.clipboard=s.exports}}(this,function(t,e,n,r){"use strict";function i(t){return t&&t.__esModule?t:{default:t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function s(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function a(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}function u(t,e){var n="data-clipboard-"+t;if(e.hasAttribute(n))return e.getAttribute(n)}var c=i(e),l=i(n),h=i(r),f="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},p=function(){function t(t,e){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};this.action="function"==typeof t.action?t.action:this.defaultAction,this.target="function"==typeof t.target?t.target:this.defaultTarget,this.text="function"==typeof t.text?t.text:this.defaultText,this.container="object"===f(t.container)?t.container:document.body}},{key:"listenClick",value:function(t){var e=this;this.listener=(0,h.default)(t,"click",function(t){return e.onClick(t)})}},{key:"onClick",value:function(t){var e=t.delegateTarget||t.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new c.default({action:this.action(e),target:this.target(e),text:this.text(e),container:this.container,trigger:e,emitter:this})}},{key:"defaultAction",value:function(t){return u("action",t)}},{key:"defaultTarget",value:function(t){var e=u("target",t);if(e)return document.querySelector(e)}},{key:"defaultText",value:function(t){return u("text",t)}},{key:"destroy",value:function(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:["copy","cut"],e="string"==typeof t?[t]:t,n=!!document.queryCommandSupported;return e.forEach(function(t){n=n&&!!document.queryCommandSupported(t)}),n}}]),e}(l.default);t.exports=d})},{"./clipboard-action":7,"good-listener":4,"tiny-emitter":6}]},{},[8])(8)})},function(t,e,n){/*! docsearch 2.4.1 | © Algolia | github.com/algolia/docsearch */
-!function(e,n){t.exports=n()}(0,function(){return function(t){function e(r){if(n[r])return n[r].exports;var i=n[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:r})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=46)}([function(t,e,n){"use strict";function r(t){return t.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}var i=n(1);t.exports={isArray:null,isFunction:null,isObject:null,bind:null,each:null,map:null,mixin:null,isMsie:function(){return!!/(msie|trident)/i.test(navigator.userAgent)&&navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2]},escapeRegExChars:function(t){return t.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},isNumber:function(t){return"number"==typeof t},toStr:function(t){return void 0===t||null===t?"":t+""},cloneDeep:function(t){var e=this.mixin({},t),n=this;return this.each(e,function(t,r){t&&(n.isArray(t)?e[r]=[].concat(t):n.isObject(t)&&(e[r]=n.cloneDeep(t)))}),e},error:function(t){throw new Error(t)},every:function(t,e){var n=!0;return t?(this.each(t,function(r,i){if(!(n=e.call(null,r,i,t)))return!1}),!!n):n},any:function(t,e){var n=!1;return t?(this.each(t,function(r,i){if(e.call(null,r,i,t))return n=!0,!1}),n):n},getUniqueId:function(){var t=0;return function(){return t++}}(),templatify:function(t){if(this.isFunction(t))return t;var e=i.element(t);return"SCRIPT"===e.prop("tagName")?function(){return e.text()}:function(){return String(t)}},defer:function(t){setTimeout(t,0)},noop:function(){},formatPrefix:function(t,e){return e?"":t+"-"},className:function(t,e,n){return(n?"":".")+t+e},escapeHighlightedString:function(t,e,n){e=e||"";var i=document.createElement("div");i.appendChild(document.createTextNode(e)),n=n||" ";var o=document.createElement("div");o.appendChild(document.createTextNode(n));var s=document.createElement("div");return s.appendChild(document.createTextNode(t)),s.innerHTML.replace(RegExp(r(i.innerHTML),"g"),e).replace(RegExp(r(o.innerHTML),"g"),n)}}},function(t,e,n){"use strict";t.exports={element:null}},function(t,e){var n=Object.prototype.hasOwnProperty,r=Object.prototype.toString;t.exports=function(t,e,i){if("[object Function]"!==r.call(e))throw new TypeError("iterator must be a function");var o=t.length;if(o===+o)for(var s=0;s was loaded but did not call our provided callback"),JSONPScriptError:i("JSONPScriptError","
-{{< /code >}}
+```
The delimiters above must match the delimiters in your site configuration.
-###### Step 3
+### Step 3
Conditionally call the partial template from the base template.
-{{< code file=layouts/_default/baseof.html >}}
+```go-html-template {file="layouts/_default/baseof.html"}
...
{{ if .Param "math" }}
@@ -116,15 +116,15 @@ Conditionally call the partial template from the base template.
{{ end }}
...
-{{< /code >}}
+```
The example above loads the partial template if you have set the `math` parameter in front matter to `true`. If you have not set the `math` parameter in front matter, the conditional statement falls back to the `math` parameter in your site configuration.
-###### Step 4
+### Step 4
-Include mathematical equations and expressions in your markdown using LaTeX or TeX typesetting syntax.
+Include mathematical equations and expressions in Markdown using LaTeX markup.
-{{< code file=content/math-examples.md copy=true >}}
+```text {file="content/math-examples.md" copy=true}
This is an inline \(a^*=x-b^*\) equation.
These are block equations:
@@ -137,7 +137,7 @@ These are block equations:
a^*=x-b^*
\]
-These are block equations using alternate delimiters:
+These are also block equations:
$$a^*=x-b^*$$
@@ -146,14 +146,15 @@ $$ a^*=x-b^* $$
$$
a^*=x-b^*
$$
-{{< /code >}}
+```
If you set the `math` parameter to `false` in your site configuration, you must set the `math` parameter to `true` in front matter. For example:
{{< code-toggle file=content/math-examples.md fm=true >}}
title = 'Math examples'
-math = true
date = 2024-01-24T18:09:49-08:00
+[params]
+math = true
{{< /code-toggle >}}
## Inline delimiters
@@ -166,26 +167,40 @@ If you add the `$...$` delimiter pair to your configuration and JavaScript, you
A \\$5 bill _saved_ is a \\$5 bill _earned_.
```
-{{% note %}}
-If you use the `$...$` delimiter pair for inline equations, and occasionally use the `$` symbol outside of math contexts, you must use MathJax instead of KaTeX to avoid unintended formatting caused by [this KaTeX limitation](https://github.com/KaTeX/KaTeX/issues/437).
-{{% /note %}}
+> [!note]
+> If you use the `$...$` delimiter pair for inline equations, and occasionally use the `$` symbol outside of math contexts, you must use MathJax instead of KaTeX to avoid unintended formatting caused by [this KaTeX limitation](https://github.com/KaTeX/KaTeX/issues/437).
## Engines
-MathJax and KaTeX are open-source JavaScript display engines. Both engines are fast, but at the time of this writing MathJax v3.2.2 is slightly faster than KaTeX v0.16.9.
+MathJax and KaTeX are open-source JavaScript display engines. Both engines are fast, but at the time of this writing MathJax v3.2.2 is slightly faster than KaTeX v0.16.11.
-{{% note %}}
-If you use the `$...$` delimiter pair for inline equations, and occasionally use the `$` symbol outside of math contexts, you must use MathJax instead of KaTeX to avoid unintended formatting caused by [this KaTeX limitation](https://github.com/KaTeX/KaTeX/issues/437).
-
-See the [inline delimiters](#inline-delimiters) section for details.
-{{% /note %}}
+> [!note]
+> If you use the `$...$` delimiter pair for inline equations, and occasionally use the `$` symbol outside of math contexts, you must use MathJax instead of KaTeX to avoid unintended formatting caused by [this KaTeX limitation](https://github.com/KaTeX/KaTeX/issues/437).
+>
+>See the [inline delimiters](#inline-delimiters) section for details.
To use KaTeX instead of MathJax, replace the partial template from [Step 2] with this:
-{{< code file=layouts/partials/math.html copy=true >}}
-
-
-
+```go-html-template {file="layouts/partials/math.html" copy=true}
+
+
+
-{{< /code >}}
+```
The delimiters above must match the delimiters in your site configuration.
@@ -214,14 +229,10 @@ $$C_p[\ce{H2O(l)}] = \pu{75.3 J // mol K}$$
As shown in [Step 2] above, MathJax supports chemical equations without additional configuration. To add chemistry support to KaTeX, enable the mhchem extension as described in the KaTeX [documentation](https://katex.org/docs/libs).
+[`transform.ToMath`]: /functions/transform/tomath/
[KaTeX]: https://katex.org/
[LaTeX]: https://www.latex-project.org/
[MathJax]: https://www.mathjax.org/
-[Microsoft VS Code]: https://code.visualstudio.com/
-[Obsidian]: https://obsidian.md/
-[Step 1]: #step-1
+[passthrough extension]: /configuration/markup/#passthrough
[Step 2]: #step-2
[Step 3]: #step-3
-[TeX]: https://en.wikipedia.org/wiki/TeX
-[Typora]: https://typora.io/
-[passthrough extension]: https://github.com/gohugoio/hugo-goldmark-extensions
diff --git a/docs/content/en/content-management/menus.md b/docs/content/en/content-management/menus.md
index 1f5d1ef71..6d01173dc 100644
--- a/docs/content/en/content-management/menus.md
+++ b/docs/content/en/content-management/menus.md
@@ -1,14 +1,8 @@
---
title: Menus
-description: Create menus by defining entries, localizing each entry, and rendering the resulting data structure.
-categories: [content management]
-keywords: [menus]
-menu:
- docs:
- parent: content-management
- weight: 190
-weight: 190
-toc: true
+description: Create menus by defining entries, localizing each entry, and rendering the resulting data structure.
+categories: []
+keywords: []
aliases: [/extras/menus/]
---
@@ -17,8 +11,8 @@ aliases: [/extras/menus/]
To create a menu for your site:
1. Define the menu entries
-2. [Localize] each entry
-3. Render the menu with a [template]
+1. [Localize](multilingual/#menus) each entry
+1. Render the menu with a [template]
Create multiple menus, either flat or nested. For example, create a main menu for the header, and a separate menu for the footer.
@@ -28,13 +22,12 @@ There are three ways to define menu entries:
1. In front matter
1. In site configuration
-{{% note %}}
-Although you can use these methods in combination when defining a menu, the menu will be easier to conceptualize and maintain if you use one method throughout the site.
-{{% /note %}}
+> [!note]
+> Although you can use these methods in combination when defining a menu, the menu will be easier to conceptualize and maintain if you use one method throughout the site.
## Define automatically
-To automatically define menu entries for each top-level section of your site, enable the section pages menu in your site configuration.
+To automatically define a menu entry for each top-level [section](g) of your site, enable the section pages menu in your site configuration.
{{< code-toggle file=hugo >}}
sectionPagesMenu = "main"
@@ -62,45 +55,22 @@ menus = ['main','footer']
Access the entry with `site.Menus.main` and `site.Menus.footer` in your templates. See [menu templates] for details.
-{{% note %}}
-The configuration key in the examples above is `menus`. The `menu` (singular) configuration key is an alias for `menus`.
-{{% /note %}}
+> [!note]
+> The configuration key in the examples above is `menus`. The `menu` (singular) configuration key is an alias for `menus`.
-### Properties {#properties-front-matter}
+### Properties
Use these properties when defining menu entries in front matter:
-identifier
-: (`string`) Required when two or more menu entries have the same `name`, or when localizing the `name` using translation tables. Must start with a letter, followed by letters, digits, or underscores.
+{{% include "/_common/menu-entry-properties.md" %}}
-name
-: (`string`) The text to display when rendering the menu entry.
-
-params
-: (`map`) User-defined properties for the menu entry.
-
-parent
-: (`string`) The `identifier` of the parent menu entry. If `identifier` is not defined, use `name`. Required for child entries in a nested menu.
-
-post
-: (`string`) The HTML to append when rendering the menu entry.
-
-pre
-: (`string`) The HTML to prepend when rendering the menu entry.
-
-title
-: (`string`) The HTML `title` attribute of the rendered menu entry.
-
-weight
-: (`int`) A non-zero integer indicating the entry's position relative the root of the menu, or to its parent for a child entry. Lighter entries float to the top, while heavier entries sink to the bottom.
-
-### Example {#example-front-matter}
+### Example
This front matter menu entry demonstrates some of the available properties:
{{< code-toggle file=content/products/software.md fm=true >}}
title = 'Software'
-[[menus.main]]
+[menus.main]
parent = 'Products'
weight = 20
pre = ' '
@@ -112,111 +82,7 @@ Access the entry with `site.Menus.main` in your templates. See [menu templates]
## Define in site configuration
-To define entries for the "main" menu:
-
-{{< code-toggle file=hugo >}}
-[[menus.main]]
-name = 'Home'
-pageRef = '/'
-weight = 10
-
-[[menus.main]]
-name = 'Products'
-pageRef = '/products'
-weight = 20
-
-[[menus.main]]
-name = 'Services'
-pageRef = '/services'
-weight = 30
-{{< /code-toggle >}}
-
-This creates a menu structure that you can access with `site.Menus.main` in your templates. See [menu templates] for details.
-
-To define entries for the "footer" menu:
-
-{{< code-toggle file=hugo >}}
-[[menus.footer]]
-name = 'Terms'
-pageRef = '/terms'
-weight = 10
-
-[[menus.footer]]
-name = 'Privacy'
-pageRef = '/privacy'
-weight = 20
-{{< /code-toggle >}}
-
-This creates a menu structure that you can access with `site.Menus.footer` in your templates. See [menu templates] for details.
-
-{{% note %}}
-The configuration key in the examples above is `menus`. The `menu` (singular) configuration key is an alias for `menus`.
-{{% /note %}}
-
-### Properties {#properties-site-configuration}
-
-{{% note %}}
-The [properties available to entries defined in front matter] are also available to entries defined in site configuration.
-
-[properties available to entries defined in front matter]: /content-management/menus/#properties-front-matter
-{{% /note %}}
-
-Each menu entry defined in site configuration requires two or more properties:
-
-- Specify `name` and `pageRef` for internal links
-- Specify `name` and `url` for external links
-
-pageRef
-: (`string`) The file path of the target page, relative to the `content` directory. Omit language code and file extension. Required for *internal* links.
-
-Kind|pageRef
-:--|:--
-home|`/`
-page|`/books/book-1`
-section|`/books`
-taxonomy|`/tags`
-term|`/tags/foo`
-
-url
-: (`string`) Required for *external* links.
-
-### Example {#example-site-configuration}
-
-This nested menu demonstrates some of the available properties:
-
-{{< code-toggle file=hugo >}}
-[[menus.main]]
-name = 'Products'
-pageRef = '/products'
-weight = 10
-
-[[menus.main]]
-name = 'Hardware'
-pageRef = '/products/hardware'
-parent = 'Products'
-weight = 1
-
-[[menus.main]]
-name = 'Software'
-pageRef = '/products/software'
-parent = 'Products'
-weight = 2
-
-[[menus.main]]
-name = 'Services'
-pageRef = '/services'
-weight = 20
-
-[[menus.main]]
-name = 'Hugo'
-pre = ' '
-url = 'https://gohugo.io/'
-weight = 30
-[menus.main.params]
-rel = 'external'
-{{< /code-toggle >}}
-
-This creates a menu structure that you can access with `site.Menus.main` in your templates. See [menu templates] for details.
+See [configure menus](/configuration/menus/).
## Localize
@@ -226,7 +92,6 @@ Hugo provides two methods to localize your menu entries. See [multilingual].
See [menu templates].
-[localize]: /content-management/multilingual/#menus
-[menu templates]: /templates/menu-templates/
+[menu templates]: /templates/menu/
[multilingual]: /content-management/multilingual/#menus
-[template]: /templates/menu-templates/
+[template]: /templates/menu/
diff --git a/docs/content/en/content-management/multilingual.md b/docs/content/en/content-management/multilingual.md
index ea9f71787..d419f4381 100644
--- a/docs/content/en/content-management/multilingual.md
+++ b/docs/content/en/content-management/multilingual.md
@@ -1,199 +1,15 @@
---
title: Multilingual mode
linkTitle: Multilingual
-description: Hugo supports the creation of websites with multiple languages side by side.
-categories: [content management]
-keywords: [multilingual,i18n,internationalization]
-menu:
- docs:
- parent: content-management
- weight: 230
-weight: 230
-toc: true
+description: Localize your project for each language and region, including translations, images, dates, currencies, numbers, percentages, and collation sequence. Hugo's multilingual framework supports single-host and multihost configurations.
+categories: []
+keywords: []
aliases: [/content/multilingual/,/tutorials/create-a-multilingual-site/]
---
-You should define the available languages in a `languages` section in your site configuration.
+## Configuration
-Also See [Hugo Multilingual Part 1: Content translation].
-
-## Configure languages
-
-This is the default language configuration:
-
-{{< code-toggle config=languages />}}
-
-This is an example of a site configuration for a multilingual project. Any key not defined in a `languages` object will fall back to the global value in the root of your site configuration.
-
-{{< code-toggle file=hugo >}}
-defaultContentLanguage = 'de'
-defaultContentLanguageInSubdir = true
-
-[languages.de]
-contentDir = 'content/de'
-disabled = false
-languageCode = 'de-DE'
-languageDirection = 'ltr'
-languageName = 'Deutsch'
-title = 'Projekt Dokumentation'
-weight = 1
-
-[languages.de.params]
-subtitle = 'Referenz, Tutorials und Erklärungen'
-
-[languages.en]
-contentDir = 'content/en'
-disabled = false
-languageCode = 'en-US'
-languageDirection = 'ltr'
-languageName = 'English'
-title = 'Project Documentation'
-weight = 2
-
-[languages.en.params]
-subtitle = 'Reference, Tutorials, and Explanations'
-{{< /code-toggle >}}
-
-defaultContentLanguage
-: (`string`) The project's default language tag as defined by [RFC 5646]. Must be lower case, and must match one of the defined language keys. Default is `en`. Examples:
-
-- `en`
-- `en-gb`
-- `pt-br`
-
-defaultContentLanguageInSubdir
-: (`bool`) If `true`, Hugo renders the default language site in a subdirectory matching the `defaultContentLanguage`. Default is `false`.
-
-contentDir
-: (`string`) The content directory for this language. Omit if [translating by file name].
-
-disabled
-: (`bool`) If `true`, Hugo will not render content for this language. Default is `false`.
-
-languageCode
-: (`string`) The language tag as defined by [RFC 5646]. This value may include upper and lower case characters, hyphens, or underscores, and does not affect localization or URLs. Hugo uses this value to populate the `language` element in the [built-in RSS template], and the `lang` attribute of the `html` element in the [built-in alias template]. Examples:
-
-- `en`
-- `en-GB`
-- `pt-BR`
-
-languageDirection
-: (`string`) The language direction, either left-to-right (`ltr`) or right-to-left (`rtl`). Use this value in your templates with the global [`dir`] HTML attribute.
-
-languageName
-: (`string`) The language name, typically used when rendering a language switcher.
-
-title
-: (`string`) The language title. When set, this overrides the site title for this language.
-
-weight
-: (`int`) The language weight. When set to a non-zero value, this is the primary sort criteria for this language.
-
-[`dir`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir
-[built-in RSS template]: https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/_default/rss.xml
-[built-in alias template]: https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/alias.html
-[RFC 5646]: https://datatracker.ietf.org/doc/html/rfc5646
-[translating by file name]: #translation-by-file-name
-
-### Changes in Hugo 0.112.0
-
-{{< new-in 0.112.0 >}}
-
-In Hugo `v0.112.0` we consolidated all configuration options, and improved how the languages and their parameters are merged with the main configuration. But while testing this on Hugo sites out there, we received some error reports and reverted some of the changes in favor of deprecation warnings:
-
-1. `site.Language.Params` is deprecated. Use `site.Params` directly.
-1. Adding custom parameters to the top level language configuration is deprecated. Define custom parameters within `languages.xx.params`. See `color` in the example below.
-
-{{< code-toggle file=hugo >}}
-
-title = "My blog"
-languageCode = "en-us"
-
-[languages]
-[languages.sv]
-title = "Min blogg"
-languageCode = "sv"
-[languages.en.params]
-color = "blue"
-{{< /code-toggle >}}
-
-In the example above, all settings except `color` below `params` map to predefined configuration options in Hugo for the site and its language, and should be accessed via the documented accessors:
-
-```go-html-template
-{{ site.Title }}
-{{ site.LanguageCode }}
-{{ site.Params.color }}
-```
-
-### Disable a language
-
-To disable a language within a `languages` object in your site configuration:
-
-{{< code-toggle file=hugo >}}
-[languages.es]
-disabled = true
-{{< /code-toggle >}}
-
-To disable one or more languages in the root of your site configuration:
-
-{{< code-toggle file=hugo >}}
-disableLanguages = ["es", "fr"]
-{{< /code-toggle >}}
-
-To disable one or more languages using an environment variable:
-
-```sh
-HUGO_DISABLELANGUAGES="es fr" hugo
-```
-
-Note that you cannot disable the default content language.
-
-### Configure multilingual multihost
-
-From **Hugo 0.31** we support multiple languages in a multihost configuration. See [this issue](https://github.com/gohugoio/hugo/issues/4027) for details.
-
-This means that you can now configure a `baseURL` per `language`:
-
-{{% note %}}
-If a `baseURL` is set on the `language` level, then all languages must have one and they must all be different.
-{{% /note %}}
-
-Example:
-
-{{< code-toggle file=hugo >}}
-[languages]
-[languages.fr]
-baseURL = "https://example.fr"
-languageName = "Français"
-weight = 1
-title = "En Français"
-
-[languages.en]
-baseURL = "https://example.org/"
-languageName = "English"
-weight = 2
-title = "In English"
-{{ code-toggle >}}
-
-With the above, the two sites will be generated into `public` with their own root:
-
-```text
-public
-├── en
-└── fr
-```
-
-**All URLs (i.e `.Permalink` etc.) will be generated from that root. So the English home page above will have its `.Permalink` set to `https://example.org/`.**
-
-When you run `hugo server` we will start multiple HTTP servers. You will typically see something like this in the console:
-
-```text
-Web Server is available at 127.0.0.1:1313 (bind address 127.0.0.1) fr
-Web Server is available at 127.0.0.1:1314 (bind address 127.0.0.1) en
-Press Ctrl+C to stop
-```
-
-Live reload and `--navigateToChanged` between the servers work as expected.
+See [configure languages](/configuration/languages/).
## Translate your content
@@ -204,7 +20,7 @@ There are two ways to manage your content translations. Both ensure each page is
Considering the following example:
1. `/content/about.en.md`
-2. `/content/about.fr.md`
+1. `/content/about.fr.md`
The first file is assigned the English language and is linked to the second.
The second file is assigned the French language and is linked to the first.
@@ -213,13 +29,12 @@ Their language is __assigned__ according to the language code added as a __suffi
By having the same **path and base file name**, the content pieces are __linked__ together as translated pages.
-{{% note %}}
-If a file has no language code, it will be assigned the default language.
-{{% /note %}}
+> [!note]
+> If a file has no language code, it will be assigned the default language.
### Translation by content directory
-This system uses different content directories for each of the languages. Each language's content directory is set using the `contentDir` parameter.
+This system uses different content directories for each of the languages. Each language's `content` directory is set using the `contentDir` parameter.
{{< code-toggle file=hugo >}}
languages:
@@ -238,14 +53,14 @@ The value of `contentDir` can be any valid path -- even absolute path references
Considering the following example in conjunction with the configuration above:
1. `/content/english/about.md`
-2. `/content/french/about.md`
+1. `/content/french/about.md`
The first file is assigned the English language and is linked to the second.
The second file is assigned the French language and is linked to the first.
-Their language is __assigned__ according to the content directory they are __placed__ in.
+Their language is __assigned__ according to the `content` directory they are __placed__ in.
-By having the same **path and basename** (relative to their language content directory), the content pieces are __linked__ together as translated pages.
+By having the same **path and basename** (relative to their language `content` directory), the content pieces are __linked__ together as translated pages.
### Bypassing default linking
@@ -254,10 +69,10 @@ Any pages sharing the same `translationKey` set in front matter will be linked a
Considering the following example:
1. `/content/about-us.en.md`
-2. `/content/om.nn.md`
-3. `/content/presentation/a-propos.fr.md`
+1. `/content/om.nn.md`
+1. `/content/presentation/a-propos.fr.md`
-{{< code-toggle >}}
+{{< code-toggle file=hugo >}}
translationKey: "about"
{{< /code-toggle >}}
@@ -272,9 +87,6 @@ To localize URLs:
- For a regular page, set either [`slug`] or [`url`] in front matter
- For a section page, set [`url`] in front matter
-[`slug`]: /content-management/urls/#slug
-[`url`]: /content-management/urls/#url
-
For example, a French translation can have its own localized slug.
{{< code-toggle file=content/about.fr.md fm=true >}}
@@ -286,37 +98,36 @@ At render, Hugo will build both `/about/` and `/fr/a-propos/` without affecting
### Page bundles
-To avoid the burden of having to duplicate files, each Page Bundle inherits the resources of its linked translated pages' bundles except for the content files (Markdown files, HTML files etc...).
+To avoid the burden of having to duplicate files, each Page Bundle inherits the resources of its linked translated pages' bundles except for the content files (Markdown files, HTML files etc.).
Therefore, from within a template, the page will have access to the files from all linked pages' bundles.
If, across the linked bundles, two or more files share the same basename, only one will be included and chosen as follows:
-* File from current language bundle, if present.
-* First file found across bundles by order of language `Weight`.
+- File from current language bundle, if present.
+- First file found across bundles by order of language `Weight`.
-{{% note %}}
-Page Bundle resources follow the same language assignment logic as content files, both by file name (`image.jpg`, `image.fr.jpg`) and by directory (`english/about/header.jpg`, `french/about/header.jpg`).
-{{%/ note %}}
+> [!note]
+> Page Bundle resources follow the same language assignment logic as content files, both by file name (`image.jpg`, `image.fr.jpg`) and by directory (`english/about/header.jpg`, `french/about/header.jpg`).
## Reference translated content
To create a list of links to translated content, use a template similar to the following:
-{{< code file=layouts/partials/i18nlist.html >}}
+```go-html-template {file="layouts/partials/i18nlist.html"}
{{ if .IsTranslated }}
{{ i18n "translations" }}
{{ end }}
-{{< /code >}}
+```
-The above can be put in a `partial` (i.e., inside `layouts/partials/`) and included in any template, whether a [single content page][contenttemplate] or the [homepage]. It will not print anything if there are no translations for a given page.
+The above can be put in a `partial` (i.e., inside `layouts/partials/`) and included in any template. It will not print anything if there are no translations for a given page.
The above also uses the [`i18n` function][i18func] described in the next section.
@@ -324,106 +135,17 @@ The above also uses the [`i18n` function][i18func] described in the next section
`.AllTranslations` on a `Page` can be used to list all translations, including the page itself. On the home page it can be used to build a language navigator:
-{{< code file=layouts/partials/allLanguages.html >}}
+```go-html-template {file="layouts/partials/allLanguages.html"}
-{{< /code >}}
+```
## Translation of strings
-Hugo uses [go-i18n] to support string translations. [See the project's source repository][go-i18n-source] to find tools that will help you manage your translation workflows.
-
-Translations are collected from the `themes//i18n/` folder (built into the theme), as well as translations present in `i18n/` at the root of your project. In the `i18n`, the translations will be merged and take precedence over what is in the theme folder. Language files should be named according to [RFC 5646] with names such as `en-US.toml`, `fr.toml`, etc.
-
-Artificial languages with private use subtags as defined in [RFC 5646 § 2.2.7](https://datatracker.ietf.org/doc/html/rfc5646#section-2.2.7) are also supported. You may omit the `art-x-` prefix for brevity. For example:
-
-```text
-art-x-hugolang
-hugolang
-```
-
-Private use subtags must not exceed 8 alphanumeric characters.
-
-### Query basic translation
-
-From within your templates, use the [`i18n`] function like this:
-
-[`i18n`]: /functions/lang/translate
-
-```go-html-template
-{{ i18n "home" }}
-```
-
-The function will search for the `"home"` id:
-
-{{< code-toggle file=i18n/en-US >}}
-[home]
-other = "Home"
-{{< /code-toggle >}}
-
-The result will be
-
-```text
-Home
-```
-
-### Query a flexible translation with variables
-
-Often you will want to use the page variables in the translation strings. To do so, pass the `.` context when calling `i18n`:
-
-```go-html-template
-{{ i18n "wordCount" . }}
-```
-
-The function will pass the `.` context to the `"wordCount"` id:
-
-{{< code-toggle file=i18n/en-US >}}
-[wordCount]
-other = "This article has {{ .WordCount }} words."
-{{< /code-toggle >}}
-
-Assume `.WordCount` in the context has value is 101. The result will be:
-
-```text
-This article has 101 words.
-```
-
-### Query a singular/plural translation
-
-To enable pluralization when translating, pass a map with a numeric `.Count` property to the `i18n` function. The example below uses `.ReadingTime` variable which has a built-in `.Count` property.
-
-```go-html-template
-{{ i18n "readingTime" .ReadingTime }}
-```
-
-The function will read `.Count` from `.ReadingTime` and evaluate whether the number is singular (`one`) or plural (`other`). After that, it will pass to `readingTime` id in `i18n/en-US.toml` file:
-
-{{< code-toggle file=i18n/en-US >}}
-[readingTime]
-one = "One minute to read"
-other = "{{ .Count }} minutes to read"
-{{< /code-toggle >}}
-
-Assuming `.ReadingTime.Count` in the context has value is 525600. The result will be:
-
-```text
-525600 minutes to read
-```
-
-If `.ReadingTime.Count` in the context has value is 1. The result is:
-
-```text
-One minute to read
-```
-
-In case you need to pass a custom data: (`(dict "Count" numeric_value_only)` is minimum requirement)
-
-```go-html-template
-{{ i18n "readingTime" (dict "Count" 25 "FirstArgument" true "SecondArgument" false "Etc" "so on, so far") }}
-```
+See the [`lang.Translate`] template function.
## Localization
@@ -452,7 +174,7 @@ weight = 3
With this front matter:
-{{< code-toggle >}}
+{{< code-toggle file=hugo >}}
date = 2021-11-03T12:34:56+01:00
{{< /code-toggle >}}
@@ -606,8 +328,6 @@ pageRef = '/services'
weight = 20
{{< /code-toggle >}}
-[configuration directory]: /getting-started/configuration/#configuration-directory
-
### Use translation tables
When rendering the text that appears in menu each entry, the [example menu template] does this:
@@ -645,20 +365,14 @@ products = 'Produkte'
services = 'Leistungen'
{{< / code-toggle >}}
-[example menu template]: /templates/menu-templates/#example
-[automatically]: /content-management/menus/#define-automatically
-[in front matter]: /content-management/menus/#define-in-front-matter
-[in site configuration]: /content-management/menus/#define-in-site-configuration
-
## Missing translations
If a string does not have a translation for the current language, Hugo will use the value from the default language. If no default value is set, an empty string will be shown.
While translating a Hugo website, it can be handy to have a visual indicator of missing translations. The [`enableMissingTranslationPlaceholders` configuration option][config] will flag all untranslated strings with the placeholder `[i18n] identifier`, where `identifier` is the id of the missing translation.
-{{% note %}}
-Hugo will generate your website with these missing translation placeholders. It might not be suitable for production environments.
-{{% /note %}}
+> [!note]
+> Hugo will generate your website with these missing translation placeholders. It might not be suitable for production environments.
For merging of content from other languages (i.e. missing content translations), see [lang.Merge].
@@ -673,10 +387,10 @@ i18n|MISSING_TRANSLATION|en|wordCount
To support Multilingual mode in your themes, some considerations must be taken for the URLs in the templates. If there is more than one language, URLs must meet the following criteria:
-* Come from the built-in `.Permalink` or `.RelPermalink`
-* Be constructed with the [`relLangURL`] or [`absLangURL`] template function, or be prefixed with `{{ .LanguagePrefix }}`
+- Come from the built-in `.Permalink` or `.RelPermalink`
+- Be constructed with the [`relLangURL`] or [`absLangURL`] template function, or be prefixed with `{{ .LanguagePrefix }}`
-If there is more than one language defined, the `LanguagePrefix` variable will equal `/en` (or whatever your `CurrentLanguage` is). If not enabled, it will be an empty string (and is therefore harmless for single-language Hugo websites).
+If there is more than one language defined, the `LanguagePrefix` method will return `/en` (or whatever the current language is). If not enabled, it will be an empty string (and is therefore harmless for single-language Hugo websites).
## Generate multilingual content with `hugo new content`
@@ -694,23 +408,22 @@ hugo new content content/en/post/test.md
hugo new content content/de/post/test.md
```
-[`abslangurl`]: /functions/urls/abslangurl
-[config]: /getting-started/configuration/
-[contenttemplate]: /templates/single-page-templates/
-[go-i18n-source]: https://github.com/nicksnyder/go-i18n
-[go-i18n]: https://github.com/nicksnyder/go-i18n
-[homepage]: /templates/homepage/
-[Hugo Multilingual Part 1: Content translation]: https://regisphilibert.com/blog/2018/08/hugo-multilingual-part-1-managing-content-translation/
-[i18func]: /functions/lang/translate
-[lang.FormatAccounting]: /functions/lang/formataccounting
-[lang.FormatCurrency]: /functions/lang/formatcurrency
-[lang.FormatNumber]: /functions/lang/formatnumber
-[lang.FormatNumberCustom]: /functions/lang/formatnumbercustom
-[lang.FormatPercent]: /functions/lang/formatpercent
+[`absLangURL`]: /functions/urls/abslangurl/
+[`lang.Translate`]: /functions/lang/translate
+[`relLangURL`]: /functions/urls/rellangurl/
+[`slug`]: /content-management/urls/#slug
+[`time.Format`]: /functions/time/format/
+[`url`]: /content-management/urls/#url
+[automatically]: /content-management/menus/#define-automatically
+[config]: /configuration/
+[configuration directory]: /configuration/introduction/#configuration-directory
+[example menu template]: /templates/menu/#example
+[i18func]: /functions/lang/translate/
+[in front matter]: /content-management/menus/#define-in-front-matter
+[in site configuration]: /content-management/menus/#define-in-site-configuration
+[lang.FormatAccounting]: /functions/lang/formataccounting/
+[lang.FormatCurrency]: /functions/lang/formatcurrency/
+[lang.FormatNumber]: /functions/lang/formatnumber/
+[lang.FormatNumberCustom]: /functions/lang/formatnumbercustom/
+[lang.FormatPercent]: /functions/lang/formatpercent/
[lang.Merge]: /functions/lang/merge/
-[menus]: /content-management/menus/
-[OS environment]: /getting-started/configuration/#configure-with-environment-variables
-[`rellangurl`]: /functions/urls/rellangurl
-[RFC 5646]: https://tools.ietf.org/html/rfc5646
-[single page templates]: /templates/single-page-templates/
-[`time.Format`]: /functions/time/format
diff --git a/docs/content/en/content-management/organization/1-featured-content-bundles.png b/docs/content/en/content-management/organization/1-featured-content-bundles.png
deleted file mode 100644
index 501e671e2..000000000
Binary files a/docs/content/en/content-management/organization/1-featured-content-bundles.png and /dev/null differ
diff --git a/docs/content/en/content-management/organization/index.md b/docs/content/en/content-management/organization/index.md
index 22b341fcf..a7682bfad 100644
--- a/docs/content/en/content-management/organization/index.md
+++ b/docs/content/en/content-management/organization/index.md
@@ -2,14 +2,8 @@
title: Content organization
linkTitle: Organization
description: Hugo assumes that the same structure that works to organize your source content is used to organize the rendered site.
-categories: [content management,fundamentals]
-keywords: [sections,content,organization,bundle,resources]
-menu:
- docs:
- parent: content-management
- weight: 20
-weight: 20
-toc: true
+categories: []
+keywords: []
aliases: [/content/sections/]
---
@@ -19,13 +13,28 @@ Hugo `0.32` announced page-relative images and other resources packaged into `Pa
These terms are connected, and you also need to read about [Page Resources](/content-management/page-resources) and [Image Processing](/content-management/image-processing) to get the full picture.
-{{< imgproc "1-featured-content-bundles.png" "resize 300x" >}}
-The illustration shows three bundles. Note that the home page bundle cannot contain other content pages, although other files (images etc.) are allowed.
-{{< /imgproc >}}
+```text
+content/
+├── blog/
+│ ├── hugo-is-cool/
+│ │ ├── images/
+│ │ │ ├── funnier-cat.jpg
+│ │ │ └── funny-cat.jpg
+│ │ ├── cats-info.md
+│ │ └── index.md
+│ ├── posts/
+│ │ ├── post1.md
+│ │ └── post2.md
+│ ├── 1-landscape.jpg
+│ ├── 2-sunset.jpg
+│ ├── _index.md
+│ ├── content-1.md
+│ └── content-2.md
+├── 1-logo.png
+└── _index.md
+```
-{{% note %}}
-The bundle documentation is a **work in progress**. We will publish more comprehensive docs about this soon.
-{{% /note %}}
+The file tree above shows three bundles. Note that the home page bundle cannot contain other content pages, although other files (images etc.) are allowed.
## Organization of content source
@@ -52,24 +61,23 @@ Without any additional configuration, the following will automatically work:
## Path breakdown in Hugo
-The following demonstrates the relationships between your content organization and the output URL structure for your Hugo website when it renders. These examples assume you are [using pretty URLs][pretty], which is the default behavior for Hugo. The examples also assume a key-value of `baseURL = "https://example.org"` in your [site's configuration file][config].
+The following demonstrates the relationships between your content organization and the output URL structure for your Hugo website when it renders. These examples assume you are [using pretty URLs][pretty], which is the default behavior for Hugo. The examples also assume a key-value of `baseURL = "https://example.org/"` in your [site's configuration file][config].
### Index pages: `_index.md`
-`_index.md` has a special role in Hugo. It allows you to add front matter and content to your [list templates][lists]. These templates include those for [section templates], [taxonomy templates], [taxonomy terms templates], and your [homepage template].
+`_index.md` has a special role in Hugo. It allows you to add front matter and content to `home`, `section`, `taxonomy`, and `term` pages.
-{{% note %}}
-**Tip:** You can get a reference to the content and metadata in `_index.md` using the [`.Site.GetPage` function](/methods/page/getpage).
-{{% /note %}}
+> [!note]
+> Access the content and metadata within an `_index.md` file by invoking the `GetPage` method on a `Site` or `Page` object.
-You can create one `_index.md` for your homepage and one in each of your content sections, taxonomies, and taxonomy terms. The following shows typical placement of an `_index.md` that would contain content and front matter for a `posts` section list page on a Hugo website:
+You can create one `_index.md` for your home page and one in each of your content sections, taxonomies, and terms. The following shows typical placement of an `_index.md` that would contain content and front matter for a `posts` section list page on a Hugo website:
```txt
. url
. ⊢--^-⊣
. path slug
. ⊢--^-⊣⊢---^---⊣
-. filepath
+. file path
. ⊢------^------⊣
content/posts/_index.md
```
@@ -91,7 +99,7 @@ The [sections] can be nested as deeply as you want. The important thing to under
### Single pages in sections
-Single content files in each of your sections will be rendered as [single page templates][singles]. Here is an example of a single `post` within `posts`:
+Single content files in each of your sections will be rendered by a [single template]. Here is an example of a single `post` within `posts`:
```txt
path ("posts/my-first-hugo-post.md")
@@ -128,27 +136,16 @@ The `slug` is the last segment of the URL path, defined by the file name and opt
### `path`
-A content's `path` is determined by the section's path to the file. The file `path`
+A content's `path` is determined by the section's path to the file. The file `path`:
-* is based on the path to the content's location AND
-* does not include the slug
+- Is based on the path to the content's location AND
+- Does not include the slug
### `url`
The `url` is the entire URL path, defined by the file path and optionally overridden by a `url` value in front matter. See [URL Management](/content-management/urls/#slug) for details.
-[config]: /getting-started/configuration/
-[formats]: /content-management/formats/
-[front matter]: /content-management/front-matter/
-[getpage]: /methods/page/getpage
-[homepage template]: /templates/homepage/
-[homepage]: /templates/homepage/
-[lists]: /templates/lists/
+[config]: /configuration/
[pretty]: /content-management/urls/#appearance
-[section templates]: /templates/section-templates/
[sections]: /content-management/sections/
-[singles]: /templates/single-page-templates/
-[taxonomy templates]: /templates/taxonomy-templates/
-[taxonomy terms templates]: /templates/taxonomy-templates/
-[types]: /content-management/types/
-[urls]: /content-management/urls/
+[single template]: /templates/types/#single
diff --git a/docs/content/en/content-management/page-bundles.md b/docs/content/en/content-management/page-bundles.md
index 860fff2bb..f6a5cf771 100644
--- a/docs/content/en/content-management/page-bundles.md
+++ b/docs/content/en/content-management/page-bundles.md
@@ -1,183 +1,145 @@
---
title: Page bundles
-description: Content organization using Page Bundles
-categories: [content management]
-keywords: [page,bundle,leaf,branch]
-menu :
- docs:
- parent: content-management
- weight: 30
-weight: 30
-toc: true
+description: Use page bundles to logically associate one or more resources with content.
+categories: []
+keywords: []
---
-Page Bundles are a way to group [Page Resources](/content-management/page-resources/).
+## Introduction
-A Page Bundle can be one of:
+A page bundle is a directory that encapsulates both content and associated resources.
-- Leaf Bundle (leaf means it has no children)
-- Branch Bundle (home page, section, taxonomy terms, taxonomy list)
+By way of example, this site has an "about" page and a "privacy" page:
-| | Leaf Bundle | Branch Bundle |
-|-------------------------------------|----------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Usage | Collection of content and attachments for single pages | Collection of attachments for section pages (home page, section, taxonomy terms, taxonomy list) |
-| Index file name | `index.md` [^fn:1] | `_index.md` [^fn:1] |
-| Allowed Resources | Page and non-page (like images, PDF, etc.) types | Only non-page (like images, PDF, etc.) types |
-| Where can the Resources live? | At any directory level within the leaf bundle directory. | Only in the directory level **of** the branch bundle directory i.e. the directory containing the `_index.md` ([ref](https://discourse.gohugo.io/t/question-about-content-folder-structure/11822/4?u=kaushalmodi)). |
-| Layout type | [`single`](/templates/single-page-templates/) | [`list`](/templates/lists) |
-| Nesting | Does not allow nesting of more bundles under it | Allows nesting of leaf or branch bundles under it |
-| Example | `content/posts/my-post/index.md` | `content/posts/_index.md` |
-| Content from non-index page files...| Accessed only as page resources | Accessed only as regular pages |
+```text
+content/
+├── about/
+│ ├── index.md
+│ └── welcome.jpg
+└── privacy.md
+```
+
+The "about" page is a page bundle. It logically associates a resource with content by bundling them together. Resources within a page bundle are [page resources], accessible with the [`Resources`] method on the `Page` object.
+
+Page bundles are either _leaf bundles_ or _branch bundles_.
+
+leaf bundle
+: A _leaf bundle_ is a directory that contains an `index.md` file and zero or more resources. Analogous to a physical leaf, a leaf bundle is at the end of a branch. It has no descendants.
+
+branch bundle
+: A _branch bundle_ is a directory that contains an `_index.md` file and zero or more resources. Analogous to a physical branch, a branch bundle may have descendants including leaf bundles and other branch bundles. Top-level directories with or without `_index.md` files are also branch bundles. This includes the home page.
+
+> [!note]
+> In the definitions above and the examples below, the extension of the index file depends on the [content format](g). For example, use `index.md` for Markdown content, `index.html` for HTML content, `index.adoc` for AsciiDoc content, etc.
+
+## Comparison
+
+Page bundle characteristics vary by bundle type.
+
+| | Leaf bundle | Branch bundle |
+|---------------------|---------------------------------------------------------|---------------------------------------------------------|
+| Index file | `index.md` | `_index.md` |
+| Example | `content/about/index.md` | `content/posts/_index.md ` |
+| [Page kinds](g) | `page` | `home`, `section`, `taxonomy`, or `term` |
+| Template types | [single] | [home], [section], [taxonomy], or [term] |
+| Descendant pages | None | Zero or more |
+| Resource location | Adjacent to the index file or in a nested subdirectory | Same as a leaf bundles, but excludes descendant bundles |
+| [Resource types](g) | `page`, `image`, `video`, etc. | all but `page` |
+
+Files with [resource type](g) `page` include content written in Markdown, HTML, AsciiDoc, Pandoc, reStructuredText, and Emacs Org Mode. In a leaf bundle, excluding the index file, these files are only accessible as page resources. In a branch bundle, these files are only accessible as content pages.
## Leaf bundles
-A _Leaf Bundle_ is a directory at any hierarchy within the `content/`
-directory, that contains an **`index.md`** file.
-
-### Examples of leaf bundle organization {#examples-of-leaf-bundle-organization}
+A _leaf bundle_ is a directory that contains an `index.md` file and zero or more resources. Analogous to a physical leaf, a leaf bundle is at the end of a branch. It has no descendants.
```text
content/
├── about
-│ ├── index.md
+│ └── index.md
├── posts
│ ├── my-post
-│ │ ├── content1.md
-│ │ ├── content2.md
-│ │ ├── image1.jpg
-│ │ ├── image2.png
+│ │ ├── content-1.md
+│ │ ├── content-2.md
+│ │ ├── image-1.jpg
+│ │ ├── image-2.png
│ │ └── index.md
│ └── my-other-post
│ └── index.md
-│
└── another-section
- ├── ..
+ ├── foo.md
└── not-a-leaf-bundle
- ├── ..
+ ├── bar.md
└── another-leaf-bundle
└── index.md
```
-In the above example `content/` directory, there are four leaf
-bundles:
+There are four leaf bundles in the example above:
about
-: This leaf bundle is at the root level (directly under
- `content` directory) and has only the `index.md`.
+: This leaf bundle does not contain any page resources.
my-post
-: This leaf bundle has the `index.md`, two other content
- Markdown files and two image files.
+: This leaf bundle contains an index file, two resources of [resource type](g) `page`, and two resources of resource type `image`.
-- image1, image2:
-These images are page resources of `my-post`
- and only available in `my-post/index.md` resources.
+ - content-1, content-2
-- content1, content2:
-These content files are page resources of `my-post`
- and only available in `my-post/index.md` resources.
- They will **not** be rendered as individual pages.
+ These are resources of resource type `page`, accessible via the [`Resources`] method on the `Page` object. Hugo will not render these as individual pages.
+
+ - image-1, image-2
+
+ These are resources of resource type `image`, accessible via the `Resources` method on the `Page` object
my-other-post
-: This leaf bundle has only the `index.md`.
+: This leaf bundle does not contain any page resources.
another-leaf-bundle
-: This leaf bundle is nested under couple of
- directories. This bundle also has only the `index.md`.
+: This leaf bundle does not contain any page resources.
-{{% note %}}
-The hierarchy depth at which a leaf bundle is created does not matter,
-as long as it is not inside another **leaf** bundle.
-{{% /note %}}
-
-### Headless bundle
-
-A headless bundle is a bundle that is configured to not get published
-anywhere:
-
-- It will have no `Permalink` and no rendered HTML in `public/`.
-- It will not be part of `.Site.RegularPages`, etc.
-
-But you can get it by `.Site.GetPage`. Here is an example:
-
-```go-html-template
-{{ $headless := .Site.GetPage "/some-headless-bundle" }}
-{{ $reusablePages := $headless.Resources.Match "author*" }}
-Authors
-{{ range $reusablePages }}
- {{ .Title }}
- {{ .Content }}
-{{ end }}
-```
-
-_In this example, we are assuming the `some-headless-bundle` to be a headless
- bundle containing one or more **page** resources whose `.Name` matches
- `"author*"`._
-
-Explanation of the above example:
-
-1. Get the `some-headless-bundle` Page "object".
-2. Collect a _slice_ of resources in this _Page Bundle_ that matches
- `"author*"` using `.Resources.Match`.
-3. Loop through that _slice_ of nested pages, and output their `.Title` and
- `.Content`.
-
----
-
-A leaf bundle can be made headless by adding below in the front matter
-(in the `index.md`):
-
-{{< code-toggle file=content/headless/index.md fm=true >}}
-headless = true
-{{< /code-toggle >}}
-
-There are many use cases of such headless page bundles:
-
-- Shared media galleries
-- Reusable page content "snippets"
+> [!note]
+> Create leaf bundles at any depth within the `content` directory, but a leaf bundle may not contain another bundle. Leaf bundles do not have descendants.
## Branch bundles
-A _Branch Bundle_ is any directory at any hierarchy within the
-`content/` directory, that contains at least an **`_index.md`** file.
-
-This `_index.md` can also be directly under the `content/` directory.
-
-{{% note %}}
-Here `md` (markdown) is used just as an example. You can use any file
-type as a content resource as long as it is a content type recognized by Hugo.
-{{% /note %}}
-
-### Examples of branch bundle organization
+A _branch bundle_ is a directory that contains an `_index.md` file and zero or more resources. Analogous to a physical branch, a branch bundle may have descendants including leaf bundles and other branch bundles. Top-level directories with or without `_index.md` files are also branch bundles. This includes the home page.
```text
content/
-├── branch-bundle-1
-│ ├── branch-content1.md
-│ ├── branch-content2.md
-│ ├── image1.jpg
-│ ├── image2.png
+├── branch-bundle-1/
+│ ├── _index.md
+│ ├── content-1.md
+│ ├── content-2.md
+│ ├── image-1.jpg
+│ └── image-2.png
+├── branch-bundle-2/
+│ ├── a-leaf-bundle/
+│ │ └── index.md
│ └── _index.md
-└── branch-bundle-2
- ├── _index.md
- └── a-leaf-bundle
- └── index.md
+└── _index.md
```
-In the above example `content/` directory, there are two branch
-bundles (and a leaf bundle):
+There are three branch bundles in the example above:
+
+home page
+: This branch bundle contains an index file, two descendant branch bundles, and no resources.
branch-bundle-1
-: This branch bundle has the `_index.md`, two
- other content Markdown files and two image files.
+: This branch bundle contains an index file, two resources of [resource type](g) `page`, and two resources of resource type `image`.
branch-bundle-2
-: This branch bundle has the `_index.md` and a
- nested leaf bundle.
+: This branch bundle contains an index file and a leaf bundle.
-{{% note %}}
-The hierarchy depth at which a branch bundle is created does not
-matter.
-{{% /note %}}
+> [!note]
+> Create branch bundles at any depth within the `content` directory. Branch bundles may have descendants.
-[^fn:1]: The `.md` extension is just an example. The extension can be `.html`, `.json` or any valid MIME type.
+## Headless bundles
+
+Use [build options] in front matter to create an unpublished leaf or branch bundle whose content and resources you can include in other pages.
+
+[`Resources`]: /methods/page/resources/
+[build options]: /content-management/build-options/
+[home]: /templates/types/#home
+[page resources]: /content-management/page-resources/
+[section]: /templates/types/#section
+[single]: /templates/types/#single
+[taxonomy]: /templates/types/#taxonomy
+[term]: /templates/types/#term
diff --git a/docs/content/en/content-management/page-resources.md b/docs/content/en/content-management/page-resources.md
index f141510bb..204ca5301 100644
--- a/docs/content/en/content-management/page-resources.md
+++ b/docs/content/en/content-management/page-resources.md
@@ -1,17 +1,12 @@
---
title: Page resources
-description: Page resources -- images, other pages, documents, etc. -- have page-relative URLs and their own metadata.
-categories: [content management]
-keywords: [bundle,content,resources]
-menu:
- docs:
- parent: content-management
- weight: 80
-weight: 80
-toc: true
+description: Use page resources to logically associate assets with a page.
+categories: []
+keywords: []
---
+
Page resources are only accessible from [page bundles](/content-management/page-bundles), those directories with `index.md` or
-`_index.md` files at their root. Page resources are only available to the
+`_index.md` files at their root. Page resources are only available to the
page with which they are bundled.
In this example, `first-post` is a page bundle with access to 10 page resources including audio, data, documents, images, and video. Although `second-post` is also a page bundle, it has no page resources and is unable to directly access the page resources associated with `first-post`.
@@ -36,108 +31,101 @@ content
└── index.md (root of page bundle)
```
-## Properties
+## Examples
-ResourceType
-: The main type of the resource's [Media Type](/templates/output-formats/#media-types). For example, a file of MIME type `image/jpeg` has the ResourceType `image`. A `Page` will have `ResourceType` with value `page`.
+Use any of these methods on a `Page` object to capture page resources:
-Name
-: Default value is the file name (relative to the owning page). Can be set in front matter.
+ - [`Resources.ByType`]
+ - [`Resources.Get`]
+ - [`Resources.GetMatch`]
+ - [`Resources.Match`]
-Title
-: Default value is the same as `.Name`. Can be set in front matter.
+ Once you have captured a resource, use any of the applicable [`Resource`] methods to return a value or perform an action.
-Permalink
-: The absolute URL to the resource. Resources of type `page` will have no value.
+The following examples assume this content structure:
-RelPermalink
-: The relative URL to the resource. Resources of type `page` will have no value.
+```text
+content/
+└── example/
+ ├── data/
+ │ └── books.json <-- page resource
+ ├── images/
+ │ ├── a.jpg <-- page resource
+ │ └── b.jpg <-- page resource
+ ├── snippets/
+ │ └── text.md <-- page resource
+ └── index.md
+```
-Content
-: The content of the resource itself. For most resources, this returns a string
-with the contents of the file. Use this to create inline resources.
+Render a single image, and throw an error if the file does not exist:
```go-html-template
-{{ with .Resources.GetMatch "script.js" }}
-
-{{ end }}
-
-{{ with .Resources.GetMatch "style.css" }}
-
-{{ end }}
-
-{{ with .Resources.GetMatch "img.png" }}
-
+{{ $path := "images/a.jpg" }}
+{{ with .Resources.Get $path }}
+
+{{ else }}
+ {{ errorf "Unable to get page resource %q" $path }}
{{ end }}
```
-MediaType.Type
-: The media type (formerly known as a MIME type) of the resource (e.g., `image/jpeg`).
-
-MediaType.MainType
-: The main type of the resource's media type (e.g., `image`).
-
-MediaType.SubType
-: The subtype of the resource's type (e.g., `jpeg`). This may or may not correspond to the file suffix.
-
-MediaType.Suffixes
-: A slice of possible file suffixes for the resource's media type (e.g., `[jpg jpeg jpe jif jfif]`).
-
-## Methods
-
-ByType
-: Returns the page resources of the given type.
+Render all images, resized to 300 px wide:
```go-html-template
-{{ .Resources.ByType "image" }}
+{{ range .Resources.ByType "image" }}
+ {{ with .Resize "300x" }}
+
+ {{ end }}
+{{ end }}
```
-Match
-: Returns all the page resources (as a slice) whose `Name` matches the given Glob pattern ([examples](https://github.com/gobwas/glob/blob/master/readme.md)). The matching is case-insensitive.
+
+Render the markdown snippet:
```go-html-template
-{{ .Resources.Match "images/*" }}
+{{ with .Resources.Get "snippets/text.md" }}
+ {{ .Content }}
+{{ end }}
```
-GetMatch
-: Same as `Match` but will return the first match.
+List the titles in the data file, and throw an error if the file does not exist.
-### Pattern matching
-
-```go
-// Using Match/GetMatch to find this images/sunset.jpg ?
-.Resources.Match "images/sun*" ✅
-.Resources.Match "**/sunset.jpg" ✅
-.Resources.Match "images/*.jpg" ✅
-.Resources.Match "**.jpg" ✅
-.Resources.Match "*" 🚫
-.Resources.Match "sunset.jpg" 🚫
-.Resources.Match "*sunset.jpg" 🚫
+```go-html-template
+{{ $path := "data/books.json" }}
+{{ with .Resources.Get $path }}
+ {{ with . | transform.Unmarshal }}
+ Books:
+
+ {{ range . }}
+ {{ .title }}
+ {{ end }}
+
+ {{ end }}
+{{ else }}
+ {{ errorf "Unable to get page resource %q" $path }}
+{{ end }}
```
-## Page resources metadata
+## Metadata
The page resources' metadata is managed from the corresponding page's front matter with an array/table parameter named `resources`. You can batch assign values using [wildcards](https://tldp.org/LDP/GNU-Linux-Tools-Summary/html/x11655.htm).
-{{% note %}}
-Resources of type `page` get `Title` etc. from their own front matter.
-{{% /note %}}
+> [!note]
+> Resources of type `page` get `Title` etc. from their own front matter.
name
-: Sets the value returned in `Name`.
+: (`string`) Sets the value returned in `Name`.
-{{% note %}}
-The methods `Match`, `Get` and `GetMatch` use `Name` to match the resources.
-{{% /note %}}
+> [!note]
+> The methods `Match`, `Get` and `GetMatch` use `Name` to match the resources.
title
-: Sets the value returned in `Title`
+: (`string`) Sets the value returned in `Title`
params
-: A map of custom key/values.
+: (`map`) A map of custom key-value pairs.
### Resources metadata example
-{{< code-toggle >}}
+{{< code-toggle file=content/example.md fm=true >}}
title: Application
date : 2018-01-25
resources :
@@ -171,9 +159,8 @@ From the example above:
- All `PDF` files will get a new `Name`. The `name` parameter contains a special placeholder [`:counter`](#the-counter-placeholder-in-name-and-title), so the `Name` will be `pdf-file-1`, `pdf-file-2`, `pdf-file-3`.
- Every docx in the bundle will receive the `word` icon.
-{{% note %}}
-The __order matters__ --- Only the **first set** values of the `title`, `name` and `params`-**keys** will be used. Consecutive parameters will be set only for the ones not already set. In the above example, `.Params.icon` is first set to `"photo"` in `src = "documents/photo_specs.pdf"`. So that would not get overridden to `"pdf"` by the later set `src = "**.pdf"` rule.
-{{% /note %}}
+> [!note]
+> The order matters; only the first set values of the `title`, `name` and `params` keys will be used. Consecutive parameters will be set only for the ones not already set. In the above example, `.Params.icon` is first set to `"photo"` in `src = "documents/photo_specs.pdf"`. So that would not get overridden to `"pdf"` by the later set `src = "**.pdf"` rule.
### The `:counter` placeholder in `name` and `title`
@@ -201,3 +188,110 @@ the `Name` and `Title` will be assigned to the resource files as follows:
| guide.pdf | `"pdf-file-2.pdf` | `"guide.pdf"` |
| other\_specs.pdf | `"pdf-file-3.pdf` | `"Specification #1"` |
| photo\_specs.pdf | `"pdf-file-4.pdf` | `"Specification #2"` |
+
+## Multilingual
+
+{{< new-in 0.123.0 />}}
+
+By default, with a multilingual single-host site, Hugo does not duplicate shared page resources when building the site.
+
+> [!note]
+> This behavior is limited to Markdown content. Shared page resources for other [content formats] are copied into each language bundle.
+
+Consider this site configuration:
+
+{{< code-toggle file=hugo >}}
+defaultContentLanguage = 'de'
+defaultContentLanguageInSubdir = true
+
+[languages.de]
+languageCode = 'de-DE'
+languageName = 'Deutsch'
+weight = 1
+
+[languages.en]
+languageCode = 'en-US'
+languageName = 'English'
+weight = 2
+{{< /code-toggle >}}
+
+And this content:
+
+```text
+content/
+└── my-bundle/
+ ├── a.jpg <-- shared page resource
+ ├── b.jpg <-- shared page resource
+ ├── c.de.jpg
+ ├── c.en.jpg
+ ├── index.de.md
+ └── index.en.md
+```
+
+With v0.122.0 and earlier, Hugo duplicated the shared page resources, creating copies for each language:
+
+```text
+public/
+├── de/
+│ ├── my-bundle/
+│ │ ├── a.jpg <-- shared page resource
+│ │ ├── b.jpg <-- shared page resource
+│ │ ├── c.de.jpg
+│ │ └── index.html
+│ └── index.html
+├── en/
+│ ├── my-bundle/
+│ │ ├── a.jpg <-- shared page resource (duplicate)
+│ │ ├── b.jpg <-- shared page resource (duplicate)
+│ │ ├── c.en.jpg
+│ │ └── index.html
+│ └── index.html
+└── index.html
+
+```
+
+With v0.123.0 and later, Hugo places the shared resources in the page bundle for the default content language:
+
+```text
+public/
+├── de/
+│ ├── my-bundle/
+│ │ ├── a.jpg <-- shared page resource
+│ │ ├── b.jpg <-- shared page resource
+│ │ ├── c.de.jpg
+│ │ └── index.html
+│ └── index.html
+├── en/
+│ ├── my-bundle/
+│ │ ├── c.en.jpg
+│ │ └── index.html
+│ └── index.html
+└── index.html
+```
+
+This approach reduces build times, storage requirements, bandwidth consumption, and deployment times, ultimately reducing cost.
+
+> [!note]
+> To resolve Markdown link and image destinations to the correct location, you must use link and image render hooks that capture the page resource with the [`Resources.Get`] method, and then invoke its [`RelPermalink`] method.
+>
+> By default, with multilingual single-host sites, Hugo enables its [embedded link render hook] and [embedded image render hook] to resolve Markdown link and image destinations.
+>
+> You may override the embedded render hooks as needed, provided they capture the resource as described above.
+
+Although duplicating shared page resources is inefficient, you can enable this feature in your site configuration if desired:
+
+{{< code-toggle file=hugo >}}
+[markup.goldmark]
+duplicateResourceFiles = true
+{{< /code-toggle >}}
+
+[`RelPermalink`]: /methods/resource/relpermalink/
+[`Resource`]: /methods/resource
+[`Resources.ByType`]: /methods/page/resources#bytype
+[`Resources.Get`]: /methods/page/resources#get
+[`Resources.Get`]: /methods/page/resources/#get
+[`Resources.GetMatch`]: /methods/page/resources#getmatch
+[`Resources.Match`]: /methods/page/resources#match
+[content formats]: /content-management/formats/
+[embedded image render hook]: /render-hooks/images/#default
+[embedded link render hook]: /render-hooks/links/#default
diff --git a/docs/content/en/content-management/related-content.md b/docs/content/en/content-management/related-content.md
new file mode 100644
index 000000000..d7b18dab0
--- /dev/null
+++ b/docs/content/en/content-management/related-content.md
@@ -0,0 +1,102 @@
+---
+title: Related content
+description: List related content in "See Also" sections.
+categories: []
+keywords: []
+aliases: [/content/related/,/related/,/content-management/related/]
+---
+
+Hugo uses a set of factors to identify a page's related content based on front matter parameters. This can be tuned to the desired set of indices and parameters or left to Hugo's default [related content configuration](/configuration/related-content/).
+
+## List related content
+
+To list up to 5 related pages (which share the same _date_ or _keyword_ parameters) is as simple as including something similar to this partial in your template:
+
+```go-html-template {file="layouts/partials/related.html" copy=true}
+{{ with site.RegularPages.Related . | first 5 }}
+ Related content:
+
+{{ end }}
+```
+
+The `Related` method takes one argument which may be a `Page` or an options map. The options map has these options:
+
+indices
+: (`slice`) The indices to search within.
+
+document
+: (`page`) The page for which to find related content. Required when specifying an options map.
+
+namedSlices
+: (`slice`) The keywords to search for, expressed as a slice of `KeyValues` using the [`keyVals`] function.
+
+fragments
+: (`slice`) A list of special keywords that is used for indices configured as type "fragments". This will match the [fragment](g) identifiers of the documents.
+
+A fictional example using all of the above options:
+
+```go-html-template
+{{ $page := . }}
+{{ $opts := dict
+ "indices" (slice "tags" "keywords")
+ "document" $page
+ "namedSlices" (slice (keyVals "tags" "hugo" "rocks") (keyVals "date" $page.Date))
+ "fragments" (slice "heading-1" "heading-2")
+}}
+```
+
+> [!note]
+> We improved and simplified this feature in Hugo 0.111.0. Before this we had 3 different methods: `Related`, `RelatedTo` and `RelatedIndices`. Now we have only one method: `Related`. The old methods are still available but deprecated. Also see [this blog article](https://regisphilibert.com/blog/2018/04/hugo-optmized-relashionships-with-related-content/) for a great explanation of more advanced usage of this feature.
+
+## Index content headings
+
+Hugo can index the headings in your content and use this to find related content. You can enable this by adding a index of type `fragments` to your `related` configuration:
+
+{{< code-toggle file=hugo >}}
+[related]
+threshold = 20
+includeNewer = true
+toLower = false
+[[related.indices]]
+name = "fragmentrefs"
+type = "fragments"
+applyFilter = true
+weight = 80
+{{< /code-toggle >}}
+
+- The `name` maps to a optional front matter slice attribute that can be used to link from the page level down to the fragment/heading level.
+- If `applyFilter` is enabled, the `.HeadingsFiltered` on each page in the result will reflect the filtered headings. This is useful if you want to show the headings in the related content listing:
+
+```go-html-template
+{{ $related := .Site.RegularPages.Related . | first 5 }}
+{{ with $related }}
+ See Also
+
+ {{ range $i, $p := . }}
+
+ {{ .LinkTitle }}
+ {{ with .HeadingsFiltered }}
+
+ {{ range . }}
+ {{ $link := printf "%s#%s" $p.RelPermalink .ID | safeURL }}
+
+ {{ .Title }}
+
+ {{ end }}
+
+ {{ end }}
+
+ {{ end }}
+
+{{ end }}
+```
+
+## Configuration
+
+See [configure related content](/configuration/related-content/).
+
+[`keyVals`]: /functions/collections/keyvals/
diff --git a/docs/content/en/content-management/related.md b/docs/content/en/content-management/related.md
deleted file mode 100644
index e73dfc32a..000000000
--- a/docs/content/en/content-management/related.md
+++ /dev/null
@@ -1,178 +0,0 @@
----
-title: Related content
-description: List related content in "See Also" sections.
-categories: [content management]
-keywords: [content]
-menu:
- docs:
- parent: content-management
- weight: 110
-weight: 110
-toc: true
-aliases: [/content/related/,/related/]
----
-
-Hugo uses a set of factors to identify a page's related content based on front matter parameters. This can be tuned to the desired set of indices and parameters or left to Hugo's default [Related Content configuration](#configure-related-content).
-
-## List related content
-
-To list up to 5 related pages (which share the same _date_ or _keyword_ parameters) is as simple as including something similar to this partial in your single page template:
-
-{{< code file=layouts/partials/related.html >}}
-{{ $related := .Site.RegularPages.Related . | first 5 }}
-{{ with $related }}
-See Also
-
-{{ end }}
-{{< /code >}}
-
-The `Related` method takes one argument which may be a `Page` or a options map. The options map have these options:
-
-indices
-: (`slice`) The indices to search within.
-
-document
-: (`page`) The page for which to find related content. Required when specifying an options map.
-
-namedSlices
-: (`slice`) The keywords to search for, expressed as a slice of `KeyValues` using the [`keyVals`] function.
-
-fragments
-: (`slice`) A list of special keywords that is used for indices configured as type "fragments". This will match the [fragment] identifiers of the documents.
-
-[fragment]: /getting-started/glossary/#fragment
-[`keyVals`]: /functions/collections/keyvals/
-
-A fictional example using all of the above options:
-
-```go-html-template
-{{ $page := . }}
-{{ $opts := dict
- "indices" (slice "tags" "keywords")
- "document" $page
- "namedSlices" (slice (keyVals "tags" "hugo" "rocks") (keyVals "date" $page.Date))
- "fragments" (slice "heading-1" "heading-2")
-}}
-```
-
-{{% note %}}
-We improved and simplified this feature in Hugo 0.111.0. Before this we had 3 different methods: `Related`, `RelatedTo` and `RelatedIndices`. Now we have only one method: `Related`. The old methods are still available but deprecated. Also see [this blog article](https://regisphilibert.com/blog/2018/04/hugo-optmized-relashionships-with-related-content/) for a great explanation of more advanced usage of this feature.
-{{% /note %}}
-
-## Index content headings in related content
-
-{{< new-in 0.111.0 >}}
-
-Hugo can index the headings in your content and use this to find related content. You can enable this by adding a index of type `fragments` to your `related` configuration:
-
-{{< code-toggle file=hugo >}}
-[related]
-threshold = 20
-includeNewer = true
-toLower = false
-[[related.indices]]
-name = "fragmentrefs"
-type = "fragments"
-applyFilter = true
-weight = 80
-{{< /code-toggle >}}
-
-* The `name` maps to a optional front matter slice attribute that can be used to link from the page level down to the fragment/heading level.
-* If `applyFilter`is enabled, the `.HeadingsFiltered` on each page in the result will reflect the filtered headings. This is useful if you want to show the headings in the related content listing:
-
-```go-html-template
-{{ $related := .Site.RegularPages.Related . | first 5 }}
-{{ with $related }}
- See Also
-
- {{ range $i, $p := . }}
-
- {{ .LinkTitle }}
- {{ with .HeadingsFiltered }}
-
- {{ range . }}
- {{ $link := printf "%s#%s" $p.RelPermalink .ID | safeURL }}
-
- {{ .Title }}
-
- {{ end }}
-
- {{ end }}
-
- {{ end }}
-
-{{ end }}
-```
-
-## Configure related content
-
-Hugo provides a sensible default configuration of Related Content, but you can fine-tune this in your configuration, on the global or language level if needed.
-
-### Default configuration
-
-Without any `related` configuration set on the project, Hugo's Related Content methods will use the following.
-
-{{< code-toggle config=related />}}
-
-Custom configuration should be set using the same syntax.
-
-{{% note %}}
-If you add a `related` configuration section, you need to add a complete configuration. It is not possible to just set, say, `includeNewer` and use the rest from the Hugo defaults.
-{{% /note %}}
-
-### Top level configuration options
-
-threshold
-: (`int`) A value between 0-100. Lower value will give more, but maybe not so relevant, matches.
-
-includeNewer
-: (`bool`) Set to `true` to include **pages newer than the current page** in the related content listing. This will mean that the output for older posts may change as new related content gets added.
-
-toLower
-: (`bool`) Set to `true` to lower case keywords in both the indexes and the queries. This may give more accurate results at a slight performance penalty. Note that this can also be set per index.
-
-### Configuration options per index
-
-name
-: (`string`) The index name. This value maps directly to a page parameter. Hugo supports string values (`author` in the example) and lists (`tags`, `keywords` etc.) and time and date objects.
-
-type {{< new-in 0.111.0 >}}
-: (`string`) One of `basic`(default) or `fragments`.
-
-applyFilter {{< new-in 0.111.0 >}}
-: (`string`) Apply a `type` specific filter to the result of a search. This is currently only used for the `fragments` type.
-
-weight
-: (`int`) An integer weight that indicates _how important_ this parameter is relative to the other parameters. It can be `0`, which has the effect of turning this index off, or even negative. Test with different values to see what fits your content best.
-
-cardinalityThreshold {{< new-in 0.111.0 >}}
-: (`int`) A percentage (0-100) used to remove common keywords from the index. As an example, setting this to `50` will remove all keywords that are used in more than 50% of the documents in the index. Default is `0`.
-
-pattern
-: (`string`) This is currently only relevant for dates. When listing related content, we may want to list content that is also close in time. Setting "2006" (default value for date indexes) as the pattern for a date index will add weight to pages published in the same year. For busier blogs, "200601" (year and month) may be a better default.
-
-toLower
-: (`bool`) See above.
-
-## Performance considerations
-
-**Fast is Hugo's middle name** and we would not have released this feature had it not been blistering fast.
-
-This feature has been in the back log and requested by many for a long time. The development got this recent kick start from this Twitter thread:
-
-{{< tweet user="scott_lowe" id="898398437527363585" >}}
-
-Scott S. Lowe removed the "Related Content" section built using the `intersect` template function on tags, and the build time dropped from 30 seconds to less than 2 seconds on his 1700 content page sized blog.
-
-He should now be able to add an improved version of that "Related Content" section without giving up the fast live-reloads. But it's worth noting that:
-
-* If you don't use any of the `Related` methods, you will not use the Relate Content feature, and performance will be the same as before.
-* Calling `.RegularPages.Related` etc. will create one inverted index, also sometimes named posting list, that will be reused for any lookups in that same page collection. Doing that in addition to, as an example, calling `.Pages.Related` will work as expected, but will create one additional inverted index. This should still be very fast, but worth having in mind, especially for bigger sites.
-
-{{% note %}}
-We currently do not index **Page content**. We thought we would release something that will make most people happy before we start solving [Sherlock's last case](https://github.com/joearms/sherlock).
-{{% /note %}}
diff --git a/docs/content/en/content-management/sections.md b/docs/content/en/content-management/sections.md
index 1b694ce44..f7a2296f5 100644
--- a/docs/content/en/content-management/sections.md
+++ b/docs/content/en/content-management/sections.md
@@ -2,26 +2,14 @@
title: Sections
description: Organize content into sections.
-categories: [content management]
-keywords: [lists,sections,content types,organization]
-menu:
- docs:
- parent: content-management
- weight: 120
-weight: 120
-toc: true
+categories: []
+keywords: []
aliases: [/content/sections/]
---
## Overview
-A section is a top-level content directory, or any content directory with an _index.md file. A content directory with an _index.md file is also known as a [branch bundle](/getting-started/glossary/#branch-bundle). Section templates receive one or more page [collections](/getting-started/glossary/#collection) in [context](/getting-started/glossary/#context).
-
-{{% note %}}
-Although top-level directories without _index.md files are sections, we recommend creating _index.md files in _all_ sections.
-{{% /note %}}
-
-A typical site consists of one or more sections. For example:
+{{% glossary-term "section" %}}
```text
content/
@@ -74,11 +62,8 @@ Have list pages|:heavy_check_mark:|:x:
With the file structure from the [example above](#overview):
1. The list page for the articles section includes all articles, regardless of directory structure; none of the subdirectories are sections.
-
1. The articles/2022 and articles/2023 directories do not have list pages; they are not sections.
-
-1. The list page for the products section, by default, includes product-1 and product-2, but not their descendant pages. To include descendant pages, use the `.RegularPagesRecursive` collection instead of the `.Pages` collection in the list template. See [details](/variables/page/#page-collections).
-
+1. The list page for the products section, by default, includes product-1 and product-2, but not their descendant pages. To include descendant pages, use the `RegularPagesRecursive` method instead of the `Pages` method in the list template.
1. All directories in the products section have list pages; each directory is a section.
## Template selection
@@ -87,23 +72,20 @@ Hugo has a defined [lookup order] to determine which template to use when render
With the file structure from the [example above](#overview):
-Content directory|List page template
+Content directory|Section template
:--|:--
-content/products|layouts/products/list.html
-content/products/product-1|layouts/products/list.html
-content/products/product-1/benefits|layouts/products/list.html
+`content/products`|`layouts/products/list.html`
+`content/products/product-1`|`layouts/products/list.html`
+`content/products/product-1/benefits`|`layouts/products/list.html`
-Content directory|Single page template
+Content directory|Single template
:--|:--
-content/products|layouts/products/single.html
-content/products/product-1|layouts/products/single.html
-content/products/product-1/benefits|layouts/products/single.html
+`content/products`|`layouts/products/single.html`
+`content/products/product-1`|`layouts/products/single.html`
+`content/products/product-1/benefits`|`layouts/products/single.html`
If you need to use a different template for a subsection, specify `type` and/or `layout` in front matter.
-[lookup rules]: /templates/lookup-order/#lookup-rules
-[lookup order]: /templates/lookup-order/
-
## Ancestors and descendants
A section has one or more ancestors (including the home page), and zero or more descendants. With the file structure from the [example above](#overview):
@@ -116,7 +98,7 @@ The content file (benefit-1.md) has four ancestors: benefits, product-1, product
For example, use the `.Ancestors` method to render breadcrumb navigation.
-{{< code file=layouts/partials/breadcrumb.html >}}
+```go-html-template {file="layouts/partials/breadcrumb.html"}
{{ range .Ancestors.Reverse }}
@@ -129,7 +111,7 @@ For example, use the `.Ancestors` method to render breadcrumb navigation.
-{{< /code >}}
+```
With this CSS:
@@ -153,9 +135,5 @@ Hugo renders this, where each breadcrumb is a link to the corresponding page:
Home » Products » Product 1 » Benefits » Benefit 1
```
-[archetype]: /content-management/archetypes/
-[content type]: /content-management/types/
-[directory structure]: /getting-started/directory-structure/
-[section templates]: /templates/section-templates/
-[leaf bundles]: /content-management/page-bundles/#leaf-bundles
-[branch bundles]: /content-management/page-bundles/#branch-bundles
+[lookup order]: /templates/lookup-order/
+[lookup rules]: /templates/lookup-order/#lookup-rules
diff --git a/docs/content/en/content-management/shortcodes.md b/docs/content/en/content-management/shortcodes.md
index bbc2b0cc8..2de387f39 100644
--- a/docs/content/en/content-management/shortcodes.md
+++ b/docs/content/en/content-management/shortcodes.md
@@ -1,404 +1,230 @@
---
title: Shortcodes
-description: Shortcodes are simple snippets inside your content files calling built-in or custom templates.
-categories: [content management]
-keywords: [markdown,content,shortcodes]
-menu:
- docs:
- parent: content-management
- weight: 100
-weight: 100
-toc: true
+description: Use embedded, custom, or inline shortcodes to insert elements such as videos, images, and social media embeds into your content.
+categories: []
+keywords: []
aliases: [/extras/shortcodes/]
-testparam: "Hugo Rocks!"
---
-## What a shortcode is
+## Introduction
-Hugo loves Markdown because of its simple content format, but there are times when Markdown falls short. Often, content authors are forced to add raw HTML (e.g., video `
\n|",
@@ -333,3 +344,186 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA
b.AssertFileContent("public/html/index.html", "! hugo_ctx")
}
+
+// Issue 12854.
+func TestRenderShortcodesWithHTML(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableLiveReload = true
+disableKinds = ["home", "taxonomy", "term"]
+markup.goldmark.renderer.unsafe = true
+-- content/p1.md --
+---
+title: "p1"
+---
+{{% include "p2" %}}
+-- content/p2.md --
+---
+title: "p2"
+---
+Hello Content p1 id-1000.
\nFoo.
\nContent p1 id-100.
\nFoo.
\nContent p1 id-100.
\nFoo.
\nContent p1 id-100.
\nFoo.
\n: error calling GetRemote: Get "gopher://example.org": unsupported protocol scheme "gopher"|
+FAILED REMOTE ERROR DETAILS CONTENT: failed to fetch remote resource from '%[2]s/fail.jpg': Not Implemented|Body: { msg: failed }
|StatusCode: 501|ContentLength: 16|ContentType: text/plain; charset=utf-8|
-`, identity.HashString(ts.URL+"/sunset.jpg", map[string]any{})))
+`, hashing.HashString(ts.URL+"/sunset.jpg", map[string]any{}), ts.URL))
b.AssertFileContent("public/styles.min.a1df58687c3c9cc38bf26532f7b4b2f2c2b0315dcde212376959995c04f11fef.css", "body{background-color:#add8e6}")
b.AssertFileContent("public//styles2.min.1cfc52986836405d37f9998a63fd6dd8608e8c410e5e3db1daaa30f78bc273ba.css", "body{background-color:orange}")
@@ -200,7 +200,7 @@ func BenchmarkResourceChainPostProcess(b *testing.B) {
for i := 0; i < b.N; i++ {
b.StopTimer()
s := newTestSitesBuilder(b)
- for i := 0; i < 300; i++ {
+ for i := range 300 {
s.WithContent(fmt.Sprintf("page%d.md", i+1), "---\ntitle: Page\n---")
}
s.WithTemplates("_default/single.html", `Start.
diff --git a/hugolib/rss_test.go b/hugolib/rss_test.go
index 0c3c21b90..34c2be393 100644
--- a/hugolib/rss_test.go
+++ b/hugolib/rss_test.go
@@ -96,3 +96,51 @@ Figure:
b.AssertFileContent("public/index.xml", "img src="http://example.com/images/sunset.jpg")
}
+
+// Issue 13332.
+func TestRSSCanonifyURLsSubDir(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+baseURL = 'https://example.org/subdir'
+disableKinds = ['section','sitemap','taxonomy','term']
+[markup.goldmark.renderHooks.image]
+enableDefault = true
+[markup.goldmark.renderHooks.link]
+enableDefault = true
+-- layouts/_default/_markup/render-image.html --
+{{- $u := urls.Parse .Destination -}}
+{{- $src := $u.String | relURL -}}
+
+
+{{- /**/ -}}
+-- layouts/_default/home.html --
+{{ .Content }}|
+-- layouts/_default/single.html --
+{{ .Content }}|
+-- layouts/_default/rss.xml --
+{{ with site.GetPage "/s1/p2" }}
+ {{ .Content | transform.XMLEscape | safeHTML }}
+{{ end }}
+-- content/s1/p1.md --
+---
+title: p1
+---
+-- content/s1/p2/index.md --
+---
+title: p2
+---
+
+
+[p1](/s1/p1)
+-- content/s1/p2/a.jpg --
+`
+
+ b := Test(t, files)
+
+ b.AssertFileContent("public/index.xml", "https://example.org/subdir/s1/p1/")
+ b.AssertFileContent("public/index.xml",
+ "img src="https://example.org/subdir/a.jpg",
+ "img srcset="https://example.org/subdir/a.jpg" src="https://example.org/subdir/a.jpg 2x")
+}
diff --git a/hugolib/securitypolicies_test.go b/hugolib/securitypolicies_test.go
index b0d39c697..facda80eb 100644
--- a/hugolib/securitypolicies_test.go
+++ b/hugolib/securitypolicies_test.go
@@ -123,7 +123,7 @@ func TestSecurityPolicies(t *testing.T) {
c.Skip()
}
cb := func(b *sitesBuilder) {
- b.WithTemplatesAdded("index.html", `{{ $scss := "body { color: #333; }" | resources.FromString "foo.scss" | resources.ToCSS (dict "transpiler" "dartsass") }}`)
+ b.WithTemplatesAdded("index.html", `{{ $scss := "body { color: #333; }" | resources.FromString "foo.scss" | css.Sass (dict "transpiler" "dartsass") }}`)
}
testVariant(c, cb, "")
})
@@ -137,10 +137,10 @@ func TestSecurityPolicies(t *testing.T) {
b.WithConfigFile("toml", `
[security]
[security.exec]
-allow="none"
-
+allow="none"
+
`)
- b.WithTemplatesAdded("index.html", `{{ $scss := "body { color: #333; }" | resources.FromString "foo.scss" | resources.ToCSS (dict "transpiler" "dartsass") }}`)
+ b.WithTemplatesAdded("index.html", `{{ $scss := "body { color: #333; }" | resources.FromString "foo.scss" | css.Sass (dict "transpiler" "dartsass") }}`)
}
testVariant(c, cb, `(?s).*sass(-embedded)?" is not whitelisted in policy "security\.exec\.allow".*`)
})
@@ -160,7 +160,7 @@ allow="none"
httpTestVariant(c, `{{ $json := resources.GetRemote "%[1]s/fruits.json" }}{{ $json.Content }}`, `(?s).*is not whitelisted in policy "security\.http\.urls".*`,
func(b *sitesBuilder) {
b.WithConfigFile("toml", `
-[security]
+[security]
[security.http]
urls="none"
`)
@@ -181,39 +181,10 @@ urls="none"
httpTestVariant(c, `{{ $json := resources.GetRemote "%[1]s/fakejson.json" }}{{ $json.Content }}`, ``,
func(b *sitesBuilder) {
b.WithConfigFile("toml", `
-[security]
+[security]
[security.http]
mediaTypes=["application/json"]
-`)
- })
- })
-
- c.Run("getJSON, OK", func(c *qt.C) {
- c.Parallel()
- httpTestVariant(c, `{{ $json := getJSON "%[1]s/fruits.json" }}{{ $json.Content }}`, "", nil)
- })
-
- c.Run("getJSON, denied URL", func(c *qt.C) {
- c.Parallel()
- httpTestVariant(c, `{{ $json := getJSON "%[1]s/fruits.json" }}{{ $json.Content }}`, `(?s).*is not whitelisted in policy "security\.http\.urls".*`,
- func(b *sitesBuilder) {
- b.WithConfigFile("toml", `
-[security]
-[security.http]
-urls="none"
-`)
- })
- })
-
- c.Run("getCSV, denied URL", func(c *qt.C) {
- c.Parallel()
- httpTestVariant(c, `{{ $d := getCSV ";" "%[1]s/cities.csv" }}{{ $d.Content }}`, `(?s).*is not whitelisted in policy "security\.http\.urls".*`,
- func(b *sitesBuilder) {
- b.WithConfigFile("toml", `
-[security]
-[security.http]
-urls="none"
`)
})
})
diff --git a/hugolib/segments/segments.go b/hugolib/segments/segments.go
index 8f7c18121..941c4ea5c 100644
--- a/hugolib/segments/segments.go
+++ b/hugolib/segments/segments.go
@@ -44,7 +44,7 @@ func (e excludeInclude) ShouldExcludeCoarse(fields SegmentMatcherFields) bool {
}
// ShouldExcludeFine returns whether the given fields should be excluded.
-// This is used for the finer grained checks, e.g. on invididual pages.
+// This is used for the finer grained checks, e.g. on individual pages.
func (e excludeInclude) ShouldExcludeFine(fields SegmentMatcherFields) bool {
if e.exclude != nil && e.exclude(fields) {
return true
diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go
index af4454a89..56bf1ff9e 100644
--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
+// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@ import (
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/types"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/gohugoio/hugo/resources/page"
@@ -36,16 +37,16 @@ import (
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/common/urls"
- "github.com/gohugoio/hugo/output"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/tpl"
)
var (
- _ urls.RefLinker = (*ShortcodeWithPage)(nil)
- _ types.Unwrapper = (*ShortcodeWithPage)(nil)
- _ text.Positioner = (*ShortcodeWithPage)(nil)
+ _ urls.RefLinker = (*ShortcodeWithPage)(nil)
+ _ types.Unwrapper = (*ShortcodeWithPage)(nil)
+ _ text.Positioner = (*ShortcodeWithPage)(nil)
+ _ maps.StoreProvider = (*ShortcodeWithPage)(nil)
)
// ShortcodeWithPage is the "." context in a shortcode template.
@@ -72,7 +73,7 @@ type ShortcodeWithPage struct {
posOffset int
pos text.Position
- scratch *maps.Scratch
+ store *maps.Scratch
}
// InnerDeindent returns the (potentially de-indented) inner content of the shortcode.
@@ -124,13 +125,19 @@ func (scp *ShortcodeWithPage) RelRef(args map[string]any) (string, error) {
return scp.Page.RelRefFrom(args, scp)
}
+// Store returns this shortcode's Store.
+func (scp *ShortcodeWithPage) Store() *maps.Scratch {
+ if scp.store == nil {
+ scp.store = maps.NewScratch()
+ }
+ return scp.store
+}
+
// Scratch returns a scratch-pad scoped for this shortcode. This can be used
// as a temporary storage for variables, counters etc.
+// Deprecated: Use Store instead. Note that from the templates this should be considered a "soft deprecation".
func (scp *ShortcodeWithPage) Scratch() *maps.Scratch {
- if scp.scratch == nil {
- scp.scratch = maps.NewScratch()
- }
- return scp.scratch
+ return scp.Store()
}
// Get is a convenience method to look up shortcode parameters by its key.
@@ -198,8 +205,7 @@ type shortcode struct {
indentation string // indentation from source.
- info tpl.Info // One of the output formats (arbitrary)
- templs []tpl.Template // All output formats
+ templ *tplimpl.TemplInfo
// If set, the rendered shortcode is sent as part of the surrounding content
// to Goldmark and similar.
@@ -223,16 +229,15 @@ func (s shortcode) insertPlaceholder() bool {
}
func (s shortcode) needsInner() bool {
- return s.info != nil && s.info.ParseInfo().IsInner
+ return s.templ != nil && s.templ.ParseInfo.IsInner
}
func (s shortcode) configVersion() int {
- if s.info == nil {
+ if s.templ == nil {
// Not set for inline shortcodes.
return 2
}
-
- return s.info.ParseInfo().Config.Version
+ return s.templ.ParseInfo.Config.Version
}
func (s shortcode) innerString() string {
@@ -308,12 +313,12 @@ func prepareShortcode(
ctx context.Context,
level int,
s *Site,
- tplVariants tpl.TemplateVariants,
sc *shortcode,
parent *ShortcodeWithPage,
- p *pageState,
+ po *pageOutput,
isRenderString bool,
) (shortcodeRenderer, error) {
+ p := po.p
toParseErr := func(err error) error {
source := p.m.content.mustSource()
return p.parseError(fmt.Errorf("failed to render shortcode %q: %w", sc.name, err), source, sc.pos)
@@ -321,12 +326,12 @@ func prepareShortcode(
// Allow the caller to delay the rendering of the shortcode if needed.
var fn shortcodeRenderFunc = func(ctx context.Context) ([]byte, bool, error) {
- if p.m.pageConfig.IsGoldmark && sc.doMarkup {
+ if p.m.pageConfig.ContentMediaType.IsMarkdown() && sc.doMarkup {
// Signal downwards that the content rendered will be
// parsed and rendered by Goldmark.
ctx = tpl.Context.IsInGoldmark.Set(ctx, true)
}
- r, err := doRenderShortcode(ctx, level, s, tplVariants, sc, parent, p, isRenderString)
+ r, err := doRenderShortcode(ctx, level, s, sc, parent, po, isRenderString)
if err != nil {
return nil, false, toParseErr(err)
}
@@ -345,30 +350,29 @@ func doRenderShortcode(
ctx context.Context,
level int,
s *Site,
- tplVariants tpl.TemplateVariants,
sc *shortcode,
parent *ShortcodeWithPage,
- p *pageState,
+ po *pageOutput,
isRenderString bool,
) (shortcodeRenderer, error) {
- var tmpl tpl.Template
+ var tmpl *tplimpl.TemplInfo
+ p := po.p
// Tracks whether this shortcode or any of its children has template variations
// in other languages or output formats. We are currently only interested in
- // the output formats, so we may get some false positives -- we
- // should improve on that.
+ // the output formats.
var hasVariants bool
if sc.isInline {
if !p.s.ExecHelper.Sec().EnableInlineShortcodes {
return zeroShortcode, nil
}
- templName := path.Join("_inline_shortcode", p.Path(), sc.name)
+ templatePath := path.Join("_inline_shortcode", p.Path(), sc.name)
if sc.isClosing {
templStr := sc.innerString()
var err error
- tmpl, err = s.TextTmpl().Parse(templName, templStr)
+ tmpl, err = s.TemplateStore.TextParse(templatePath, templStr)
if err != nil {
if isRenderString {
return zeroShortcode, p.wrapError(err)
@@ -382,24 +386,47 @@ func doRenderShortcode(
} else {
// Re-use of shortcode defined earlier in the same page.
- var found bool
- tmpl, found = s.TextTmpl().Lookup(templName)
- if !found {
+ tmpl = s.TemplateStore.TextLookup(templatePath)
+ if tmpl == nil {
return zeroShortcode, fmt.Errorf("no earlier definition of shortcode %q found", sc.name)
}
}
- tmpl = tpl.AddIdentity(tmpl)
} else {
- var found, more bool
- tmpl, found, more = s.Tmpl().LookupVariant(sc.name, tplVariants)
- if !found {
- s.Log.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path())
- return zeroShortcode, nil
+ ofCount := map[string]int{}
+ include := func(match *tplimpl.TemplInfo) bool {
+ ofCount[match.D.OutputFormat]++
+ return true
}
- hasVariants = hasVariants || more
+ base, layoutDescriptor := po.GetInternalTemplateBasePathAndDescriptor()
+
+ // With shortcodes/mymarkdown.md (only), this allows {{% mymarkdown %}} when rendering HTML,
+ // but will not resolve any template when doing {{< mymarkdown >}}.
+ layoutDescriptor.AlwaysAllowPlainText = sc.doMarkup
+ q := tplimpl.TemplateQuery{
+ Path: base,
+ Name: sc.name,
+ Category: tplimpl.CategoryShortcode,
+ Desc: layoutDescriptor,
+ Consider: include,
+ }
+ v, err := s.TemplateStore.LookupShortcode(q)
+ if v == nil {
+ return zeroShortcode, err
+ }
+ tmpl = v
+ hasVariants = hasVariants || len(ofCount) > 1
+ }
+
+ data := &ShortcodeWithPage{
+ Ordinal: sc.ordinal,
+ posOffset: sc.pos,
+ indentation: sc.indentation,
+ Params: sc.params,
+ Page: newPageForShortcode(p),
+ Parent: parent,
+ Name: sc.name,
}
- data := &ShortcodeWithPage{Ordinal: sc.ordinal, posOffset: sc.pos, indentation: sc.indentation, Params: sc.params, Page: newPageForShortcode(p), Parent: parent, Name: sc.name}
if sc.params != nil {
data.IsNamedParams = reflect.TypeOf(sc.params).Kind() == reflect.Map
}
@@ -411,7 +438,7 @@ func doRenderShortcode(
case string:
inner += innerData
case *shortcode:
- s, err := prepareShortcode(ctx, level+1, s, tplVariants, innerData, data, p, isRenderString)
+ s, err := prepareShortcode(ctx, level+1, s, innerData, data, po, isRenderString)
if err != nil {
return zeroShortcode, err
}
@@ -449,7 +476,7 @@ func doRenderShortcode(
// unchanged.
// 2 If inner does not have a newline, strip the wrapping block and
// the newline.
- switch p.m.pageConfig.Markup {
+ switch p.m.pageConfig.Content.Markup {
case "", "markdown":
if match, _ := regexp.MatchString(innerNewlineRegexp, inner); !match {
cleaner, err := regexp.Compile(innerCleanupRegexp)
@@ -468,7 +495,7 @@ func doRenderShortcode(
}
- result, err := renderShortcodeWithPage(ctx, s.Tmpl(), tmpl, data)
+ result, err := renderShortcodeWithPage(ctx, s.GetTemplateStore(), tmpl, data)
if err != nil && sc.isInline {
fe := herrors.NewFileErrorFromName(err, p.File().Filename())
@@ -518,16 +545,11 @@ func (s *shortcodeHandler) hasName(name string) bool {
return ok
}
-func (s *shortcodeHandler) prepareShortcodesForPage(ctx context.Context, p *pageState, f output.Format, isRenderString bool) (map[string]shortcodeRenderer, error) {
+func (s *shortcodeHandler) prepareShortcodesForPage(ctx context.Context, po *pageOutput, isRenderString bool) (map[string]shortcodeRenderer, error) {
rendered := make(map[string]shortcodeRenderer)
- tplVariants := tpl.TemplateVariants{
- Language: p.Language().Lang,
- OutputFormat: f,
- }
-
for _, v := range s.shortcodes {
- s, err := prepareShortcode(ctx, 0, s.s, tplVariants, v, nil, p, isRenderString)
+ s, err := prepareShortcode(ctx, 0, s.s, v, nil, po, isRenderString)
if err != nil {
return nil, err
}
@@ -620,7 +642,7 @@ Loop:
// we trust the template on this:
// if there's no inner, we're done
if !sc.isInline {
- if !sc.info.ParseInfo().IsInner {
+ if !sc.templ.ParseInfo.IsInner {
return sc, nil
}
}
@@ -634,7 +656,11 @@ Loop:
// return that error, more specific
continue
}
- return nil, fmt.Errorf("%s: shortcode %q does not evaluate .Inner or .InnerDeindent, yet a closing tag was provided", errorPrefix, next.ValStr(source))
+ name := sc.name
+ if name == "" {
+ name = next.ValStr(source)
+ }
+ return nil, fmt.Errorf("%s: shortcode %q does not evaluate .Inner or .InnerDeindent, yet a closing tag was provided", errorPrefix, name)
}
}
if next.IsRightShortcodeDelim() {
@@ -652,14 +678,13 @@ Loop:
sc.name = currItem.ValStr(source)
- // Used to check if the template expects inner content.
- templs := s.s.Tmpl().LookupVariants(sc.name)
- if templs == nil {
+ // Used to check if the template expects inner content,
+ // so just pick one arbitrarily with the same name.
+ templ := s.s.TemplateStore.LookupShortcodeByName(sc.name)
+ if templ == nil {
return nil, fmt.Errorf("%s: template for shortcode %q not found", errorPrefix, sc.name)
}
-
- sc.info = templs[0].(tpl.Info)
- sc.templs = templs
+ sc.templ = templ
case currItem.IsInlineShortcodeName():
sc.name = currItem.ValStr(source)
sc.isInline = true
@@ -758,7 +783,7 @@ func expandShortcodeTokens(
return source, nil
}
-func renderShortcodeWithPage(ctx context.Context, h tpl.TemplateHandler, tmpl tpl.Template, data *ShortcodeWithPage) (string, error) {
+func renderShortcodeWithPage(ctx context.Context, h *tplimpl.TemplateStore, tmpl *tplimpl.TemplInfo, data *ShortcodeWithPage) (string, error) {
buffer := bp.GetBuffer()
defer bp.PutBuffer(buffer)
diff --git a/hugolib/shortcode_page.go b/hugolib/shortcode_page.go
index 7c32f2ea1..3d27cc93c 100644
--- a/hugolib/shortcode_page.go
+++ b/hugolib/shortcode_page.go
@@ -65,6 +65,7 @@ var zeroShortcode = prerenderedShortcode{}
type pageForShortcode struct {
page.PageWithoutContent
page.TableOfContentsProvider
+ page.MarkupProvider
page.ContentProvider
// We need to replace it after we have rendered it, so provide a
@@ -80,6 +81,7 @@ func newPageForShortcode(p *pageState) page.Page {
return &pageForShortcode{
PageWithoutContent: p,
TableOfContentsProvider: p,
+ MarkupProvider: page.NopPage,
ContentProvider: page.NopPage,
toc: template.HTML(tocShortcodePlaceholder),
p: p,
@@ -105,6 +107,7 @@ var _ types.Unwrapper = (*pageForRenderHooks)(nil)
type pageForRenderHooks struct {
page.PageWithoutContent
page.TableOfContentsProvider
+ page.MarkupProvider
page.ContentProvider
p *pageState
}
@@ -112,6 +115,7 @@ type pageForRenderHooks struct {
func newPageForRenderHook(p *pageState) page.Page {
return &pageForRenderHooks{
PageWithoutContent: p,
+ MarkupProvider: page.NopPage,
ContentProvider: page.NopPage,
TableOfContentsProvider: p,
p: p,
@@ -121,3 +125,7 @@ func newPageForRenderHook(p *pageState) page.Page {
func (p *pageForRenderHooks) Unwrapv() any {
return p.p
}
+
+func (p *pageForRenderHooks) String() string {
+ return p.p.String()
+}
diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go
index a1c5c0aea..a1f12e77a 100644
--- a/hugolib/shortcode_test.go
+++ b/hugolib/shortcode_test.go
@@ -33,14 +33,14 @@ func TestExtractShortcodes(t *testing.T) {
b := newTestSitesBuilder(t).WithSimpleConfigFile()
b.WithTemplates(
- "default/single.html", `EMPTY`,
- "_internal/shortcodes/tag.html", `tag`,
- "_internal/shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`,
- "_internal/shortcodes/sc1.html", `sc1`,
- "_internal/shortcodes/sc2.html", `sc2`,
- "_internal/shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`,
- "_internal/shortcodes/inner2.html", `{{.Inner}}`,
- "_internal/shortcodes/inner3.html", `{{.Inner}}`,
+ "pages/single.html", `EMPTY`,
+ "shortcodes/tag.html", `tag`,
+ "shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`,
+ "shortcodes/sc1.html", `sc1`,
+ "shortcodes/sc2.html", `sc2`,
+ "shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`,
+ "shortcodes/inner2.html", `{{.Inner}}`,
+ "shortcodes/inner3.html", `{{.Inner}}`,
).WithContent("page.md", `---
title: "Shortcodes Galore!"
---
@@ -57,10 +57,9 @@ title: "Shortcodes Galore!"
if s == nil {
return ""
}
-
var version int
- if s.info != nil {
- version = s.info.ParseInfo().Config.Version
+ if s.templ != nil {
+ version = s.templ.ParseInfo.Config.Version
}
return strReplacer.Replace(fmt.Sprintf("%s;inline:%t;closing:%t;inner:%v;params:%v;ordinal:%d;markup:%t;version:%d;pos:%d",
s.name, s.isInline, s.isClosing, s.inner, s.params, s.ordinal, s.doMarkup, version, s.pos))
@@ -69,7 +68,7 @@ title: "Shortcodes Galore!"
regexpCheck := func(re string) func(c *qt.C, shortcode *shortcode, err error) {
return func(c *qt.C, shortcode *shortcode, err error) {
c.Assert(err, qt.IsNil)
- c.Assert(str(shortcode), qt.Matches, ".*"+re+".*")
+ c.Assert(str(shortcode), qt.Matches, ".*"+re+".*", qt.Commentf("%s", shortcode.name))
}
}
@@ -126,10 +125,11 @@ func TestShortcodeMultipleOutputFormats(t *testing.T) {
siteConfig := `
baseURL = "http://example.com/blog"
-paginate = 1
-
disableKinds = ["section", "term", "taxonomy", "RSS", "sitemap", "robotsTXT", "404"]
+[pagination]
+pagerSize = 1
+
[outputs]
home = [ "HTML", "AMP", "Calendar" ]
page = [ "HTML", "AMP", "JSON" ]
@@ -756,12 +756,15 @@ title: "Hugo Rocks!"
func TestShortcodeParams(t *testing.T) {
t.Parallel()
- c := qt.New(t)
- builder := newTestSitesBuilder(t).WithSimpleConfigFile()
-
- builder.WithContent("page.md", `---
+ files := `
+-- hugo.toml --
+baseURL = "https://example.org"
+-- layouts/shortcodes/hello.html --
+{{ range $i, $v := .Params }}{{ printf "- %v: %v (%T) " $i $v $v -}}{{ end }}
+-- content/page.md --
title: "Hugo Rocks!"
+summary: "Foo"
---
# doc
@@ -770,23 +773,15 @@ types positional: {{< hello true false 33 3.14 >}}
types named: {{< hello b1=true b2=false i1=33 f1=3.14 >}}
types string: {{< hello "true" trues "33" "3.14" >}}
escaped quoute: {{< hello "hello \"world\"." >}}
+-- layouts/_default/single.html --
+Content: {{ .Content }}|
+`
+ b := Test(t, files)
-`).WithTemplatesAdded(
- "layouts/shortcodes/hello.html",
- `{{ range $i, $v := .Params }}
-- {{ printf "%v: %v (%T)" $i $v $v }}
-{{ end }}
-{{ $b1 := .Get "b1" }}
-Get: {{ printf "%v (%T)" $b1 $b1 | safeHTML }}
-`).Build(BuildCfg{})
-
- s := builder.H.Sites[0]
- c.Assert(len(s.RegularPages()), qt.Equals, 1)
-
- builder.AssertFileContent("public/page/index.html",
+ b.AssertFileContent("public/page/index.html",
"types positional: - 0: true (bool) - 1: false (bool) - 2: 33 (int) - 3: 3.14 (float64)",
- "types named: - b1: true (bool) - b2: false (bool) - f1: 3.14 (float64) - i1: 33 (int) Get: true (bool) ",
+ "types named: - b1: true (bool) - b2: false (bool) - f1: 3.14 (float64) - i1: 33 (int)",
"types string: - 0: true (string) - 1: trues (string) - 2: 33 (string) - 3: 3.14 (string) ",
"hello "world". (string)",
)
@@ -835,31 +830,47 @@ title: "Hugo Rocks!"
func TestShortcodeNoInner(t *testing.T) {
t.Parallel()
- b := newTestSitesBuilder(t)
-
- b.WithContent("mypage.md", `---
+ files := `
+-- hugo.toml --
+baseURL = "https://example.org"
+disableKinds = ["term", "taxonomy", "home", "section"]
+-- content/mypage.md --
+---
title: "No Inner!"
---
+
{{< noinner >}}{{< /noinner >}}
+-- layouts/shortcodes/noinner.html --
+No inner here.
+-- layouts/_default/single.html --
+Content: {{ .Content }}|
-`).WithTemplatesAdded(
- "layouts/shortcodes/noinner.html", `No inner here.`)
+`
- err := b.BuildE(BuildCfg{})
- b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`"content/mypage.md:4:16": failed to extract shortcode: shortcode "noinner" does not evaluate .Inner or .InnerDeindent, yet a closing tag was provided`))
+ b, err := TestE(t, files)
+
+ assert := func() {
+ b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`failed to extract shortcode: shortcode "noinner" does not evaluate .Inner or .InnerDeindent, yet a closing tag was provided`))
+ }
+
+ assert()
+
+ b, err = TestE(t, strings.Replace(files, `{{< noinner >}}{{< /noinner >}}`, `{{< noinner />}}`, 1))
+
+ assert()
}
func TestShortcodeStableOutputFormatTemplates(t *testing.T) {
t.Parallel()
- for i := 0; i < 5; i++ {
+ for range 5 {
b := newTestSitesBuilder(t)
const numPages = 10
- for i := 0; i < numPages; i++ {
+ for i := range numPages {
b.WithContent(fmt.Sprintf("page%d.md", i), `---
title: "Page"
outputs: ["html", "css", "csv", "json"]
@@ -876,21 +887,22 @@ outputs: ["html", "css", "csv", "json"]
"_default/single.json", "{{ .Content }}",
"shortcodes/myshort.html", `Short-HTML`,
"shortcodes/myshort.csv", `Short-CSV`,
+ "shortcodes/myshort.txt", `Short-TXT`,
)
b.Build(BuildCfg{})
// helpers.PrintFs(b.Fs.Destination, "public", os.Stdout)
- for i := 0; i < numPages; i++ {
+ for i := range numPages {
b.AssertFileContent(fmt.Sprintf("public/page%d/index.html", i), "Short-HTML")
b.AssertFileContent(fmt.Sprintf("public/page%d/index.csv", i), "Short-CSV")
- b.AssertFileContent(fmt.Sprintf("public/page%d/index.json", i), "Short-HTML")
+ b.AssertFileContent(fmt.Sprintf("public/page%d/index.json", i), "Short-CSV")
}
- for i := 0; i < numPages; i++ {
- b.AssertFileContent(fmt.Sprintf("public/page%d/styles.css", i), "Short-HTML")
+ for i := range numPages {
+ b.AssertFileContent(fmt.Sprintf("public/page%d/styles.css", i), "Short-CSV")
}
}
@@ -906,7 +918,7 @@ func TestShortcodeMarkdownOutputFormat(t *testing.T) {
---
title: "p1"
---
-{{< foo >}}
+{{% foo %}}
# The below would have failed using the HTML template parser.
-- layouts/shortcodes/foo.md --
§§§
@@ -918,9 +930,7 @@ title: "p1"
b := Test(t, files)
- b.AssertFileContent("public/p1/index.html", `
-<x")
}
func TestShortcodePreserveIndentation(t *testing.T) {
diff --git a/hugolib/site.go b/hugolib/site.go
index 280387838..acd3b5410 100644
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -15,6 +15,7 @@ package hugolib
import (
"context"
+ "errors"
"fmt"
"io"
"mime"
@@ -28,10 +29,31 @@ import (
"time"
"github.com/bep/logg"
+ "github.com/gohugoio/hugo/cache/dynacache"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/hugio"
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/para"
"github.com/gohugoio/hugo/common/types"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/config/allconfig"
+ "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugolib/doctree"
+ "github.com/gohugoio/hugo/hugolib/pagesfromdata"
+ "github.com/gohugoio/hugo/internal/js/esbuild"
+ "github.com/gohugoio/hugo/internal/warpc"
+ "github.com/gohugoio/hugo/langs/i18n"
+ "github.com/gohugoio/hugo/modules"
+ "github.com/gohugoio/hugo/resources"
+
+ "github.com/gohugoio/hugo/tpl/tplimpl"
+ "github.com/gohugoio/hugo/tpl/tplimplinit"
+ xmaps "golang.org/x/exp/maps"
+
+ // Loads the template funcs namespaces.
+
"golang.org/x/text/unicode/norm"
"github.com/gohugoio/hugo/common/paths"
@@ -50,6 +72,9 @@ import (
"github.com/gohugoio/hugo/resources/kinds"
"github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/page/pagemeta"
+ "github.com/gohugoio/hugo/resources/page/siteidentities"
+ "github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/lazy"
@@ -61,7 +86,567 @@ import (
"github.com/gohugoio/hugo/tpl"
)
+var _ page.Site = (*Site)(nil)
+
+type siteState int
+
+const (
+ siteStateInit siteState = iota
+ siteStateReady
+)
+
+type Site struct {
+ state siteState
+ conf *allconfig.Config
+ language *langs.Language
+ languagei int
+ pageMap *pageMap
+ store *maps.Scratch
+
+ // The owning container.
+ h *HugoSites
+
+ *deps.Deps
+
+ // Page navigation.
+ *pageFinder
+ taxonomies page.TaxonomyList
+ menus navigation.Menus
+
+ // Shortcut to the home page. Note that this may be nil if
+ // home page, for some odd reason, is disabled.
+ home *pageState
+
+ // The last modification date of this site.
+ lastmod time.Time
+
+ relatedDocsHandler *page.RelatedDocsHandler
+ siteRefLinker
+ publisher publisher.Publisher
+ frontmatterHandler pagemeta.FrontMatterHandler
+
+ // The output formats that we need to render this site in. This slice
+ // will be fixed once set.
+ // This will be the union of Site.Pages' outputFormats.
+ // This slice will be sorted.
+ renderFormats output.Formats
+
+ // Lazily loaded site dependencies
+ init *siteInit
+}
+
+func (s *Site) Debug() {
+ fmt.Println("Debugging site", s.Lang(), "=>")
+ // fmt.Println(s.pageMap.testDump())
+}
+
+// NewHugoSites creates HugoSites from the given config.
+func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
+ conf := cfg.Configs.GetFirstLanguageConfig()
+
+ var logger loggers.Logger
+ if cfg.TestLogger != nil {
+ logger = cfg.TestLogger
+ } else {
+ var logHookLast func(e *logg.Entry) error
+ if cfg.Configs.Base.PanicOnWarning {
+ logHookLast = loggers.PanicOnWarningHook
+ }
+ if cfg.StdOut == nil {
+ cfg.StdOut = os.Stdout
+ }
+ if cfg.StdErr == nil {
+ cfg.StdErr = os.Stderr
+ }
+ if cfg.LogLevel == 0 {
+ cfg.LogLevel = logg.LevelWarn
+ }
+
+ logOpts := loggers.Options{
+ Level: cfg.LogLevel,
+ DistinctLevel: logg.LevelWarn, // This will drop duplicate log warning and errors.
+ HandlerPost: logHookLast,
+ StdOut: cfg.StdOut,
+ StdErr: cfg.StdErr,
+ StoreErrors: conf.Watching(),
+ SuppressStatements: conf.IgnoredLogs(),
+ }
+ logger = loggers.New(logOpts)
+
+ }
+
+ memCache := dynacache.New(dynacache.Options{Watching: conf.Watching(), Log: logger})
+
+ var h *HugoSites
+ onSignalRebuild := func(ids ...identity.Identity) {
+ // This channel is buffered, but make sure we do this in a non-blocking way.
+ if cfg.ChangesFromBuild != nil {
+ go func() {
+ cfg.ChangesFromBuild <- ids
+ }()
+ }
+ }
+
+ firstSiteDeps := &deps.Deps{
+ Fs: cfg.Fs,
+ Log: logger,
+ Conf: conf,
+ BuildState: &deps.BuildState{
+ OnSignalRebuild: onSignalRebuild,
+ },
+ Counters: &deps.Counters{},
+ MemCache: memCache,
+ TranslationProvider: i18n.NewTranslationProvider(),
+ WasmDispatchers: warpc.AllDispatchers(
+ warpc.Options{
+ CompilationCacheDir: filepath.Join(conf.Dirs().CacheDir, "_warpc"),
+
+ // Katex is relatively slow.
+ PoolSize: 8,
+ Infof: logger.InfoCommand("wasm").Logf,
+ Warnf: logger.WarnCommand("wasm").Logf,
+ },
+ ),
+ }
+
+ if err := firstSiteDeps.Init(); err != nil {
+ return nil, err
+ }
+
+ batcherClient, err := esbuild.NewBatcherClient(firstSiteDeps)
+ if err != nil {
+ return nil, err
+ }
+ firstSiteDeps.JSBatcherClient = batcherClient
+
+ confm := cfg.Configs
+ if err := confm.Validate(logger); err != nil {
+ return nil, err
+ }
+ var sites []*Site
+
+ ns := &contentNodeShifter{
+ numLanguages: len(confm.Languages),
+ }
+
+ treeConfig := doctree.Config[contentNodeI]{
+ Shifter: ns,
+ }
+
+ pageTrees := &pageTrees{
+ treePages: doctree.New(
+ treeConfig,
+ ),
+ treeResources: doctree.New(
+ treeConfig,
+ ),
+ treeTaxonomyEntries: doctree.NewTreeShiftTree[*weightedContentNode](doctree.DimensionLanguage.Index(), len(confm.Languages)),
+ treePagesFromTemplateAdapters: doctree.NewTreeShiftTree[*pagesfromdata.PagesFromTemplate](doctree.DimensionLanguage.Index(), len(confm.Languages)),
+ }
+
+ pageTrees.createMutableTrees()
+
+ for i, confp := range confm.ConfigLangs() {
+ language := confp.Language()
+ if language.Disabled {
+ continue
+ }
+ k := language.Lang
+ conf := confm.LanguageConfigMap[k]
+ frontmatterHandler, err := pagemeta.NewFrontmatterHandler(firstSiteDeps.Log, conf.Frontmatter)
+ if err != nil {
+ return nil, err
+ }
+
+ langs.SetParams(language, conf.Params)
+
+ s := &Site{
+ conf: conf,
+ language: language,
+ languagei: i,
+ frontmatterHandler: frontmatterHandler,
+ store: maps.NewScratch(),
+ }
+
+ if i == 0 {
+ firstSiteDeps.Site = s
+ s.Deps = firstSiteDeps
+ } else {
+ d, err := firstSiteDeps.Clone(s, confp)
+ if err != nil {
+ return nil, err
+ }
+ s.Deps = d
+ }
+
+ s.pageMap = newPageMap(i, s, memCache, pageTrees)
+
+ s.pageFinder = newPageFinder(s.pageMap)
+ s.siteRefLinker, err = newSiteRefLinker(s)
+ if err != nil {
+ return nil, err
+ }
+ // Set up the main publishing chain.
+ pub, err := publisher.NewDestinationPublisher(
+ firstSiteDeps.ResourceSpec,
+ s.conf.OutputFormats.Config,
+ s.conf.MediaTypes.Config,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ s.publisher = pub
+ s.relatedDocsHandler = page.NewRelatedDocsHandler(s.conf.Related)
+ // Site deps end.
+
+ s.prepareInits()
+ sites = append(sites, s)
+ }
+
+ if len(sites) == 0 {
+ return nil, errors.New("no sites to build")
+ }
+
+ // Pull the default content language to the top, then sort the sites by language weight (if set) or lang.
+ defaultContentLanguage := confm.Base.DefaultContentLanguage
+ sort.Slice(sites, func(i, j int) bool {
+ li := sites[i].language
+ lj := sites[j].language
+ if li.Lang == defaultContentLanguage {
+ return true
+ }
+
+ if lj.Lang == defaultContentLanguage {
+ return false
+ }
+
+ if li.Weight != lj.Weight {
+ return li.Weight < lj.Weight
+ }
+ return li.Lang < lj.Lang
+ })
+
+ h, err = newHugoSites(cfg, firstSiteDeps, pageTrees, sites)
+ if err == nil && h == nil {
+ panic("hugo: newHugoSitesNew returned nil error and nil HugoSites")
+ }
+
+ return h, err
+}
+
+func newHugoSites(cfg deps.DepsCfg, d *deps.Deps, pageTrees *pageTrees, sites []*Site) (*HugoSites, error) {
+ numWorkers := config.GetNumWorkerMultiplier()
+ numWorkersSite := min(numWorkers, len(sites))
+ workersSite := para.New(numWorkersSite)
+
+ h := &HugoSites{
+ Sites: sites,
+ Deps: sites[0].Deps,
+ Configs: cfg.Configs,
+ workersSite: workersSite,
+ numWorkersSites: numWorkers,
+ numWorkers: numWorkers,
+ pageTrees: pageTrees,
+ cachePages: dynacache.GetOrCreatePartition[string,
+ page.Pages](d.MemCache, "/pags/all",
+ dynacache.OptionsPartition{Weight: 10, ClearWhen: dynacache.ClearOnRebuild},
+ ),
+ cacheContentSource: dynacache.GetOrCreatePartition[string, *resources.StaleValue[[]byte]](d.MemCache, "/cont/src", dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
+ translationKeyPages: maps.NewSliceCache[page.Page](),
+ currentSite: sites[0],
+ skipRebuildForFilenames: make(map[string]bool),
+ init: &hugoSitesInit{
+ data: lazy.New(),
+ gitInfo: lazy.New(),
+ },
+ }
+
+ // Assemble dependencies to be used in hugo.Deps.
+ var dependencies []*hugo.Dependency
+ var depFromMod func(m modules.Module) *hugo.Dependency
+ depFromMod = func(m modules.Module) *hugo.Dependency {
+ dep := &hugo.Dependency{
+ Path: m.Path(),
+ Version: m.Version(),
+ Time: m.Time(),
+ Vendor: m.Vendor(),
+ }
+
+ // These are pointers, but this all came from JSON so there's no recursive navigation,
+ // so just create new values.
+ if m.Replace() != nil {
+ dep.Replace = depFromMod(m.Replace())
+ }
+ if m.Owner() != nil {
+ dep.Owner = depFromMod(m.Owner())
+ }
+ return dep
+ }
+ for _, m := range d.Paths.AllModules() {
+ dependencies = append(dependencies, depFromMod(m))
+ }
+
+ h.hugoInfo = hugo.NewInfo(h.Configs.GetFirstLanguageConfig(), dependencies)
+
+ var prototype *deps.Deps
+ for i, s := range sites {
+ s.h = h
+ // The template store needs to be initialized after the h container is set on s.
+ if i == 0 {
+ templateStore, err := tplimpl.NewStore(
+ tplimpl.StoreOptions{
+ Fs: s.BaseFs.Layouts.Fs,
+ Log: s.Log,
+ DefaultContentLanguage: s.Conf.DefaultContentLanguage(),
+ Watching: s.Conf.Watching(),
+ PathParser: s.Conf.PathParser(),
+ Metrics: d.Metrics,
+ OutputFormats: s.conf.OutputFormats.Config,
+ MediaTypes: s.conf.MediaTypes.Config,
+ DefaultOutputFormat: s.conf.DefaultOutputFormat,
+ TaxonomySingularPlural: s.conf.Taxonomies,
+ }, tplimpl.SiteOptions{
+ Site: s,
+ TemplateFuncs: tplimplinit.CreateFuncMap(s.Deps),
+ })
+ if err != nil {
+ return nil, err
+ }
+ s.Deps.TemplateStore = templateStore
+ } else {
+ s.Deps.TemplateStore = prototype.TemplateStore.WithSiteOpts(
+ tplimpl.SiteOptions{
+ Site: s,
+ TemplateFuncs: tplimplinit.CreateFuncMap(s.Deps),
+ })
+ }
+ if err := s.Deps.Compile(prototype); err != nil {
+ return nil, err
+ }
+ if i == 0 {
+ prototype = s.Deps
+ }
+ }
+
+ h.fatalErrorHandler = &fatalErrorHandler{
+ h: h,
+ donec: make(chan bool),
+ }
+
+ h.init.data.Add(func(context.Context) (any, error) {
+ err := h.loadData()
+ if err != nil {
+ return nil, fmt.Errorf("failed to load data: %w", err)
+ }
+ return nil, nil
+ })
+
+ h.init.gitInfo.Add(func(context.Context) (any, error) {
+ err := h.loadGitInfo()
+ if err != nil {
+ return nil, fmt.Errorf("failed to load Git info: %w", err)
+ }
+ return nil, nil
+ })
+
+ return h, nil
+}
+
+// Returns the server port.
+func (s *Site) ServerPort() int {
+ return s.conf.C.BaseURL.Port()
+}
+
+// Returns the configured title for this Site.
+func (s *Site) Title() string {
+ return s.conf.Title
+}
+
+func (s *Site) Copyright() string {
+ return s.conf.Copyright
+}
+
+func (s *Site) Config() page.SiteConfig {
+ return page.SiteConfig{
+ Privacy: s.conf.Privacy,
+ Services: s.conf.Services,
+ }
+}
+
+func (s *Site) LanguageCode() string {
+ return s.Language().LanguageCode()
+}
+
+// Returns all Sites for all languages.
+func (s *Site) Sites() page.Sites {
+ sites := make(page.Sites, len(s.h.Sites))
+ for i, s := range s.h.Sites {
+ sites[i] = s.Site()
+ }
+ return sites
+}
+
+// Returns Site currently rendering.
+func (s *Site) Current() page.Site {
+ return s.h.currentSite
+}
+
+// MainSections returns the list of main sections.
+func (s *Site) MainSections() []string {
+ s.CheckReady()
+ return s.conf.C.MainSections
+}
+
+// Returns a struct with some information about the build.
+func (s *Site) Hugo() hugo.HugoInfo {
+ if s.h == nil {
+ panic("site: hugo: h not initialized")
+ }
+ if s.h.hugoInfo.Environment == "" {
+ panic("site: hugo: hugoInfo not initialized")
+ }
+ return s.h.hugoInfo
+}
+
+// Returns the BaseURL for this Site.
+func (s *Site) BaseURL() string {
+ return s.conf.C.BaseURL.WithPath
+}
+
+// Deprecated: Use .Site.Lastmod instead.
+func (s *Site) LastChange() time.Time {
+ s.CheckReady()
+ hugo.Deprecate(".Site.LastChange", "Use .Site.Lastmod instead.", "v0.123.0")
+ return s.lastmod
+}
+
+// Returns the last modification date of the content.
+func (s *Site) Lastmod() time.Time {
+ return s.lastmod
+}
+
+// Returns the Params configured for this site.
+func (s *Site) Params() maps.Params {
+ return s.conf.Params
+}
+
+// Deprecated: Use taxonomies instead.
+func (s *Site) Author() map[string]any {
+ if len(s.conf.Author) != 0 {
+ hugo.Deprecate(".Site.Author", "Implement taxonomy 'author' or use .Site.Params.Author instead.", "v0.124.0")
+ }
+ return s.conf.Author
+}
+
+// Deprecated: Use taxonomies instead.
+func (s *Site) Authors() page.AuthorList {
+ hugo.Deprecate(".Site.Authors", "Implement taxonomy 'authors' or use .Site.Params.Author instead.", "v0.124.0")
+ return page.AuthorList{}
+}
+
+// Deprecated: Use .Site.Params instead.
+func (s *Site) Social() map[string]string {
+ hugo.Deprecate(".Site.Social", "Implement taxonomy 'social' or use .Site.Params.Social instead.", "v0.124.0")
+ return s.conf.Social
+}
+
+func (s *Site) Param(key any) (any, error) {
+ return resource.Param(s, nil, key)
+}
+
+// Returns a map of all the data inside /data.
+func (s *Site) Data() map[string]any {
+ return s.s.h.Data()
+}
+
+func (s *Site) BuildDrafts() bool {
+ return s.conf.BuildDrafts
+}
+
+// Deprecated: Use hugo.IsMultilingual instead.
+func (s *Site) IsMultiLingual() bool {
+ hugo.Deprecate(".Site.IsMultiLingual", "Use hugo.IsMultilingual instead.", "v0.124.0")
+ return s.h.isMultilingual()
+}
+
+func (s *Site) LanguagePrefix() string {
+ prefix := s.GetLanguagePrefix()
+ if prefix == "" {
+ return ""
+ }
+ return "/" + prefix
+}
+
+func (s *Site) Site() page.Site {
+ return page.WrapSite(s)
+}
+
+func (s *Site) ForEeachIdentityByName(name string, f func(identity.Identity) bool) {
+ if id, found := siteidentities.FromString(name); found {
+ if f(id) {
+ return
+ }
+ }
+}
+
+// Pages returns all pages.
+// This is for the current language only.
+func (s *Site) Pages() page.Pages {
+ s.CheckReady()
+ return s.pageMap.getPagesInSection(
+ pageMapQueryPagesInSection{
+ pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
+ Path: "",
+ KeyPart: "global",
+ Include: pagePredicates.ShouldListGlobal,
+ },
+ Recursive: true,
+ IncludeSelf: true,
+ },
+ )
+}
+
+// RegularPages returns all the regular pages.
+// This is for the current language only.
+func (s *Site) RegularPages() page.Pages {
+ s.CheckReady()
+ return s.pageMap.getPagesInSection(
+ pageMapQueryPagesInSection{
+ pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
+ Path: "",
+ KeyPart: "global",
+ Include: pagePredicates.ShouldListGlobal.And(pagePredicates.KindPage),
+ },
+ Recursive: true,
+ },
+ )
+}
+
+// AllPages returns all pages for all sites.
+func (s *Site) AllPages() page.Pages {
+ s.CheckReady()
+ return s.h.Pages()
+}
+
+// AllRegularPages returns all regular pages for all sites.
+func (s *Site) AllRegularPages() page.Pages {
+ s.CheckReady()
+ return s.h.RegularPages()
+}
+
+func (s *Site) Store() *maps.Scratch {
+ return s.store
+}
+
+func (s *Site) CheckReady() {
+ if s.state != siteStateReady {
+ panic("this method cannot be called before the site is fully initialized")
+ }
+}
+
func (s *Site) Taxonomies() page.TaxonomyList {
+ s.CheckReady()
s.init.taxonomies.Do(context.Background())
return s.taxonomies
}
@@ -116,6 +701,9 @@ func (s *Site) prepareInits() {
s.init.prevNext = init.Branch(func(context.Context) (any, error) {
regularPages := s.RegularPages()
+ if s.conf.Page.NextPrevSortOrder == "asc" {
+ regularPages = regularPages.Reverse()
+ }
for i, p := range regularPages {
np, ok := p.(nextPrevProvider)
if !ok {
@@ -180,7 +768,11 @@ func (s *Site) prepareInits() {
)
for _, section := range sections {
- setNextPrev(section.RegularPages())
+ ps := section.RegularPages()
+ if s.conf.Page.NextPrevInSectionSortOrder == "asc" {
+ ps = ps.Reverse()
+ }
+ setNextPrev(ps)
}
return nil, nil
@@ -199,11 +791,8 @@ func (s *Site) prepareInits() {
})
}
-type siteRenderingContext struct {
- output.Format
-}
-
func (s *Site) Menus() navigation.Menus {
+ s.CheckReady()
s.init.menus.Do(context.Background())
return s.menus
}
@@ -216,7 +805,7 @@ func (s *Site) initRenderFormats() {
Tree: s.pageMap.treePages,
Handle: func(key string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
if p, ok := n.(*pageState); ok {
- for _, f := range p.m.configuredOutputFormats {
+ for _, f := range p.m.pageConfig.ConfiguredOutputFormats {
if !formatSet[f.Name] {
formats = append(formats, f)
formatSet[f.Name] = true
@@ -247,7 +836,7 @@ func (s *Site) initRenderFormats() {
s.renderFormats = formats
}
-func (s *Site) GetRelatedDocsHandler() *page.RelatedDocsHandler {
+func (s *Site) GetInternalRelatedDocsHandler() *page.RelatedDocsHandler {
return s.relatedDocsHandler
}
@@ -369,27 +958,54 @@ func (s *Site) watching() bool {
return s.h != nil && s.h.Configs.Base.Internal.Watch
}
-type whatChanged struct {
+type WhatChanged struct {
mu sync.Mutex
- contentChanged bool
- identitySet identity.Identities
+ needsPagesAssembly bool
+
+ ids map[identity.Identity]bool
}
-func (w *whatChanged) Add(ids ...identity.Identity) {
+func (w *WhatChanged) init() {
+ if w.ids == nil {
+ w.ids = make(map[identity.Identity]bool)
+ }
+}
+
+func (w *WhatChanged) Add(ids ...identity.Identity) {
w.mu.Lock()
defer w.mu.Unlock()
+ w.init()
+
for _, id := range ids {
- w.identitySet[id] = true
+ w.ids[id] = true
}
}
-func (w *whatChanged) Changes() []identity.Identity {
- if w == nil || w.identitySet == nil {
+func (w *WhatChanged) Clear() {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ w.clear()
+}
+
+func (w *WhatChanged) clear() {
+ w.ids = nil
+}
+
+func (w *WhatChanged) Changes() []identity.Identity {
+ if w == nil || w.ids == nil {
return nil
}
- return w.identitySet.AsSlice()
+ return xmaps.Keys(w.ids)
+}
+
+func (w *WhatChanged) Drain() []identity.Identity {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ ids := w.Changes()
+ w.clear()
+ return ids
}
// RegisterMediaTypes will register the Site's media types in the mime
@@ -649,6 +1265,8 @@ func (s *Site) assembleMenus() error {
// If page is still nill, we must make sure that we have a URL that considers baseURL etc.
if types.IsNil(me.Page) {
me.ConfiguredURL = s.createNodeMenuEntryURL(me.MenuConfig.URL)
+ } else {
+ navigation.SetPageValues(me, me.Page)
}
flat[twoD{name, me.KeyName()}] = me
@@ -765,6 +1383,7 @@ func (s *Site) getLanguagePermalinkLang(alwaysInSubDir bool) string {
func (s *Site) resetBuildState(sourceChanged bool) {
s.relatedDocsHandler = s.relatedDocsHandler.Clone()
s.init.Reset()
+ s.pageMap.Reset()
}
func (s *Site) errorCollator(results <-chan error, errs chan<- error) {
@@ -786,6 +1405,7 @@ func (s *Site) errorCollator(results <-chan error, errs chan<- error) {
// as possible for existing sites. Most sites will use {{ .Site.GetPage "section" "my/section" }},
// i.e. 2 arguments, so we test for that.
func (s *Site) GetPage(ref ...string) (page.Page, error) {
+ s.CheckReady()
p, err := s.s.getPageForRefs(ref...)
if p == nil {
@@ -818,7 +1438,7 @@ const (
pageDependencyScopeGlobal
)
-func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, d any, templ tpl.Template) error {
+func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, d any, templ *tplimpl.TemplInfo) error {
s.h.buildCounters.pageRenderCounter.Add(1)
renderBuffer := bp.GetBuffer()
defer bp.PutBuffer(renderBuffer)
@@ -877,8 +1497,8 @@ var infoOnMissingLayout = map[string]bool{
// hookRendererTemplate is the canonical implementation of all hooks.ITEMRenderer,
// where ITEM is the thing being hooked.
type hookRendererTemplate struct {
- templateHandler tpl.TemplateHandler
- templ tpl.Template
+ templateHandler *tplimpl.TemplateStore
+ templ *tplimpl.TemplInfo
resolvePosition func(ctx any) text.Position
}
@@ -894,6 +1514,18 @@ func (hr hookRendererTemplate) RenderCodeblock(cctx context.Context, w hugio.Fle
return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
}
+func (hr hookRendererTemplate) RenderPassthrough(cctx context.Context, w io.Writer, ctx hooks.PassthroughContext) error {
+ return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
+}
+
+func (hr hookRendererTemplate) RenderBlockquote(cctx context.Context, w hugio.FlexiWriter, ctx hooks.BlockquoteContext) error {
+ return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
+}
+
+func (hr hookRendererTemplate) RenderTable(cctx context.Context, w hugio.FlexiWriter, ctx hooks.TableContext) error {
+ return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
+}
+
func (hr hookRendererTemplate) ResolvePosition(ctx any) text.Position {
return hr.resolvePosition(ctx)
}
@@ -902,7 +1534,7 @@ func (hr hookRendererTemplate) IsDefaultCodeBlockRenderer() bool {
return false
}
-func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string, d any, w io.Writer, templ tpl.Template) (err error) {
+func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string, d any, w io.Writer, templ *tplimpl.TemplInfo) (err error) {
if templ == nil {
s.logMissingLayout(name, "", "", outputFormat)
return nil
@@ -912,8 +1544,12 @@ func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string,
panic("nil context")
}
- if err = s.Tmpl().ExecuteWithContext(ctx, templ, w, d); err != nil {
- return fmt.Errorf("render of %q failed: %w", name, err)
+ if err = s.GetTemplateStore().ExecuteWithContext(ctx, templ, w, d); err != nil {
+ filename := name
+ if p, ok := d.(*pageState); ok {
+ filename = p.String()
+ }
+ return fmt.Errorf("render of %q failed: %w", filename, err)
}
return
}
@@ -947,7 +1583,7 @@ func (s *Site) render(ctx *siteRenderContext) (err error) {
return err
}
- if ctx.outIdx == 0 {
+ if ctx.outIdx == 0 && s.h.buildCounter.Load() == 0 {
// Note that even if disableAliases is set, the aliases themselves are
// preserved on page. The motivation with this is to be able to generate
// 301 redirects in a .htaccess file and similar using a custom output format.
diff --git a/hugolib/site_new.go b/hugolib/site_new.go
deleted file mode 100644
index 496889295..000000000
--- a/hugolib/site_new.go
+++ /dev/null
@@ -1,560 +0,0 @@
-// Copyright 2024 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package hugolib
-
-import (
- "context"
- "errors"
- "fmt"
- "html/template"
- "os"
- "sort"
- "time"
-
- "github.com/bep/logg"
- "github.com/gohugoio/hugo/cache/dynacache"
- "github.com/gohugoio/hugo/common/hugo"
- "github.com/gohugoio/hugo/common/loggers"
- "github.com/gohugoio/hugo/common/maps"
- "github.com/gohugoio/hugo/common/para"
- "github.com/gohugoio/hugo/config"
- "github.com/gohugoio/hugo/config/allconfig"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/hugolib/doctree"
- "github.com/gohugoio/hugo/identity"
- "github.com/gohugoio/hugo/langs"
- "github.com/gohugoio/hugo/langs/i18n"
- "github.com/gohugoio/hugo/lazy"
- "github.com/gohugoio/hugo/modules"
- "github.com/gohugoio/hugo/navigation"
- "github.com/gohugoio/hugo/output"
- "github.com/gohugoio/hugo/publisher"
- "github.com/gohugoio/hugo/resources"
- "github.com/gohugoio/hugo/resources/page"
- "github.com/gohugoio/hugo/resources/page/pagemeta"
- "github.com/gohugoio/hugo/resources/page/siteidentities"
- "github.com/gohugoio/hugo/resources/resource"
- "github.com/gohugoio/hugo/tpl"
- "github.com/gohugoio/hugo/tpl/tplimpl"
-)
-
-var _ page.Site = (*Site)(nil)
-
-type Site struct {
- conf *allconfig.Config
- language *langs.Language
- languagei int
- pageMap *pageMap
-
- // The owning container.
- h *HugoSites
-
- *deps.Deps
-
- // Page navigation.
- *pageFinder
- taxonomies page.TaxonomyList
- menus navigation.Menus
-
- // Shortcut to the home page. Note that this may be nil if
- // home page, for some odd reason, is disabled.
- home *pageState
-
- // The last modification date of this site.
- lastmod time.Time
-
- relatedDocsHandler *page.RelatedDocsHandler
- siteRefLinker
- publisher publisher.Publisher
- frontmatterHandler pagemeta.FrontMatterHandler
-
- // We render each site for all the relevant output formats in serial with
- // this rendering context pointing to the current one.
- rc *siteRenderingContext
-
- // The output formats that we need to render this site in. This slice
- // will be fixed once set.
- // This will be the union of Site.Pages' outputFormats.
- // This slice will be sorted.
- renderFormats output.Formats
-
- // Lazily loaded site dependencies
- init *siteInit
-}
-
-func (s *Site) Debug() {
- fmt.Println("Debugging site", s.Lang(), "=>")
- // fmt.Println(s.pageMap.testDump())
-}
-
-// NewHugoSites creates HugoSites from the given config.
-func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
- conf := cfg.Configs.GetFirstLanguageConfig()
-
- var logger loggers.Logger
- if cfg.TestLogger != nil {
- logger = cfg.TestLogger
- } else {
- var logHookLast func(e *logg.Entry) error
- if cfg.Configs.Base.PanicOnWarning {
- logHookLast = loggers.PanicOnWarningHook
- }
- if cfg.LogOut == nil {
- cfg.LogOut = os.Stdout
- }
- if cfg.LogLevel == 0 {
- cfg.LogLevel = logg.LevelWarn
- }
-
- logOpts := loggers.Options{
- Level: cfg.LogLevel,
- DistinctLevel: logg.LevelWarn, // This will drop duplicate log warning and errors.
- HandlerPost: logHookLast,
- Stdout: cfg.LogOut,
- Stderr: cfg.LogOut,
- StoreErrors: conf.Watching(),
- SuppressStatements: conf.IgnoredLogs(),
- }
- logger = loggers.New(logOpts)
-
- }
-
- memCache := dynacache.New(dynacache.Options{Watching: conf.Watching(), Log: logger})
-
- firstSiteDeps := &deps.Deps{
- Fs: cfg.Fs,
- Log: logger,
- Conf: conf,
- MemCache: memCache,
- TemplateProvider: tplimpl.DefaultTemplateProvider,
- TranslationProvider: i18n.NewTranslationProvider(),
- }
-
- if err := firstSiteDeps.Init(); err != nil {
- return nil, err
- }
-
- confm := cfg.Configs
- if err := confm.Validate(logger); err != nil {
- return nil, err
- }
- var sites []*Site
-
- ns := &contentNodeShifter{
- numLanguages: len(confm.Languages),
- }
-
- treeConfig := doctree.Config[contentNodeI]{
- Shifter: ns,
- }
-
- pageTrees := &pageTrees{
- treePages: doctree.New(
- treeConfig,
- ),
- treeResources: doctree.New(
- treeConfig,
- ),
- treeTaxonomyEntries: doctree.NewTreeShiftTree[*weightedContentNode](doctree.DimensionLanguage.Index(), len(confm.Languages)),
- }
-
- pageTrees.createMutableTrees()
-
- for i, confp := range confm.ConfigLangs() {
- language := confp.Language()
- if language.Disabled {
- continue
- }
- k := language.Lang
- conf := confm.LanguageConfigMap[k]
- frontmatterHandler, err := pagemeta.NewFrontmatterHandler(firstSiteDeps.Log, conf.Frontmatter)
- if err != nil {
- return nil, err
- }
-
- langs.SetParams(language, conf.Params)
-
- s := &Site{
- conf: conf,
- language: language,
- languagei: i,
- frontmatterHandler: frontmatterHandler,
- }
-
- if i == 0 {
- firstSiteDeps.Site = s
- s.Deps = firstSiteDeps
- } else {
- d, err := firstSiteDeps.Clone(s, confp)
- if err != nil {
- return nil, err
- }
- s.Deps = d
- }
-
- s.pageMap = newPageMap(i, s, memCache, pageTrees)
-
- s.pageFinder = newPageFinder(s.pageMap)
- s.siteRefLinker, err = newSiteRefLinker(s)
- if err != nil {
- return nil, err
- }
- // Set up the main publishing chain.
- pub, err := publisher.NewDestinationPublisher(
- firstSiteDeps.ResourceSpec,
- s.conf.OutputFormats.Config,
- s.conf.MediaTypes.Config,
- )
- if err != nil {
- return nil, err
- }
-
- s.publisher = pub
- s.relatedDocsHandler = page.NewRelatedDocsHandler(s.conf.Related)
- // Site deps end.
-
- s.prepareInits()
- sites = append(sites, s)
- }
-
- if len(sites) == 0 {
- return nil, errors.New("no sites to build")
- }
-
- // Pull the default content language to the top, then sort the sites by language weight (if set) or lang.
- defaultContentLanguage := confm.Base.DefaultContentLanguage
- sort.Slice(sites, func(i, j int) bool {
- li := sites[i].language
- lj := sites[j].language
- if li.Lang == defaultContentLanguage {
- return true
- }
-
- if lj.Lang == defaultContentLanguage {
- return false
- }
-
- if li.Weight != lj.Weight {
- return li.Weight < lj.Weight
- }
- return li.Lang < lj.Lang
- })
-
- h, err := newHugoSites(cfg, firstSiteDeps, pageTrees, sites)
- if err == nil && h == nil {
- panic("hugo: newHugoSitesNew returned nil error and nil HugoSites")
- }
-
- return h, err
-}
-
-func newHugoSites(cfg deps.DepsCfg, d *deps.Deps, pageTrees *pageTrees, sites []*Site) (*HugoSites, error) {
- numWorkers := config.GetNumWorkerMultiplier()
- numWorkersSite := numWorkers
- if numWorkersSite > len(sites) {
- numWorkersSite = len(sites)
- }
- workersSite := para.New(numWorkersSite)
-
- h := &HugoSites{
- Sites: sites,
- Deps: sites[0].Deps,
- Configs: cfg.Configs,
- workersSite: workersSite,
- numWorkersSites: numWorkers,
- numWorkers: numWorkers,
- pageTrees: pageTrees,
- cachePages: dynacache.GetOrCreatePartition[string,
- page.Pages](d.MemCache, "/pags/all",
- dynacache.OptionsPartition{Weight: 10, ClearWhen: dynacache.ClearOnRebuild},
- ),
- cacheContentSource: dynacache.GetOrCreatePartition[string, *resources.StaleValue[[]byte]](d.MemCache, "/cont/src", dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
- translationKeyPages: maps.NewSliceCache[page.Page](),
- currentSite: sites[0],
- skipRebuildForFilenames: make(map[string]bool),
- init: &hugoSitesInit{
- data: lazy.New(),
- layouts: lazy.New(),
- gitInfo: lazy.New(),
- },
- }
-
- // Assemble dependencies to be used in hugo.Deps.
- var dependencies []*hugo.Dependency
- var depFromMod func(m modules.Module) *hugo.Dependency
- depFromMod = func(m modules.Module) *hugo.Dependency {
- dep := &hugo.Dependency{
- Path: m.Path(),
- Version: m.Version(),
- Time: m.Time(),
- Vendor: m.Vendor(),
- }
-
- // These are pointers, but this all came from JSON so there's no recursive navigation,
- // so just create new values.
- if m.Replace() != nil {
- dep.Replace = depFromMod(m.Replace())
- }
- if m.Owner() != nil {
- dep.Owner = depFromMod(m.Owner())
- }
- return dep
- }
- for _, m := range d.Paths.AllModules() {
- dependencies = append(dependencies, depFromMod(m))
- }
-
- h.hugoInfo = hugo.NewInfo(h.Configs.GetFirstLanguageConfig(), dependencies)
-
- var prototype *deps.Deps
- for i, s := range sites {
- s.h = h
- if err := s.Deps.Compile(prototype); err != nil {
- return nil, err
- }
- if i == 0 {
- prototype = s.Deps
- }
- }
-
- h.fatalErrorHandler = &fatalErrorHandler{
- h: h,
- donec: make(chan bool),
- }
-
- h.init.data.Add(func(context.Context) (any, error) {
- err := h.loadData()
- if err != nil {
- return nil, fmt.Errorf("failed to load data: %w", err)
- }
- return nil, nil
- })
-
- h.init.layouts.Add(func(context.Context) (any, error) {
- for _, s := range h.Sites {
- if err := s.Tmpl().(tpl.TemplateManager).MarkReady(); err != nil {
- return nil, err
- }
- }
- return nil, nil
- })
-
- h.init.gitInfo.Add(func(context.Context) (any, error) {
- err := h.loadGitInfo()
- if err != nil {
- return nil, fmt.Errorf("failed to load Git info: %w", err)
- }
- return nil, nil
- })
-
- return h, nil
-}
-
-// Deprecated: Use hugo.IsServer instead.
-func (s *Site) IsServer() bool {
- hugo.Deprecate(".Site.IsServer", "Use hugo.IsServer instead.", "v0.120.0")
- return s.conf.Internal.Running
-}
-
-// Returns the server port.
-func (s *Site) ServerPort() int {
- return s.conf.C.BaseURL.Port()
-}
-
-// Returns the configured title for this Site.
-func (s *Site) Title() string {
- return s.conf.Title
-}
-
-func (s *Site) Copyright() string {
- return s.conf.Copyright
-}
-
-// Deprecated: Use .Site.Home.OutputFormats.Get "rss" instead.
-func (s *Site) RSSLink() template.URL {
- hugo.Deprecate(".Site.RSSLink", "Use the Output Format's Permalink method instead, e.g. .OutputFormats.Get \"RSS\".Permalink", "v0.114.0")
- rssOutputFormat := s.home.OutputFormats().Get("rss")
- return template.URL(rssOutputFormat.Permalink())
-}
-
-func (s *Site) Config() page.SiteConfig {
- return page.SiteConfig{
- Privacy: s.conf.Privacy,
- Services: s.conf.Services,
- }
-}
-
-func (s *Site) LanguageCode() string {
- return s.Language().LanguageCode()
-}
-
-// Returns all Sites for all languages.
-func (s *Site) Sites() page.Sites {
- sites := make(page.Sites, len(s.h.Sites))
- for i, s := range s.h.Sites {
- sites[i] = s.Site()
- }
- return sites
-}
-
-// Returns Site currently rendering.
-func (s *Site) Current() page.Site {
- return s.h.currentSite
-}
-
-// MainSections returns the list of main sections.
-func (s *Site) MainSections() []string {
- return s.conf.C.MainSections
-}
-
-// Returns a struct with some information about the build.
-func (s *Site) Hugo() hugo.HugoInfo {
- if s.h == nil || s.h.hugoInfo.Environment == "" {
- panic("site: hugo: hugoInfo not initialized")
- }
- return s.h.hugoInfo
-}
-
-// Returns the BaseURL for this Site.
-func (s *Site) BaseURL() string {
- return s.conf.C.BaseURL.WithPath
-}
-
-// Deprecated: Use .Site.Lastmod instead.
-func (s *Site) LastChange() time.Time {
- hugo.Deprecate(".Site.LastChange", "Use .Site.Lastmod instead.", "v0.123.0")
- return s.lastmod
-}
-
-// Returns the last modification date of the content.
-func (s *Site) Lastmod() time.Time {
- return s.lastmod
-}
-
-// Returns the Params configured for this site.
-func (s *Site) Params() maps.Params {
- return s.conf.Params
-}
-
-// Deprecated: Use taxonomies instead.
-func (s *Site) Author() map[string]any {
- if len(s.conf.Author) != 0 {
- hugo.Deprecate(".Site.Author", "Use taxonomies instead.", "v0.124.0")
- }
- return s.conf.Author
-}
-
-// Deprecated: Use taxonomies instead.
-func (s *Site) Authors() page.AuthorList {
- hugo.Deprecate(".Site.Authors", "Use taxonomies instead.", "v0.124.0")
- return page.AuthorList{}
-}
-
-// Deprecated: Use .Site.Params instead.
-func (s *Site) Social() map[string]string {
- hugo.Deprecate(".Site.Social", "Use .Site.Params instead.", "v0.124.0")
- return s.conf.Social
-}
-
-// Deprecated: Use .Site.Config.Services.Disqus.Shortname instead.
-func (s *Site) DisqusShortname() string {
- hugo.Deprecate(".Site.DisqusShortname", "Use .Site.Config.Services.Disqus.Shortname instead.", "v0.120.0")
- return s.Config().Services.Disqus.Shortname
-}
-
-// Deprecated: Use .Site.Config.Services.GoogleAnalytics.ID instead.
-func (s *Site) GoogleAnalytics() string {
- hugo.Deprecate(".Site.GoogleAnalytics", "Use .Site.Config.Services.GoogleAnalytics.ID instead.", "v0.120.0")
- return s.Config().Services.GoogleAnalytics.ID
-}
-
-func (s *Site) Param(key any) (any, error) {
- return resource.Param(s, nil, key)
-}
-
-// Returns a map of all the data inside /data.
-func (s *Site) Data() map[string]any {
- return s.s.h.Data()
-}
-
-func (s *Site) BuildDrafts() bool {
- return s.conf.BuildDrafts
-}
-
-// Deprecated: Use hugo.IsMultilingual instead.
-func (s *Site) IsMultiLingual() bool {
- hugo.Deprecate(".Site.IsMultiLingual", "Use hugo.IsMultilingual instead.", "v0.124.0")
- return s.h.isMultilingual()
-}
-
-func (s *Site) LanguagePrefix() string {
- prefix := s.GetLanguagePrefix()
- if prefix == "" {
- return ""
- }
- return "/" + prefix
-}
-
-func (s *Site) Site() page.Site {
- return page.WrapSite(s)
-}
-
-func (s *Site) ForEeachIdentityByName(name string, f func(identity.Identity) bool) {
- if id, found := siteidentities.FromString(name); found {
- if f(id) {
- return
- }
- }
-}
-
-// Pages returns all pages.
-// This is for the current language only.
-func (s *Site) Pages() page.Pages {
- return s.pageMap.getPagesInSection(
- pageMapQueryPagesInSection{
- pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
- Path: "",
- KeyPart: "global",
- Include: pagePredicates.ShouldListGlobal,
- },
- Recursive: true,
- IncludeSelf: true,
- },
- )
-}
-
-// RegularPages returns all the regular pages.
-// This is for the current language only.
-func (s *Site) RegularPages() page.Pages {
- return s.pageMap.getPagesInSection(
- pageMapQueryPagesInSection{
- pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
- Path: "",
- KeyPart: "global",
- Include: pagePredicates.ShouldListGlobal.And(pagePredicates.KindPage),
- },
- Recursive: true,
- },
- )
-}
-
-// AllPages returns all pages for all sites.
-func (s *Site) AllPages() page.Pages {
- return s.h.Pages()
-}
-
-// AllRegularPages returns all regular pages for all sites.
-func (s *Site) AllRegularPages() page.Pages {
- return s.h.RegularPages()
-}
diff --git a/hugolib/site_output.go b/hugolib/site_output.go
index 2744c0133..3438ea9f7 100644
--- a/hugolib/site_output.go
+++ b/hugolib/site_output.go
@@ -27,6 +27,7 @@ func createDefaultOutputFormats(allFormats output.Formats) map[string]output.For
htmlOut, _ := allFormats.GetByName(output.HTMLFormat.Name)
robotsOut, _ := allFormats.GetByName(output.RobotsTxtFormat.Name)
sitemapOut, _ := allFormats.GetByName(output.SitemapFormat.Name)
+ httpStatus404Out, _ := allFormats.GetByName(output.HTTPStatus404HTMLFormat.Name)
defaultListTypes := output.Formats{htmlOut}
if rssFound {
@@ -42,7 +43,7 @@ func createDefaultOutputFormats(allFormats output.Formats) map[string]output.For
// Below are for consistency. They are currently not used during rendering.
kinds.KindSitemap: {sitemapOut},
kinds.KindRobotsTXT: {robotsOut},
- kinds.KindStatus404: {htmlOut},
+ kinds.KindStatus404: {httpStatus404Out},
}
// May be disabled
@@ -80,7 +81,7 @@ func createSiteOutputFormats(allFormats output.Formats, outputs map[string]any,
f, found := allFormats.GetByName(format)
if !found {
if rssDisabled && strings.EqualFold(format, "RSS") {
- // This is legacy behaviour. We used to have both
+ // This is legacy behavior. We used to have both
// a RSS page kind and output format.
continue
}
diff --git a/hugolib/site_output_test.go b/hugolib/site_output_test.go
index 3d95709c5..caec4c700 100644
--- a/hugolib/site_output_test.go
+++ b/hugolib/site_output_test.go
@@ -15,7 +15,6 @@ package hugolib
import (
"fmt"
- "html/template"
"strings"
"testing"
@@ -45,11 +44,13 @@ func doTestSiteWithPageOutputs(t *testing.T, outputs []string) {
siteConfig := `
baseURL = "http://example.com/blog"
-paginate = 1
defaultContentLanguage = "en"
disableKinds = ["section", "term", "taxonomy", "RSS", "sitemap", "robotsTXT", "404"]
+[pagination]
+pagerSize = 1
+
[Taxonomies]
tag = "tags"
category = "categories"
@@ -221,11 +222,13 @@ func TestRedefineRSSOutputFormat(t *testing.T) {
siteConfig := `
baseURL = "http://example.com/blog"
-paginate = 1
defaultContentLanguage = "en"
disableKinds = ["page", "section", "term", "taxonomy", "sitemap", "robotsTXT", "404"]
+[pagination]
+pagerSize = 1
+
[outputFormats]
[outputFormats.RSS]
mediatype = "application/rss"
@@ -249,7 +252,7 @@ baseName = "feed"
s := h.Sites[0]
// Issue #3450
- c.Assert(s.RSSLink(), qt.Equals, template.URL("http://example.com/blog/feed.xml"))
+ c.Assert(s.Home().OutputFormats().Get("rss").Permalink(), qt.Equals, "http://example.com/blog/feed.xml")
}
// Issue #3614
@@ -257,11 +260,13 @@ func TestDotLessOutputFormat(t *testing.T) {
siteConfig := `
baseURL = "http://example.com/blog"
-paginate = 1
defaultContentLanguage = "en"
disableKinds = ["page", "section", "term", "taxonomy", "sitemap", "robotsTXT", "404"]
+[pagination]
+pagerSize = 1
+
[mediaTypes]
[mediaTypes."text/nodot"]
delimiter = ""
@@ -382,7 +387,7 @@ func TestCreateSiteOutputFormats(t *testing.T) {
c.Assert(outputs[kinds.KindRSS], deepEqualsOutputFormats, output.Formats{output.RSSFormat})
c.Assert(outputs[kinds.KindSitemap], deepEqualsOutputFormats, output.Formats{output.SitemapFormat})
c.Assert(outputs[kinds.KindRobotsTXT], deepEqualsOutputFormats, output.Formats{output.RobotsTxtFormat})
- c.Assert(outputs[kinds.KindStatus404], deepEqualsOutputFormats, output.Formats{output.HTMLFormat})
+ c.Assert(outputs[kinds.KindStatus404], deepEqualsOutputFormats, output.Formats{output.HTTPStatus404HTMLFormat})
})
// Issue #4528
@@ -476,6 +481,7 @@ permalinkable = true
[outputFormats.nobase]
mediaType = "application/json"
permalinkable = true
+isPlainText = true
`
diff --git a/hugolib/site_render.go b/hugolib/site_render.go
index 1cd509fea..6dbb19827 100644
--- a/hugolib/site_render.go
+++ b/hugolib/site_render.go
@@ -20,11 +20,12 @@ import (
"strings"
"sync"
+ "github.com/bep/logg"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/hugolib/doctree"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/config"
- "github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/resources/kinds"
"github.com/gohugoio/hugo/resources/page"
@@ -33,6 +34,8 @@ import (
type siteRenderContext struct {
cfg *BuildCfg
+ infol logg.LevelLogger
+
// languageIdx is the zero based index of the site.
languageIdx int
@@ -54,7 +57,7 @@ func (s siteRenderContext) shouldRenderStandalonePage(kind string) bool {
return s.outIdx == 0
}
- if kind == kinds.KindStatus404 {
+ if kind == kinds.KindTemporary || kind == kinds.KindStatus404 {
// 1 for all output formats
return s.outIdx == 0
}
@@ -75,7 +78,7 @@ func (s *Site) renderPages(ctx *siteRenderContext) error {
wg := &sync.WaitGroup{}
- for i := 0; i < numWorkers; i++ {
+ for range numWorkers {
wg.Add(1)
go pageRenderer(ctx, s, pages, results, wg)
}
@@ -86,7 +89,7 @@ func (s *Site) renderPages(ctx *siteRenderContext) error {
Tree: s.pageMap.treePages,
Handle: func(key string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
if p, ok := n.(*pageState); ok {
- if cfg.shouldRender(p) {
+ if cfg.shouldRender(ctx.infol, p) {
select {
case <-s.h.Done():
return true, nil
@@ -111,7 +114,7 @@ func (s *Site) renderPages(ctx *siteRenderContext) error {
err := <-errs
if err != nil {
- return fmt.Errorf("failed to render pages: %w", herrors.ImproveIfNilPointer(err))
+ return fmt.Errorf("failed to render pages: %w", herrors.ImproveRenderErr(err))
}
return nil
}
@@ -222,18 +225,18 @@ func (s *Site) logMissingLayout(name, layout, kind, outputFormat string) {
}
// renderPaginator must be run after the owning Page has been rendered.
-func (s *Site) renderPaginator(p *pageState, templ tpl.Template) error {
- paginatePath := s.conf.PaginatePath
+func (s *Site) renderPaginator(p *pageState, templ *tplimpl.TemplInfo) error {
+ paginatePath := s.Conf.Pagination().Path
d := p.targetPathDescriptor
- f := p.s.rc.Format
+ f := p.outputFormat()
d.Type = f
if p.paginator.current == nil || p.paginator.current != p.paginator.current.First() {
panic(fmt.Sprintf("invalid paginator state for %q", p.pathOrTitle()))
}
- if f.IsHTML {
+ if f.IsHTML && !s.Conf.Pagination().DisableAliases {
// Write alias for page 1
d.Addends = fmt.Sprintf("/%s/%d", paginatePath, 1)
targetPaths := page.CreateTargetPaths(d)
@@ -334,6 +337,9 @@ func (s *Site) renderAliases() error {
// renderMainLanguageRedirect creates a redirect to the main language home,
// depending on if it lives in sub folder (e.g. /en) or not.
func (s *Site) renderMainLanguageRedirect() error {
+ if s.conf.DisableDefaultLanguageRedirect {
+ return nil
+ }
if s.h.Conf.IsMultihost() || !(s.h.Conf.DefaultContentLanguageInSubdir() || s.h.Conf.IsMultilingual()) {
// No need for a redirect
return nil
diff --git a/hugolib/site_sections.go b/hugolib/site_sections.go
index 1ce091f59..385f3f291 100644
--- a/hugolib/site_sections.go
+++ b/hugolib/site_sections.go
@@ -19,10 +19,12 @@ import (
// Sections returns the top level sections.
func (s *Site) Sections() page.Pages {
+ s.CheckReady()
return s.Home().Sections()
}
// Home is a shortcut to the home page, equivalent to .Site.GetPage "home".
func (s *Site) Home() page.Page {
+ s.CheckReady()
return s.s.home
}
diff --git a/hugolib/site_sections_test.go b/hugolib/site_sections_test.go
index 7fa15fb66..0bf166092 100644
--- a/hugolib/site_sections_test.go
+++ b/hugolib/site_sections_test.go
@@ -115,7 +115,7 @@ PAG|{{ .Title }}|{{ $sect.InSection . }}
{{ $sections := (.Site.GetPage "section" .Section).Sections.ByWeight }}
`)
- cfg.Set("paginate", 2)
+ cfg.Set("pagination.pagerSize", 2)
th, configs := newTestHelperFromProvider(cfg, fs, t)
diff --git a/hugolib/site_stats_test.go b/hugolib/site_stats_test.go
index 167194ef5..c045963f3 100644
--- a/hugolib/site_stats_test.go
+++ b/hugolib/site_stats_test.go
@@ -16,7 +16,6 @@ package hugolib
import (
"bytes"
"fmt"
- "io"
"testing"
"github.com/gohugoio/hugo/helpers"
@@ -32,9 +31,11 @@ func TestSiteStats(t *testing.T) {
siteConfig := `
baseURL = "http://example.com/blog"
-paginate = 1
defaultContentLanguage = "nn"
+[pagination]
+pagerSize = 1
+
[languages]
[languages.nn]
languageName = "Nynorsk"
@@ -67,15 +68,15 @@ aliases: [/Ali%d]
"_default/terms.html", "Terms List|{{ .Title }}|{{ .Content }}",
)
- for i := 0; i < 2; i++ {
- for j := 0; j < 2; j++ {
+ for i := range 2 {
+ for j := range 2 {
pageID := i + j + 1
b.WithContent(fmt.Sprintf("content/sect/p%d.md", pageID),
fmt.Sprintf(pageTemplate, pageID, fmt.Sprintf("- tag%d", j), fmt.Sprintf("- category%d", j), pageID))
}
}
- for i := 0; i < 5; i++ {
+ for i := range 5 {
b.WithContent(fmt.Sprintf("assets/image%d.png", i+1), "image")
}
@@ -87,14 +88,11 @@ aliases: [/Ali%d]
h.Sites[1].PathSpec.ProcessingStats,
}
- stats[0].Table(io.Discard)
- stats[1].Table(io.Discard)
-
var buff bytes.Buffer
helpers.ProcessingStatsTable(&buff, stats...)
- c.Assert(buff.String(), qt.Contains, "Pages | 21 | 7")
+ c.Assert(buff.String(), qt.Contains, "Pages │ 21 │ 7")
}
func TestSiteLastmod(t *testing.T) {
@@ -121,11 +119,10 @@ date: 2023-04-01
---
-- layouts/index.html --
site.Lastmod: {{ .Site.Lastmod.Format "2006-01-02" }}
-site.LastChange: {{ .Site.LastChange.Format "2006-01-02" }}
home.Lastmod: {{ site.Home.Lastmod.Format "2006-01-02" }}
`
b := Test(t, files)
- b.AssertFileContent("public/index.html", "site.Lastmod: 2023-04-01\nsite.LastChange: 2023-04-01\nhome.Lastmod: 2023-01-01")
+ b.AssertFileContent("public/index.html", "site.Lastmod: 2023-04-01\nhome.Lastmod: 2023-01-01")
}
diff --git a/hugolib/site_test.go b/hugolib/site_test.go
index 1de1d688a..199c878cd 100644
--- a/hugolib/site_test.go
+++ b/hugolib/site_test.go
@@ -147,8 +147,8 @@ func TestLastChange(t *testing.T) {
s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{SkipRender: true})
- c.Assert(s.LastChange().IsZero(), qt.Equals, false)
- c.Assert(s.LastChange().Year(), qt.Equals, 2017)
+ c.Assert(s.Lastmod().IsZero(), qt.Equals, false)
+ c.Assert(s.Lastmod().Year(), qt.Equals, 2017)
}
// Issue #_index
@@ -372,14 +372,14 @@ func TestMainSections(t *testing.T) {
b := newTestSitesBuilder(c).WithViper(v)
- for i := 0; i < 20; i++ {
+ for i := range 20 {
b.WithContent(fmt.Sprintf("page%d.md", i), `---
title: "Page"
---
`)
}
- for i := 0; i < 5; i++ {
+ for i := range 5 {
b.WithContent(fmt.Sprintf("blog/page%d.md", i), `---
title: "Page"
tags: ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
@@ -387,7 +387,7 @@ tags: ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
`)
}
- for i := 0; i < 3; i++ {
+ for i := range 3 {
b.WithContent(fmt.Sprintf("docs/page%d.md", i), `---
title: "Page"
---
@@ -427,8 +427,8 @@ mainSections=["a", "b"]
{{/* Behaviour before Hugo 0.112.0. */}}
MainSections Params: {{ site.Params.mainSections }}|
MainSections Site method: {{ site.MainSections }}|
-
-
+
+
`
b := Test(t, files)
@@ -478,8 +478,8 @@ disableKinds = ['RSS','sitemap','taxonomy','term']
-- layouts/index.html --
MainSections Params: {{ site.Params.mainSections }}|
MainSections Site method: {{ site.MainSections }}|
-
-
+
+
`
b := Test(t, files)
@@ -615,7 +615,7 @@ var weightedPage5 = `+++
weight = "5"
title = "Five"
-[_build]
+[build]
render = "never"
+++
Front Matter with Ordered Pages 5`
@@ -787,9 +787,12 @@ func TestGroupedPages(t *testing.T) {
t.Errorf("PageGroup has unexpected number of pages. First group should have '%d' pages, got '%d' pages", 2, len(byparam[0].Pages))
}
- _, err = s.RegularPages().GroupByParam("not_exist")
- if err == nil {
- t.Errorf("GroupByParam didn't return an expected error")
+ byNonExistentParam, err := s.RegularPages().GroupByParam("not_exist")
+ if err != nil {
+ t.Errorf("GroupByParam returned an error when it shouldn't")
+ }
+ if len(byNonExistentParam) != 0 {
+ t.Errorf("PageGroup array has unexpected elements. Group length should be '%d', got '%d'", 0, len(byNonExistentParam))
}
byOnlyOneParam, err := s.RegularPages().GroupByParam("only_one")
@@ -975,7 +978,6 @@ func TestRefLinking(t *testing.T) {
{".", "", true, "/level2/level3/"},
{"./", "", true, "/level2/level3/"},
- // try to confuse parsing
{"embedded.dot.md", "", true, "/level2/level3/embedded.dot/"},
// test empty link, as well as fragment only link
diff --git a/hugolib/site_url_test.go b/hugolib/site_url_test.go
index 8efaae3a2..091251f80 100644
--- a/hugolib/site_url_test.go
+++ b/hugolib/site_url_test.go
@@ -91,13 +91,13 @@ Do not go gentle into that good night.
`
cfg, fs := newTestCfg()
- cfg.Set("paginate", 1)
+ cfg.Set("pagination.pagerSize", 1)
th, configs := newTestHelperFromProvider(cfg, fs, t)
writeSource(t, fs, filepath.Join("content", "sect1", "_index.md"), fmt.Sprintf(st, "/ss1/"))
writeSource(t, fs, filepath.Join("content", "sect2", "_index.md"), fmt.Sprintf(st, "/ss2/"))
- for i := 0; i < 5; i++ {
+ for i := range 5 {
writeSource(t, fs, filepath.Join("content", "sect1", fmt.Sprintf("p%d.md", i+1)), pt)
writeSource(t, fs, filepath.Join("content", "sect2", fmt.Sprintf("p%d.md", i+1)), pt)
}
diff --git a/hugolib/sitemap_test.go b/hugolib/sitemap_test.go
index 1c2642468..922ecbc12 100644
--- a/hugolib/sitemap_test.go
+++ b/hugolib/sitemap_test.go
@@ -139,7 +139,7 @@ weight = 1
languageName = "English"
[languages.nn]
weight = 2
--- layouts/_default/list.xml --
+-- layouts/list.xml --
Site: {{ .Site.Title }}|
-- layouts/home --
Home.
diff --git a/hugolib/taxonomy_test.go b/hugolib/taxonomy_test.go
index bfdfd8dfd..7aeaa780c 100644
--- a/hugolib/taxonomy_test.go
+++ b/hugolib/taxonomy_test.go
@@ -76,12 +76,15 @@ func TestTaxonomiesWithAndWithoutContentFile(t *testing.T) {
}
func doTestTaxonomiesWithAndWithoutContentFile(t *testing.T, uglyURLs bool) {
+ t.Helper()
+
siteConfig := `
baseURL = "http://example.com/blog"
titleCaseStyle = "firstupper"
uglyURLs = %t
-paginate = 1
defaultContentLanguage = "en"
+[pagination]
+pagerSize = 1
[Taxonomies]
tag = "tags"
category = "categories"
@@ -313,7 +316,7 @@ func TestTaxonomiesNextGenLoops(t *testing.T) {
`)
- for i := 0; i < 10; i++ {
+ for i := range 10 {
b.WithContent(fmt.Sprintf("page%d.md", i+1), `
---
Title: "Taxonomy!"
@@ -970,3 +973,58 @@ title: p1
b.AssertFileExists("public/ja/s1/index.html", false) // failing test
b.AssertFileExists("public/ja/s1/category/index.html", true)
}
+
+func BenchmarkTaxonomiesGetTerms(b *testing.B) {
+ createBuilders := func(b *testing.B, numPages int) []*IntegrationTestBuilder {
+ files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+disableKinds = ["RSS", "sitemap", "section"]
+[taxononomies]
+tag = "tags"
+-- layouts/_default/list.html --
+List.
+-- layouts/_default/single.html --
+GetTerms.tags: {{ range .GetTerms "tags" }}{{ .Title }}|{{ end }}
+-- content/_index.md --
+`
+
+ tagsVariants := []string{
+ "tags: ['a']",
+ "tags: ['a', 'b']",
+ "tags: ['a', 'b', 'c']",
+ "tags: ['a', 'b', 'c', 'd']",
+ "tags: ['a', 'b', 'd', 'e']",
+ "tags: ['a', 'b', 'c', 'd', 'e']",
+ "tags: ['a', 'd']",
+ "tags: ['a', 'f']",
+ }
+
+ for i := 1; i < numPages; i++ {
+ tags := tagsVariants[i%len(tagsVariants)]
+ files += fmt.Sprintf("\n-- content/posts/p%d.md --\n---\n%s\n---", i+1, tags)
+ }
+ cfg := IntegrationTestConfig{
+ T: b,
+ TxtarString: files,
+ }
+ builders := make([]*IntegrationTestBuilder, b.N)
+
+ for i := range builders {
+ builders[i] = NewIntegrationTestBuilder(cfg)
+ }
+
+ b.ResetTimer()
+
+ return builders
+ }
+
+ for _, numPages := range []int{100, 1000, 10000, 20000} {
+ b.Run(fmt.Sprintf("pages_%d", numPages), func(b *testing.B) {
+ builders := createBuilders(b, numPages)
+ for i := 0; i < b.N; i++ {
+ builders[i].Build()
+ }
+ })
+ }
+}
diff --git a/hugolib/template_test.go b/hugolib/template_test.go
index 1c60a88b3..a08f83cb8 100644
--- a/hugolib/template_test.go
+++ b/hugolib/template_test.go
@@ -26,6 +26,8 @@ import (
"github.com/gohugoio/hugo/hugofs"
)
+// TODO(bep) keep this until we release v0.146.0 as a security against breaking changes, but it's rather messy and mostly duplicate of
+// tests in the tplimpl package, so eventually just remove it.
func TestTemplateLookupOrder(t *testing.T) {
var (
fs *hugofs.Fs
@@ -185,6 +187,9 @@ func TestTemplateLookupOrder(t *testing.T) {
} {
this := this
+ if this.name != "Variant 1" {
+ continue
+ }
t.Run(this.name, func(t *testing.T) {
// TODO(bep) there are some function vars need to pull down here to enable => t.Parallel()
cfg, fs = newTestCfg()
@@ -200,7 +205,7 @@ Some content
}
buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{})
- // helpers.PrintFs(s.BaseFs.Layouts.Fs, "", os.Stdout)
+ // s.TemplateStore.PrintDebug("", 0, os.Stdout)
this.assert(t)
})
@@ -250,7 +255,7 @@ Content.
Base %d: {{ block "main" . }}FOO{{ end }}
`
- for i := 0; i < numPages; i++ {
+ for i := range numPages {
id := i + 1
b.WithContent(fmt.Sprintf("page%d.md", id), fmt.Sprintf(pageTemplate, id, id))
b.WithTemplates(fmt.Sprintf("_default/layout%d.html", id), fmt.Sprintf(singleTemplate, id))
@@ -258,7 +263,7 @@ Base %d: {{ block "main" . }}FOO{{ end }}
}
b.Build(BuildCfg{})
- for i := 0; i < numPages; i++ {
+ for i := range numPages {
id := i + 1
b.AssertFileContent(fmt.Sprintf("public/page%d/index.html", id), fmt.Sprintf(`Base %d: %d`, id, id))
}
@@ -270,11 +275,11 @@ func TestTemplateNoBasePlease(t *testing.T) {
b := newTestSitesBuilder(t).WithSimpleConfigFile()
b.WithTemplates("_default/list.html", `
- {{ define "main" }}
- Bonjour
- {{ end }}
+{{ define "main" }}
+ Bonjour
+{{ end }}
- {{ printf "list" }}
+{{ printf "list" }}
`)
@@ -344,40 +349,43 @@ title: %s
b.AssertFileContent("public/p1/index.html", `Single: P1`)
})
- t.Run("baseof", func(t *testing.T) {
- t.Parallel()
- b := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
+ {
+ }
+}
- b.WithTemplatesAdded(
- "index.html", `{{ define "main" }}Main Home En{{ end }}`,
- "index.fr.html", `{{ define "main" }}Main Home Fr{{ end }}`,
- "baseof.html", `Baseof en: {{ block "main" . }}main block{{ end }}`,
- "baseof.fr.html", `Baseof fr: {{ block "main" . }}main block{{ end }}`,
- "mysection/baseof.html", `Baseof mysection: {{ block "main" . }}mysection block{{ end }}`,
- "_default/single.html", `{{ define "main" }}Main Default Single{{ end }}`,
- "_default/list.html", `{{ define "main" }}Main Default List{{ end }}`,
- )
+func TestTemplateLookupSitBaseOf(t *testing.T) {
+ t.Parallel()
+ b := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
- b.WithContent("mysection/p1.md", `---
+ b.WithTemplatesAdded(
+ "index.html", `{{ define "main" }}Main Home En{{ end }}`,
+ "index.fr.html", `{{ define "main" }}Main Home Fr{{ end }}`,
+ "baseof.html", `Baseof en: {{ block "main" . }}main block{{ end }}`,
+ "baseof.fr.html", `Baseof fr: {{ block "main" . }}main block{{ end }}`,
+ "mysection/baseof.html", `Baseof mysection: {{ block "main" . }}mysection block{{ end }}`,
+ "_default/single.html", `{{ define "main" }}Main Default Single{{ end }}`,
+ "_default/list.html", `{{ define "main" }}Main Default List{{ end }}`,
+ )
+
+ b.WithContent("mysection/p1.md", `---
title: My Page
---
`)
- b.CreateSites().Build(BuildCfg{})
+ b.CreateSites().Build(BuildCfg{})
- b.AssertFileContent("public/en/index.html", `Baseof en: Main Home En`)
- b.AssertFileContent("public/fr/index.html", `Baseof fr: Main Home Fr`)
- b.AssertFileContent("public/en/mysection/index.html", `Baseof mysection: Main Default List`)
- b.AssertFileContent("public/en/mysection/p1/index.html", `Baseof mysection: Main Default Single`)
- })
+ b.AssertFileContent("public/en/index.html", `Baseof en: Main Home En`)
+ b.AssertFileContent("public/fr/index.html", `Baseof fr: Main Home Fr`)
+ b.AssertFileContent("public/en/mysection/index.html", `Baseof mysection: Main Default List`)
+ b.AssertFileContent("public/en/mysection/p1/index.html", `Baseof mysection: Main Default Single`)
}
func TestTemplateFuncs(t *testing.T) {
b := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
homeTpl := `Site: {{ site.Language.Lang }} / {{ .Site.Language.Lang }} / {{ site.BaseURL }}
-Sites: {{ site.Sites.First.Home.Language.Lang }}
+Sites: {{ site.Sites.Default.Home.Language.Lang }}
Hugo: {{ hugo.Generator }}
`
@@ -460,7 +468,7 @@ complex: 80: 80
func TestPartialWithZeroedArgs(t *testing.T) {
b := newTestSitesBuilder(t)
b.WithTemplatesAdded("index.html",
- `
+ `
X{{ partial "retval" dict }}X
X{{ partial "retval" slice }}X
X{{ partial "retval" "" }}X
@@ -696,7 +704,7 @@ func TestApplyWithNamespace(t *testing.T) {
b.WithTemplates(
"index.html", `
-{{ $b := slice " a " " b " " c" }}
+{{ $b := slice " a " " b " " c" }}
{{ $a := apply $b "strings.Trim" "." " " }}
a: {{ $a }}
`,
@@ -706,3 +714,18 @@ a: {{ $a }}
b.AssertFileContent("public/index.html", `a: [a b c]`)
}
+
+// Legacy behavior for internal templates.
+func TestOverrideInternalTemplate(t *testing.T) {
+ files := `
+-- hugo.toml --
+baseURL = "https://example.org"
+-- layouts/index.html --
+{{ template "_internal/google_analytics_async.html" . }}
+-- layouts/_internal/google_analytics_async.html --
+Overridden.
+`
+ b := Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Overridden.")
+}
diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go
index dab693623..2007b658d 100644
--- a/hugolib/testhelpers_test.go
+++ b/hugolib/testhelpers_test.go
@@ -260,7 +260,7 @@ disable = false
respectDoNotTrack = true
[privacy.instagram]
simple = true
-[privacy.twitter]
+[privacy.x]
enableDNT = true
[privacy.vimeo]
disable = false
@@ -292,11 +292,13 @@ func (s *sitesBuilder) WithDefaultMultiSiteConfig() *sitesBuilder {
defaultMultiSiteConfig := `
baseURL = "http://example.com/blog"
-paginate = 1
disablePathToLower = true
defaultContentLanguage = "en"
defaultContentLanguageInSubdir = true
+[pagination]
+pagerSize = 1
+
[permalinks]
other = "/somewhere/else/:filename"
@@ -324,7 +326,8 @@ plaque = "plaques"
weight = 30
title = "På nynorsk"
languageName = "Nynorsk"
-paginatePath = "side"
+[Languages.nn.pagination]
+path = "side"
[Languages.nn.Taxonomies]
lag = "lag"
[[Languages.nn.menu.main]]
@@ -336,7 +339,8 @@ weight = 1
weight = 40
title = "På bokmål"
languageName = "Bokmål"
-paginatePath = "side"
+[Languages.nb.pagination]
+path = "side"
[Languages.nb.Taxonomies]
lag = "lag"
` + commonConfigSections
@@ -834,7 +838,7 @@ func (s *sitesBuilder) NpmInstall() hexec.Runner {
var err error
sc.Exec.Allow, err = security.NewWhitelist("npm")
s.Assert(err, qt.IsNil)
- ex := hexec.New(sc)
+ ex := hexec.New(sc, s.workingDir, loggers.NewDefault())
command, err := ex.New("npm", "install")
s.Assert(err, qt.IsNil)
return command
diff --git a/hugoreleaser.env b/hugoreleaser.env
index 1635ecf78..6da749524 100644
--- a/hugoreleaser.env
+++ b/hugoreleaser.env
@@ -1,7 +1,66 @@
# Release env.
# These will be replaced by script before release.
-HUGORELEASER_TAG=v0.125.6
-HUGORELEASER_COMMITISH=69ede10edcd539380914bbee58d4d32953dd8b43
+HUGORELEASER_TAG=v0.147.9
+HUGORELEASER_COMMITISH=29bdbde19c288d190e889294a862103c6efb70bf
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hugoreleaser.toml b/hugoreleaser.toml
deleted file mode 100644
index d516bd34b..000000000
--- a/hugoreleaser.toml
+++ /dev/null
@@ -1,239 +0,0 @@
-project = "hugo"
-
-# In Hugo v0.103.0 we removed the archive name replacements (e.g. amd64 => 64bit).
-# Using standard GOOS/GOARCH values makes it easier for scripts out there,
-# but to prevent breakage in Netlify etc. that has adopted to the old names,
-# we create aliases for the most common variants.
-# According to download numbers from v0.101.0, these are by a good margin the two most popular:
-# hugo_extended_0.101.0_Linux-64bit.tar.gz Downloaded 129,016 times
-# hugo_0.101.0_Linux-64bit.tar.gz Downloaded 87,846 times
-# This replacement will create 2 extra alias archives.
-archive_alias_replacements = { "linux-amd64.tar.gz" = "Linux-64bit.tar.gz" }
-
-[go_settings]
- go_proxy = "https://proxy.golang.org"
- go_exe = "go"
-
-[build_settings]
- binary = "hugo"
- flags = ["-buildmode", "exe"]
- env = ["CGO_ENABLED=0"]
- ldflags = "-s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio"
-
-[archive_settings]
- name_template = "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}"
- extra_files = [
- { source_path = "README.md", target_path = "README.md" },
- { source_path = "LICENSE", target_path = "LICENSE" },
- ]
- [archive_settings.type]
- format = "tar.gz"
- extension = ".tar.gz"
-
-[release_settings]
- name = "${HUGORELEASER_TAG}"
- type = "github"
- repository = "hugo"
- repository_owner = "gohugoio"
- draft = true
- prerelease = false
-
- [release_settings.release_notes_settings]
- # Use Hugoreleaser's autogenerated release notes.
- generate = true
-
- # Collapse releases with < 10 changes below one title.
- short_threshold = 10
- short_title = "What's Changed"
-
- groups = [
- # Group the changes in the release notes by title.
- # You need at least one.
- # The groups will be tested in order until a match is found.
- # The titles will so be listed in the given order in the release note.
- # Any match with ignore=true title will be dropped.
- { regexp = "Merge commit|Squashed|releaser:", ignore = true },
- { title = "Note", regexp = "(note|deprecated)", ordinal = 10 },
- { title = "Bug fixes", regexp = "fix", ordinal = 15 },
- { title = "Dependency Updates", regexp = "deps", ordinal = 30 },
- { title = "Build Setup", regexp = "(snap|release|update to)", ordinal = 40 },
- { title = "Documentation", regexp = "(doc|readme)", ordinal = 40 },
- { title = "Improvements", regexp = ".*", ordinal = 20 },
- ]
-
-[[builds]]
- path = "container1/unix/regular"
-
- [[builds.os]]
- goos = "darwin"
- [[builds.os.archs]]
- goarch = "universal"
- [[builds.os]]
- goos = "linux"
- [[builds.os.archs]]
- goarch = "amd64"
- [[builds.os.archs]]
- goarch = "arm64"
- [[builds.os.archs]]
- goarch = "arm"
- [builds.os.archs.build_settings]
- env = ["CGO_ENABLED=0", "GOARM=7"]
-
- # Unix BSD variants
- [[builds.os]]
- goos = "dragonfly"
- [[builds.os.archs]]
- goarch = "amd64"
- [[builds.os]]
- goos = "freebsd"
- [[builds.os.archs]]
- goarch = "amd64"
- [[builds.os]]
- goos = "netbsd"
- [[builds.os.archs]]
- goarch = "amd64"
- [[builds.os]]
- goos = "openbsd"
- [[builds.os.archs]]
- goarch = "amd64"
- [[builds.os]]
- goos = "solaris"
- [[builds.os.archs]]
- goarch = "amd64"
-
-[[builds]]
- path = "container1/unix/extended"
-
- [builds.build_settings]
- flags = ["-buildmode", "exe", "-tags", "extended"]
- env = ["CGO_ENABLED=1"]
-
- [[builds.os]]
- goos = "darwin"
- [builds.os.build_settings]
- env = ["CGO_ENABLED=1", "CC=o64-clang", "CXX=o64-clang++"]
- [[builds.os.archs]]
- goarch = "universal"
- [[builds.os]]
- goos = "linux"
- [[builds.os.archs]]
- goarch = "amd64"
-
-[[builds]]
- path = "container2/linux/extended"
-
- [builds.build_settings]
- flags = ["-buildmode", "exe", "-tags", "extended"]
-
- [[builds.os]]
- goos = "linux"
- [builds.os.build_settings]
- env = [
- "CGO_ENABLED=1",
- "CC=aarch64-linux-gnu-gcc",
- "CXX=aarch64-linux-gnu-g++",
- ]
- [[builds.os.archs]]
- goarch = "arm64"
-
-[[builds]]
- path = "container1/windows/regular"
-
- [[builds.os]]
- goos = "windows"
- [builds.os.build_settings]
- binary = "hugo.exe"
- [[builds.os.archs]]
- goarch = "amd64"
- [[builds.os.archs]]
- goarch = "arm64"
-
-[[builds]]
- path = "container1/windows/extended"
-
- [builds.build_settings]
- flags = ["-buildmode", "exe", "-tags", "extended"]
- env = [
- "CGO_ENABLED=1",
- "CC=x86_64-w64-mingw32-gcc",
- "CXX=x86_64-w64-mingw32-g++",
- ]
- ldflags = "-s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio -extldflags '-static'"
-
- [[builds.os]]
- goos = "windows"
- [builds.os.build_settings]
- binary = "hugo.exe"
- [[builds.os.archs]]
- goarch = "amd64"
-
-[[archives]]
- paths = ["builds/container1/unix/regular/**"]
-[[archives]]
- paths = ["builds/container1/unix/extended/**"]
- [archives.archive_settings]
- name_template = "{{ .Project }}_extended_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}"
-[[archives]]
- # Only extended builds in container2.
- paths = ["builds/container2/**"]
- [archives.archive_settings]
- name_template = "{{ .Project }}_extended_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}"
-[[archives]]
- paths = ["builds/**/windows/regular/**"]
- [archives.archive_settings.type]
- format = "zip"
- extension = ".zip"
-[[archives]]
- paths = ["builds/**/windows/extended/**"]
- [archives.archive_settings]
- name_template = "{{ .Project }}_extended_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}"
- [archives.archive_settings.type]
- format = "zip"
- extension = ".zip"
-[[archives]]
- paths = ["builds/**/regular/linux/{arm64,amd64}"]
- [archives.archive_settings]
- binary_dir = "/usr/local/bin"
- extra_files = []
- [archives.archive_settings.type]
- format = "_plugin"
- extension = ".deb"
- [archives.archive_settings.plugin]
- id = "deb"
- type = "gorun"
- command = "github.com/gohugoio/hugoreleaser-archive-plugins/deb@v0.6.1"
- [archives.archive_settings.custom_settings]
- vendor = "gohugo.io"
- homepage = "https://github.com/gohugoio/hugo"
- maintainer = "Bjørn Erik Pedersen "
- description = "A fast and flexible Static Site Generator written in Go."
- license = "Apache-2.0"
-[[archives]]
- paths = ["builds/**/extended/linux/{arm64,amd64}"]
- [archives.archive_settings]
- binary_dir = "/usr/local/bin"
- extra_files = []
- name_template = "{{ .Project }}_extended_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}"
- [archives.archive_settings.type]
- format = "_plugin"
- extension = ".deb"
- [archives.archive_settings.plugin]
- id = "deb"
- type = "gorun"
- command = "github.com/gohugoio/hugoreleaser-archive-plugins/deb@latest"
- [archives.archive_settings.custom_settings]
- vendor = "gohugo.io"
- homepage = "https://github.com/gohugoio/hugo"
- maintainer = "Bjørn Erik Pedersen "
- description = "A fast and flexible Static Site Generator written in Go."
- license = "Apache-2.0"
-
-[[releases]]
- paths = ["archives/**"]
- path = "r1"
-
- # The above should allow the following build commands:
- # hugoreleaser build -paths "builds/container1/**"
- # hugoreleaser build -paths "builds/container2/**"
- # hugoreleaser archive
- # hugoreleaser release
diff --git a/hugoreleaser.yaml b/hugoreleaser.yaml
new file mode 100644
index 000000000..368bc898f
--- /dev/null
+++ b/hugoreleaser.yaml
@@ -0,0 +1,272 @@
+project: hugo
+
+# Common definitions.
+definitions:
+ archive_type_zip: &archive_type_zip
+ type:
+ format: zip
+ extension: .zip
+ env_extended_linux: &env_extended_linux
+ - CGO_ENABLED=1
+ - CC=aarch64-linux-gnu-gcc
+ - CXX=aarch64-linux-gnu-g++
+ env_extended_windows: &env_extended_windows
+ - CGO_ENABLED=1
+ - CC=x86_64-w64-mingw32-gcc
+ - CXX=x86_64-w64-mingw32-g++
+ env_extended_darwin: &env_extended_darwin
+ - CGO_ENABLED=1
+ - CC=o64-clang
+ - CXX=o64-clang++
+ name_template_extended_withdeploy: &name_template_extended_withdeploy "{{ .Project }}_extended_withdeploy_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}"
+ name_template_extended: &name_template_extended "{{ .Project }}_extended_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}"
+ archive_deb: &archive_deb
+ binary_dir: /usr/local/bin
+ extra_files: []
+ type:
+ format: _plugin
+ extension: .deb
+ plugin:
+ id: deb
+ type: gorun
+ command: github.com/gohugoio/hugoreleaser-archive-plugins/deb@latest
+ custom_settings:
+ vendor: gohugo.io
+ homepage: https://github.com/gohugoio/hugo
+ maintainer: Bjørn Erik Pedersen
+ description: A fast and flexible Static Site Generator written in Go.
+ license: Apache-2.0
+archive_alias_replacements:
+ linux-amd64.tar.gz: Linux-64bit.tar.gz
+go_settings:
+ go_proxy: https://proxy.golang.org
+ go_exe: go
+build_settings:
+ binary: hugo
+ flags:
+ - -buildmode
+ - exe
+ env:
+ - CGO_ENABLED=0
+ ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio
+archive_settings:
+ name_template: "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}"
+ extra_files:
+ - source_path: README.md
+ target_path: README.md
+ - source_path: LICENSE
+ target_path: LICENSE
+ type:
+ format: tar.gz
+ extension: .tar.gz
+release_settings:
+ name: ${HUGORELEASER_TAG}
+ type: github
+ repository: hugo
+ repository_owner: gohugoio
+ draft: true
+ prerelease: false
+ release_notes_settings:
+ generate: true
+ short_threshold: 10
+ short_title: What's Changed
+ groups:
+ - regexp: "Merge commit|Squashed|releaser:"
+ ignore: true
+ - title: Note
+ regexp: (note|deprecated)
+ ordinal: 10
+ - title: Bug fixes
+ regexp: fix
+ ordinal: 15
+ - title: Dependency Updates
+ regexp: deps
+ ordinal: 30
+ - title: Build Setup
+ regexp: (snap|release|update to)
+ ordinal: 40
+ - title: Documentation
+ regexp: (doc|readme)
+ ordinal: 40
+ - title: Improvements
+ regexp: .*
+ ordinal: 20
+builds:
+ - path: container1/unix/regular
+ os:
+ - goos: darwin
+ archs:
+ - goarch: universal
+ - goos: linux
+ archs:
+ - goarch: amd64
+ - goarch: arm64
+ - goarch: arm
+ build_settings:
+ env:
+ - CGO_ENABLED=0
+ - GOARM=7
+ - goos: dragonfly
+ archs:
+ - goarch: amd64
+ - goos: freebsd
+ archs:
+ - goarch: amd64
+ - goos: netbsd
+ archs:
+ - goarch: amd64
+ - goos: openbsd
+ archs:
+ - goarch: amd64
+ - goos: solaris
+ archs:
+ - goarch: amd64
+ - path: container1/unix/extended
+ build_settings:
+ flags:
+ - -buildmode
+ - exe
+ - -tags
+ - extended
+ env:
+ - CGO_ENABLED=1
+ os:
+ - goos: darwin
+ build_settings:
+ env: *env_extended_darwin
+ archs:
+ - goarch: universal
+ - goos: linux
+ archs:
+ - goarch: amd64
+ - path: container1/unix/extended-withdeploy
+ build_settings:
+ flags:
+ - -buildmode
+ - exe
+ - -tags
+ - extended,withdeploy
+ env:
+ - CGO_ENABLED=1
+ os:
+ - goos: darwin
+ build_settings:
+ env: *env_extended_darwin
+ archs:
+ - goarch: universal
+ - goos: linux
+ archs:
+ - goarch: amd64
+ - path: container2/linux/extended
+ build_settings:
+ flags:
+ - -buildmode
+ - exe
+ - -tags
+ - extended
+ os:
+ - goos: linux
+ build_settings:
+ env: *env_extended_linux
+ archs:
+ - goarch: arm64
+ - path: container2/linux/extended-withdeploy
+ build_settings:
+ flags:
+ - -buildmode
+ - exe
+ - -tags
+ - extended,withdeploy
+ os:
+ - goos: linux
+ build_settings:
+ env: *env_extended_linux
+ archs:
+ - goarch: arm64
+ - path: container1/windows/regular
+ os:
+ - goos: windows
+ build_settings:
+ binary: hugo.exe
+ archs:
+ - goarch: amd64
+ - goarch: arm64
+ - path: container1/windows/extended
+ build_settings:
+ flags:
+ - -buildmode
+ - exe
+ - -tags
+ - extended
+ env: *env_extended_windows
+ ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio -extldflags '-static'
+ os:
+ - goos: windows
+ build_settings:
+ binary: hugo.exe
+ archs:
+ - goarch: amd64
+ - path: container1/windows/extended-withdeploy
+ build_settings:
+ flags:
+ - -buildmode
+ - exe
+ - -tags
+ - extended,withdeploy
+ env: *env_extended_windows
+ ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio -extldflags '-static'
+ os:
+ - goos: windows
+ build_settings:
+ binary: hugo.exe
+ archs:
+ - goarch: amd64
+archives:
+ - paths:
+ - builds/container1/unix/regular/**
+ - paths:
+ - builds/container1/unix/extended/**
+ archive_settings:
+ name_template: *name_template_extended
+ - paths:
+ - builds/container1/unix/extended-withdeploy/**
+ archive_settings:
+ name_template: *name_template_extended_withdeploy
+ - paths:
+ - builds/container2/*/extended/**
+ archive_settings:
+ name_template: *name_template_extended
+ - paths:
+ - builds/container2/*/extended-withdeploy/**
+ archive_settings:
+ name_template: *name_template_extended_withdeploy
+ - paths:
+ - builds/**/windows/regular/**
+ archive_settings: *archive_type_zip
+ - paths:
+ - builds/**/windows/extended/**
+ archive_settings:
+ name_template: *name_template_extended
+ <<: *archive_type_zip
+ - paths:
+ - builds/**/windows/extended-withdeploy/**
+ archive_settings:
+ name_template: *name_template_extended_withdeploy
+ <<: *archive_type_zip
+ - paths:
+ - builds/**/regular/linux/{arm64,amd64}
+ archive_settings: *archive_deb
+ - paths:
+ - builds/**/extended/linux/{arm64,amd64}
+ archive_settings:
+ name_template: *name_template_extended
+ <<: *archive_deb
+ - paths:
+ - builds/**/extended-withdeploy/linux/{arm64,amd64}
+ archive_settings:
+ name_template: *name_template_extended_withdeploy
+ <<: *archive_deb
+releases:
+ - paths:
+ - archives/**
+ path: r1
diff --git a/identity/finder.go b/identity/finder.go
index 91fac7237..9d9f9d138 100644
--- a/identity/finder.go
+++ b/identity/finder.go
@@ -27,7 +27,7 @@ func NewFinder(cfg FinderConfig) *Finder {
}
var searchIDPool = sync.Pool{
- New: func() interface{} {
+ New: func() any {
return &searchID{seen: make(map[Manager]bool)}
},
}
@@ -45,9 +45,7 @@ func putSearchID(sid *searchID) {
sid.dp = nil
sid.peq = nil
sid.eqer = nil
- for k := range sid.seen {
- delete(sid.seen, k)
- }
+ clear(sid.seen)
searchIDPool.Put(sid)
}
@@ -122,17 +120,21 @@ func (f *Finder) Contains(id, in Identity, maxDepth int) FinderResult {
defer putSearchID(sid)
- if r := f.checkOne(sid, in, 0); r > 0 {
+ r := FinderNotFound
+ if i := f.checkOne(sid, in, 0); i > r {
+ r = i
+ }
+ if r == FinderFound {
return r
}
m := GetDependencyManager(in)
if m != nil {
- if r := f.checkManager(sid, m, 0); r > 0 {
- return r
+ if i := f.checkManager(sid, m, 0); i > r {
+ r = i
}
}
- return FinderNotFound
+ return r
}
func (f *Finder) checkMaxDepth(sid *searchID, level int) FinderResult {
@@ -279,15 +281,18 @@ func (f *Finder) search(sid *searchID, m Manager, depth int) FinderResult {
var r FinderResult
m.forEeachIdentity(
func(v Identity) bool {
- if r > 0 {
- panic("should be terminated")
+ i := f.checkOne(sid, v, depth)
+ if i > r {
+ r = i
}
- r = f.checkOne(sid, v, depth)
- if r > 0 {
+ if r == FinderFound {
return true
}
m := GetDependencyManager(v)
- if r = f.checkManager(sid, m, depth+1); r > 0 {
+ if i := f.checkManager(sid, m, depth+1); i > r {
+ r = i
+ }
+ if r == FinderFound {
return true
}
return false
diff --git a/identity/identity.go b/identity/identity.go
index f924f335c..c78ed0fdd 100644
--- a/identity/identity.go
+++ b/identity/identity.go
@@ -33,6 +33,9 @@ const (
// GenghisKhan is an Identity everyone relates to.
GenghisKhan = StringIdentity("__genghiskhan")
+
+ StructuralChangeAdd = StringIdentity("__structural_change_add")
+ StructuralChangeRemove = StringIdentity("__structural_change_remove")
)
var NopManager = new(nopManager)
@@ -82,9 +85,8 @@ func FirstIdentity(v any) Identity {
var result Identity = Anonymous
WalkIdentitiesShallow(v, func(level int, id Identity) bool {
result = id
- return true
+ return result != Anonymous
})
-
return result
}
@@ -146,6 +148,7 @@ func (d DependencyManagerProviderFunc) GetDependencyManager() Manager {
// DependencyManagerScopedProvider provides a manager for dependencies with a given scope.
type DependencyManagerScopedProvider interface {
GetDependencyManagerForScope(scope int) Manager
+ GetDependencyManagerForScopesAll() []Manager
}
// ForEeachIdentityProvider provides a way iterate over identities.
@@ -241,6 +244,11 @@ type IdentityProvider interface {
GetIdentity() Identity
}
+// SignalRebuilder is an optional interface for types that can signal a rebuild.
+type SignalRebuilder interface {
+ SignalRebuild(ids ...Identity)
+}
+
// IncrementByOne implements Incrementer adding 1 every time Incr is called.
type IncrementByOne struct {
counter uint64
@@ -303,11 +311,13 @@ type identityManager struct {
func (im *identityManager) AddIdentity(ids ...Identity) {
im.mu.Lock()
+ defer im.mu.Unlock()
for _, id := range ids {
if id == nil || id == Anonymous {
continue
}
+
if _, found := im.ids[id]; !found {
if im.onAddIdentity != nil {
im.onAddIdentity(id)
@@ -315,7 +325,6 @@ func (im *identityManager) AddIdentity(ids ...Identity) {
im.ids[id] = true
}
}
- im.mu.Unlock()
}
func (im *identityManager) AddIdentityForEach(ids ...ForEeachIdentityProvider) {
@@ -350,6 +359,10 @@ func (im *identityManager) GetDependencyManagerForScope(int) Manager {
return im
}
+func (im *identityManager) GetDependencyManagerForScopesAll() []Manager {
+ return []Manager{im}
+}
+
func (im *identityManager) String() string {
return fmt.Sprintf("IdentityManager(%s)", im.name)
}
@@ -496,6 +509,10 @@ func probablyEq(a, b Identity) bool {
return true
}
+ if a2, ok := a.(compare.ProbablyEqer); ok && a2.ProbablyEq(b) {
+ return true
+ }
+
if a2, ok := a.(IsProbablyDependentProvider); ok {
return a2.IsProbablyDependent(b)
}
diff --git a/identity/identity_test.go b/identity/identity_test.go
index d003caaf0..f9b04aa14 100644
--- a/identity/identity_test.go
+++ b/identity/identity_test.go
@@ -25,7 +25,7 @@ import (
func BenchmarkIdentityManager(b *testing.B) {
createIds := func(num int) []identity.Identity {
ids := make([]identity.Identity, num)
- for i := 0; i < num; i++ {
+ for i := range num {
name := fmt.Sprintf("id%d", i)
ids[i] = &testIdentity{base: name, name: name}
}
@@ -108,10 +108,10 @@ func BenchmarkIsNotDependent(b *testing.B) {
newNestedManager := func(depth, count int) identity.Manager {
m1 := identity.NewManager("")
- for i := 0; i < depth; i++ {
+ for range depth {
m2 := identity.NewManager("")
m1.AddIdentity(m2)
- for j := 0; j < count; j++ {
+ for j := range count {
id := fmt.Sprintf("id%d", j)
m2.AddIdentity(&testIdentity{id, id, "", ""})
}
diff --git a/identity/identityhash.go b/identity/identityhash.go
deleted file mode 100644
index 8760ff64d..000000000
--- a/identity/identityhash.go
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright 2024 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package identity
-
-import (
- "strconv"
-
- "github.com/mitchellh/hashstructure"
-)
-
-// HashString returns a hash from the given elements.
-// It will panic if the hash cannot be calculated.
-// Note that this hash should be used primarily for identity, not for change detection as
-// it in the more complex values (e.g. Page) will not hash the full content.
-func HashString(vs ...any) string {
- hash := HashUint64(vs...)
- return strconv.FormatUint(hash, 10)
-}
-
-// HashUint64 returns a hash from the given elements.
-// It will panic if the hash cannot be calculated.
-// Note that this hash should be used primarily for identity, not for change detection as
-// it in the more complex values (e.g. Page) will not hash the full content.
-func HashUint64(vs ...any) uint64 {
- var o any
- if len(vs) == 1 {
- o = toHashable(vs[0])
- } else {
- elements := make([]any, len(vs))
- for i, e := range vs {
- elements[i] = toHashable(e)
- }
- o = elements
- }
-
- hash, err := hashstructure.Hash(o, nil)
- if err != nil {
- panic(err)
- }
- return hash
-}
-
-type keyer interface {
- Key() string
-}
-
-// For structs, hashstructure.Hash only works on the exported fields,
-// so rewrite the input slice for known identity types.
-func toHashable(v any) any {
- switch t := v.(type) {
- case keyer:
- return t.Key()
- case IdentityProvider:
- return t.GetIdentity()
- default:
- return v
- }
-}
diff --git a/internal/js/api.go b/internal/js/api.go
new file mode 100644
index 000000000..30180dece
--- /dev/null
+++ b/internal/js/api.go
@@ -0,0 +1,51 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package js
+
+import (
+ "context"
+
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+// BatcherClient is used to do JS batch operations.
+type BatcherClient interface {
+ New(id string) (Batcher, error)
+ Store() *maps.Cache[string, Batcher]
+}
+
+// BatchPackage holds a group of JavaScript resources.
+type BatchPackage interface {
+ Groups() map[string]resource.Resources
+}
+
+// Batcher is used to build JavaScript packages.
+type Batcher interface {
+ Build(context.Context) (BatchPackage, error)
+ Config(ctx context.Context) OptionsSetter
+ Group(ctx context.Context, id string) BatcherGroup
+}
+
+// BatcherGroup is a group of scripts and instances.
+type BatcherGroup interface {
+ Instance(sid, iid string) OptionsSetter
+ Runner(id string) OptionsSetter
+ Script(id string) OptionsSetter
+}
+
+// OptionsSetter is used to set options for a batch, script or instance.
+type OptionsSetter interface {
+ SetOptions(map[string]any) string
+}
diff --git a/internal/js/esbuild/batch-esm-runner.gotmpl b/internal/js/esbuild/batch-esm-runner.gotmpl
new file mode 100644
index 000000000..3193b4c30
--- /dev/null
+++ b/internal/js/esbuild/batch-esm-runner.gotmpl
@@ -0,0 +1,20 @@
+{{ range $i, $e := .Scripts -}}
+ {{ if eq .Export "*" }}
+ {{- printf "import %s as Script%d from %q;" .Export $i .Import -}}
+ {{ else -}}
+ {{- printf "import { %s as Script%d } from %q;" .Export $i .Import -}}
+ {{ end -}}
+{{ end -}}
+{{ range $i, $e := .Runners }}
+ {{- printf "import { %s as Run%d } from %q;" .Export $i .Import -}}
+{{ end -}}
+{{ if .Runners -}}
+ let group = { id: "{{ $.ID }}", scripts: [] }
+ {{ range $i, $e := .Scripts -}}
+ group.scripts.push({{ .RunnerJSON $i }});
+ {{ end -}}
+ {{ range $i, $e := .Runners -}}
+ {{ $id := printf "Run%d" $i }}
+ {{ $id }}(group);
+ {{ end -}}
+{{ end -}}
diff --git a/internal/js/esbuild/batch.go b/internal/js/esbuild/batch.go
new file mode 100644
index 000000000..aa50cf2c1
--- /dev/null
+++ b/internal/js/esbuild/batch.go
@@ -0,0 +1,1444 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package esbuild provides functions for building JavaScript resources.
+package esbuild
+
+import (
+ "bytes"
+ "context"
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "io"
+ "path"
+ "path/filepath"
+ "reflect"
+ "sort"
+ "strings"
+ "sync"
+ "sync/atomic"
+
+ "github.com/evanw/esbuild/pkg/api"
+ "github.com/gohugoio/hugo/cache/dynacache"
+ "github.com/gohugoio/hugo/common/hugio"
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/paths"
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/internal/js"
+ "github.com/gohugoio/hugo/lazy"
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/gohugoio/hugo/resources/resource_factories/create"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
+ "github.com/mitchellh/mapstructure"
+ "github.com/spf13/cast"
+)
+
+var _ js.Batcher = (*batcher)(nil)
+
+const (
+ NsBatch = "_hugo-js-batch"
+
+ propsKeyImportContext = "importContext"
+ propsResoure = "resource"
+)
+
+//go:embed batch-esm-runner.gotmpl
+var runnerTemplateStr string
+
+var _ js.BatchPackage = (*Package)(nil)
+
+var _ buildToucher = (*optsHolder[scriptOptions])(nil)
+
+var (
+ _ buildToucher = (*scriptGroup)(nil)
+ _ isBuiltOrTouchedProvider = (*scriptGroup)(nil)
+)
+
+func NewBatcherClient(deps *deps.Deps) (js.BatcherClient, error) {
+ c := &BatcherClient{
+ d: deps,
+ buildClient: NewBuildClient(deps.BaseFs.Assets, deps.ResourceSpec),
+ createClient: create.New(deps.ResourceSpec),
+ batcherStore: maps.NewCache[string, js.Batcher](),
+ bundlesStore: maps.NewCache[string, js.BatchPackage](),
+ }
+
+ deps.BuildEndListeners.Add(func(...any) bool {
+ c.bundlesStore.Reset()
+ return false
+ })
+
+ return c, nil
+}
+
+func (o optionsMap[K, C]) ByKey() optionsGetSetters[K, C] {
+ var values []optionsGetSetter[K, C]
+ for _, v := range o {
+ values = append(values, v)
+ }
+
+ sort.Slice(values, func(i, j int) bool {
+ return values[i].Key().String() < values[j].Key().String()
+ })
+
+ return values
+}
+
+func (o *opts[K, C]) Compiled() C {
+ o.h.checkCompileErr()
+ return o.h.compiled
+}
+
+func (os optionsGetSetters[K, C]) Filter(predicate func(K) bool) optionsGetSetters[K, C] {
+ var a optionsGetSetters[K, C]
+ for _, v := range os {
+ if predicate(v.Key()) {
+ a = append(a, v)
+ }
+ }
+ return a
+}
+
+func (o *optsHolder[C]) IdentifierBase() string {
+ return o.optionsID
+}
+
+func (o *opts[K, C]) Key() K {
+ return o.key
+}
+
+func (o *opts[K, C]) Reset() {
+ mu := o.once.ResetWithLock()
+ defer mu.Unlock()
+ o.h.resetCounter++
+}
+
+func (o *opts[K, C]) Get(id uint32) js.OptionsSetter {
+ var b *optsHolder[C]
+ o.once.Do(func() {
+ b = o.h
+ b.setBuilt(id)
+ })
+ return b
+}
+
+func (o *opts[K, C]) GetIdentity() identity.Identity {
+ return o.h
+}
+
+func (o *optsHolder[C]) SetOptions(m map[string]any) string {
+ o.optsSetCounter++
+ o.optsPrev = o.optsCurr
+ o.optsCurr = m
+ o.compiledPrev = o.compiled
+ o.compiled, o.compileErr = o.compiled.compileOptions(m, o.defaults)
+ o.checkCompileErr()
+ return ""
+}
+
+// ValidateBatchID validates the given ID according to some very
+func ValidateBatchID(id string, isTopLevel bool) error {
+ if id == "" {
+ return fmt.Errorf("id must be set")
+ }
+ // No Windows slashes.
+ if strings.Contains(id, "\\") {
+ return fmt.Errorf("id must not contain backslashes")
+ }
+
+ // Allow forward slashes in top level IDs only.
+ if !isTopLevel && strings.Contains(id, "/") {
+ return fmt.Errorf("id must not contain forward slashes")
+ }
+
+ return nil
+}
+
+func newIsBuiltOrTouched() isBuiltOrTouched {
+ return isBuiltOrTouched{
+ built: make(buildIDs),
+ touched: make(buildIDs),
+ }
+}
+
+func newOpts[K any, C optionsCompiler[C]](key K, optionsID string, defaults defaultOptionValues) *opts[K, C] {
+ return &opts[K, C]{
+ key: key,
+ h: &optsHolder[C]{
+ optionsID: optionsID,
+ defaults: defaults,
+ isBuiltOrTouched: newIsBuiltOrTouched(),
+ },
+ }
+}
+
+// BatcherClient is a client for building JavaScript packages.
+type BatcherClient struct {
+ d *deps.Deps
+
+ once sync.Once
+ runnerTemplate *tplimpl.TemplInfo
+
+ createClient *create.Client
+ buildClient *BuildClient
+
+ batcherStore *maps.Cache[string, js.Batcher]
+ bundlesStore *maps.Cache[string, js.BatchPackage]
+}
+
+// New creates a new Batcher with the given ID.
+// This will be typically created once and reused across rebuilds.
+func (c *BatcherClient) New(id string) (js.Batcher, error) {
+ var initErr error
+ c.once.Do(func() {
+ // We should fix the initialization order here (or use the Go template package directly), but we need to wait
+ // for the Hugo templates to be ready.
+ tmpl, err := c.d.TemplateStore.TextParse("batch-esm-runner", runnerTemplateStr)
+ if err != nil {
+ initErr = err
+ return
+ }
+ c.runnerTemplate = tmpl
+ })
+
+ if initErr != nil {
+ return nil, initErr
+ }
+
+ dependencyManager := c.d.Conf.NewIdentityManager("jsbatch_" + id)
+ configID := "config_" + id
+
+ b := &batcher{
+ id: id,
+ scriptGroups: make(map[string]*scriptGroup),
+ dependencyManager: dependencyManager,
+ client: c,
+ configOptions: newOpts[scriptID, configOptions](
+ scriptID(configID),
+ configID,
+ defaultOptionValues{},
+ ),
+ }
+
+ c.d.BuildEndListeners.Add(func(...any) bool {
+ b.reset()
+ return false
+ })
+
+ idFinder := identity.NewFinder(identity.FinderConfig{})
+
+ c.d.OnChangeListeners.Add(func(ids ...identity.Identity) bool {
+ for _, id := range ids {
+ if r := idFinder.Contains(id, b.dependencyManager, 50); r > 0 {
+ b.staleVersion.Add(1)
+ return false
+ }
+
+ sp, ok := id.(identity.DependencyManagerScopedProvider)
+ if !ok {
+ continue
+ }
+ idms := sp.GetDependencyManagerForScopesAll()
+
+ for _, g := range b.scriptGroups {
+ g.forEachIdentity(func(id2 identity.Identity) bool {
+ bt, ok := id2.(buildToucher)
+ if !ok {
+ return false
+ }
+ for _, id3 := range idms {
+ // This handles the removal of the only source for a script group (e.g. all shortcodes in a contnt page).
+ // Note the very shallow search.
+ if r := idFinder.Contains(id2, id3, 0); r > 0 {
+ bt.setTouched(b.buildCount)
+ return false
+ }
+ }
+ return false
+ })
+ }
+ }
+
+ return false
+ })
+
+ return b, nil
+}
+
+func (c *BatcherClient) Store() *maps.Cache[string, js.Batcher] {
+ return c.batcherStore
+}
+
+func (c *BatcherClient) buildBatchGroup(ctx context.Context, t *batchGroupTemplateContext) (resource.Resource, string, error) {
+ var buf bytes.Buffer
+
+ if err := c.d.GetTemplateStore().ExecuteWithContext(ctx, c.runnerTemplate, &buf, t); err != nil {
+ return nil, "", err
+ }
+
+ s := paths.AddLeadingSlash(t.keyPath + ".js")
+ r, err := c.createClient.FromString(s, buf.String())
+ if err != nil {
+ return nil, "", err
+ }
+
+ return r, s, nil
+}
+
+// Package holds a group of JavaScript resources.
+type Package struct {
+ id string
+ b *batcher
+
+ groups map[string]resource.Resources
+}
+
+func (p *Package) Groups() map[string]resource.Resources {
+ return p.groups
+}
+
+type batchGroupTemplateContext struct {
+ keyPath string
+ ID string
+ Runners []scriptRunnerTemplateContext
+ Scripts []scriptBatchTemplateContext
+}
+
+type batcher struct {
+ mu sync.Mutex
+ id string
+ buildCount uint32
+ staleVersion atomic.Uint32
+ scriptGroups scriptGroups
+
+ client *BatcherClient
+ dependencyManager identity.Manager
+
+ configOptions optionsGetSetter[scriptID, configOptions]
+
+ // The last successfully built package.
+ // If this is non-nil and not stale, we can reuse it (e.g. on server rebuilds)
+ prevBuild *Package
+}
+
+// Build builds the batch if not already built or if it's stale.
+func (b *batcher) Build(ctx context.Context) (js.BatchPackage, error) {
+ key := dynacache.CleanKey(b.id + ".js")
+ p, err := b.client.bundlesStore.GetOrCreate(key, func() (js.BatchPackage, error) {
+ return b.build(ctx)
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to build JS batch %q: %w", b.id, err)
+ }
+ return p, nil
+}
+
+func (b *batcher) Config(ctx context.Context) js.OptionsSetter {
+ return b.configOptions.Get(b.buildCount)
+}
+
+func (b *batcher) Group(ctx context.Context, id string) js.BatcherGroup {
+ if err := ValidateBatchID(id, false); err != nil {
+ panic(err)
+ }
+
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ group, found := b.scriptGroups[id]
+ if !found {
+ idm := b.client.d.Conf.NewIdentityManager("jsbatch_" + id)
+ b.dependencyManager.AddIdentity(idm)
+
+ group = &scriptGroup{
+ id: id, b: b,
+ isBuiltOrTouched: newIsBuiltOrTouched(),
+ dependencyManager: idm,
+ scriptsOptions: make(optionsMap[scriptID, scriptOptions]),
+ instancesOptions: make(optionsMap[instanceID, paramsOptions]),
+ runnersOptions: make(optionsMap[scriptID, scriptOptions]),
+ }
+ b.scriptGroups[id] = group
+ }
+
+ group.setBuilt(b.buildCount)
+
+ return group
+}
+
+func (b *batcher) isStale() bool {
+ if b.staleVersion.Load() > 0 {
+ return true
+ }
+
+ if b.removeNotSet() {
+ return true
+ }
+
+ if b.configOptions.isStale() {
+ return true
+ }
+
+ for _, v := range b.scriptGroups {
+ if v.isStale() {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (b *batcher) build(ctx context.Context) (js.BatchPackage, error) {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+ defer func() {
+ b.staleVersion.Store(0)
+ b.buildCount++
+ }()
+
+ if b.prevBuild != nil {
+ if !b.isStale() {
+ return b.prevBuild, nil
+ }
+ }
+
+ p, err := b.doBuild(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ b.prevBuild = p
+
+ return p, nil
+}
+
+func (b *batcher) doBuild(ctx context.Context) (*Package, error) {
+ type importContext struct {
+ name string
+ resourceGetter resource.ResourceGetter
+ scriptOptions scriptOptions
+ dm identity.Manager
+ }
+
+ state := struct {
+ importResource *maps.Cache[string, resource.Resource]
+ resultResource *maps.Cache[string, resource.Resource]
+ importerImportContext *maps.Cache[string, importContext]
+ pathGroup *maps.Cache[string, string]
+ }{
+ importResource: maps.NewCache[string, resource.Resource](),
+ resultResource: maps.NewCache[string, resource.Resource](),
+ importerImportContext: maps.NewCache[string, importContext](),
+ pathGroup: maps.NewCache[string, string](),
+ }
+
+ multihostBasePaths := b.client.d.ResourceSpec.MultihostTargetBasePaths
+
+ // Entry points passed to ESBuid.
+ var entryPoints []string
+ addResource := func(group, pth string, r resource.Resource, isResult bool) {
+ state.pathGroup.Set(paths.TrimExt(pth), group)
+ state.importResource.Set(pth, r)
+ if isResult {
+ state.resultResource.Set(pth, r)
+ }
+ entryPoints = append(entryPoints, pth)
+ }
+
+ for _, g := range b.scriptGroups.Sorted() {
+ keyPath := g.id
+
+ t := &batchGroupTemplateContext{
+ keyPath: keyPath,
+ ID: g.id,
+ }
+
+ instances := g.instancesOptions.ByKey()
+
+ for _, vv := range g.scriptsOptions.ByKey() {
+ keyPath := keyPath + "_" + vv.Key().String()
+ opts := vv.Compiled()
+ impPath := path.Join(PrefixHugoVirtual, opts.Dir(), keyPath+opts.Resource.MediaType().FirstSuffix.FullSuffix)
+ impCtx := opts.ImportContext
+
+ state.importerImportContext.Set(impPath, importContext{
+ name: keyPath,
+ resourceGetter: impCtx,
+ scriptOptions: opts,
+ dm: g.dependencyManager,
+ })
+
+ bt := scriptBatchTemplateContext{
+ opts: vv,
+ Import: impPath,
+ Instances: []scriptInstanceBatchTemplateContext{},
+ }
+ state.importResource.Set(bt.Import, vv.Compiled().Resource)
+ predicate := func(k instanceID) bool {
+ return k.scriptID == vv.Key()
+ }
+ for _, vvv := range instances.Filter(predicate) {
+ bt.Instances = append(bt.Instances, scriptInstanceBatchTemplateContext{opts: vvv})
+ }
+
+ t.Scripts = append(t.Scripts, bt)
+ }
+
+ for _, vv := range g.runnersOptions.ByKey() {
+ runnerKeyPath := keyPath + "_" + vv.Key().String()
+ runnerImpPath := paths.AddLeadingSlash(runnerKeyPath + "_runner" + vv.Compiled().Resource.MediaType().FirstSuffix.FullSuffix)
+ t.Runners = append(t.Runners, scriptRunnerTemplateContext{opts: vv, Import: runnerImpPath})
+ addResource(g.id, runnerImpPath, vv.Compiled().Resource, false)
+ }
+
+ r, s, err := b.client.buildBatchGroup(ctx, t)
+ if err != nil {
+ return nil, fmt.Errorf("failed to build JS batch: %w", err)
+ }
+
+ state.importerImportContext.Set(s, importContext{
+ name: s,
+ resourceGetter: nil,
+ dm: g.dependencyManager,
+ })
+
+ addResource(g.id, s, r, true)
+ }
+
+ mediaTypes := b.client.d.ResourceSpec.MediaTypes()
+
+ externalOptions := b.configOptions.Compiled().Options
+ if externalOptions.Format == "" {
+ externalOptions.Format = "esm"
+ }
+ if externalOptions.Format != "esm" {
+ return nil, fmt.Errorf("only esm format is currently supported")
+ }
+
+ jsOpts := Options{
+ ExternalOptions: externalOptions,
+ InternalOptions: InternalOptions{
+ DependencyManager: b.dependencyManager,
+ Splitting: true,
+ ImportOnResolveFunc: func(imp string, args api.OnResolveArgs) string {
+ var importContextPath string
+ if args.Kind == api.ResolveEntryPoint {
+ importContextPath = args.Path
+ } else {
+ importContextPath = args.Importer
+ }
+ importContext, importContextFound := state.importerImportContext.Get(importContextPath)
+
+ // We want to track the dependencies closest to where they're used.
+ dm := b.dependencyManager
+ if importContextFound {
+ dm = importContext.dm
+ }
+
+ if r, found := state.importResource.Get(imp); found {
+ dm.AddIdentity(identity.FirstIdentity(r))
+ return imp
+ }
+
+ if importContext.resourceGetter != nil {
+ resolved := ResolveResource(imp, importContext.resourceGetter)
+ if resolved != nil {
+ resolvePath := resources.InternalResourceTargetPath(resolved)
+ dm.AddIdentity(identity.FirstIdentity(resolved))
+ imp := PrefixHugoVirtual + resolvePath
+ state.importResource.Set(imp, resolved)
+ state.importerImportContext.Set(imp, importContext)
+ return imp
+
+ }
+ }
+ return ""
+ },
+ ImportOnLoadFunc: func(args api.OnLoadArgs) string {
+ imp := args.Path
+
+ if r, found := state.importResource.Get(imp); found {
+ content, err := r.(resource.ContentProvider).Content(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return cast.ToString(content)
+ }
+ return ""
+ },
+ ImportParamsOnLoadFunc: func(args api.OnLoadArgs) json.RawMessage {
+ if importContext, found := state.importerImportContext.Get(args.Path); found {
+ if !importContext.scriptOptions.IsZero() {
+ return importContext.scriptOptions.Params
+ }
+ }
+ return nil
+ },
+ ErrorMessageResolveFunc: func(args api.Message) *ErrorMessageResolved {
+ if loc := args.Location; loc != nil {
+ path := strings.TrimPrefix(loc.File, NsHugoImportResolveFunc+":")
+ if r, found := state.importResource.Get(path); found {
+ sourcePath := resources.InternalResourceSourcePathBestEffort(r)
+
+ var contentr hugio.ReadSeekCloser
+ if cp, ok := r.(hugio.ReadSeekCloserProvider); ok {
+ contentr, _ = cp.ReadSeekCloser()
+ }
+ return &ErrorMessageResolved{
+ Content: contentr,
+ Path: sourcePath,
+ Message: args.Text,
+ }
+
+ }
+
+ }
+ return nil
+ },
+ ResolveSourceMapSource: func(s string) string {
+ if r, found := state.importResource.Get(s); found {
+ if ss := resources.InternalResourceSourcePath(r); ss != "" {
+ return ss
+ }
+ return PrefixHugoMemory + s
+ }
+ return ""
+ },
+ EntryPoints: entryPoints,
+ },
+ }
+
+ result, err := b.client.buildClient.Build(jsOpts)
+ if err != nil {
+ return nil, fmt.Errorf("failed to build JS bundle: %w", err)
+ }
+
+ groups := make(map[string]resource.Resources)
+
+ createAndAddResource := func(targetPath, group string, o api.OutputFile, mt media.Type) error {
+ var sourceFilename string
+ if r, found := state.importResource.Get(targetPath); found {
+ sourceFilename = resources.InternalResourceSourcePathBestEffort(r)
+ }
+ targetPath = path.Join(b.id, targetPath)
+
+ rd := resources.ResourceSourceDescriptor{
+ LazyPublish: true,
+ OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) {
+ return hugio.NewReadSeekerNoOpCloserFromBytes(o.Contents), nil
+ },
+ MediaType: mt,
+ TargetPath: targetPath,
+ SourceFilenameOrPath: sourceFilename,
+ }
+ r, err := b.client.d.ResourceSpec.NewResource(rd)
+ if err != nil {
+ return err
+ }
+
+ groups[group] = append(groups[group], r)
+
+ return nil
+ }
+
+ outDir := b.client.d.AbsPublishDir
+
+ createAndAddResources := func(o api.OutputFile) (bool, error) {
+ p := paths.ToSlashPreserveLeading(strings.TrimPrefix(o.Path, outDir))
+ ext := path.Ext(p)
+ mt, _, found := mediaTypes.GetBySuffix(ext)
+ if !found {
+ return false, nil
+ }
+
+ group, found := state.pathGroup.Get(paths.TrimExt(p))
+
+ if !found {
+ return false, nil
+ }
+
+ if err := createAndAddResource(p, group, o, mt); err != nil {
+ return false, err
+ }
+
+ return true, nil
+ }
+
+ for _, o := range result.OutputFiles {
+ handled, err := createAndAddResources(o)
+ if err != nil {
+ return nil, err
+ }
+
+ if !handled {
+ // Copy to destination.
+ // In a multihost setup, we will have multiple targets.
+ var targetFilenames []string
+ if len(multihostBasePaths) > 0 {
+ for _, base := range multihostBasePaths {
+ p := strings.TrimPrefix(o.Path, outDir)
+ targetFilename := filepath.Join(base, b.id, p)
+ targetFilenames = append(targetFilenames, targetFilename)
+ }
+ } else {
+ p := strings.TrimPrefix(o.Path, outDir)
+ targetFilename := filepath.Join(b.id, p)
+ targetFilenames = append(targetFilenames, targetFilename)
+ }
+
+ fs := b.client.d.BaseFs.PublishFs
+
+ if err := func() error {
+ fw, err := helpers.OpenFilesForWriting(fs, targetFilenames...)
+ if err != nil {
+ return err
+ }
+ defer fw.Close()
+
+ fr := bytes.NewReader(o.Contents)
+
+ _, err = io.Copy(fw, fr)
+
+ return err
+ }(); err != nil {
+ return nil, fmt.Errorf("failed to copy to %q: %w", targetFilenames, err)
+ }
+ }
+ }
+
+ p := &Package{
+ id: path.Join(NsBatch, b.id),
+ b: b,
+ groups: groups,
+ }
+
+ return p, nil
+}
+
+func (b *batcher) removeNotSet() bool {
+ // We already have the lock.
+ var removed bool
+ currentBuildID := b.buildCount
+ for k, v := range b.scriptGroups {
+ if !v.isBuilt(currentBuildID) && v.isTouched(currentBuildID) {
+ // Remove entire group.
+ removed = true
+ delete(b.scriptGroups, k)
+ continue
+ }
+ if v.removeTouchedButNotSet() {
+ removed = true
+ }
+ if v.removeNotSet() {
+ removed = true
+ }
+ }
+
+ return removed
+}
+
+func (b *batcher) reset() {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+ b.configOptions.Reset()
+ for _, v := range b.scriptGroups {
+ v.Reset()
+ }
+}
+
+type buildIDs map[uint32]bool
+
+func (b buildIDs) Has(buildID uint32) bool {
+ return b[buildID]
+}
+
+func (b buildIDs) Set(buildID uint32) {
+ b[buildID] = true
+}
+
+type buildToucher interface {
+ setTouched(buildID uint32)
+}
+
+type configOptions struct {
+ Options ExternalOptions
+}
+
+func (s configOptions) isStaleCompiled(prev configOptions) bool {
+ return false
+}
+
+func (s configOptions) compileOptions(m map[string]any, defaults defaultOptionValues) (configOptions, error) {
+ config, err := DecodeExternalOptions(m)
+ if err != nil {
+ return configOptions{}, err
+ }
+
+ return configOptions{
+ Options: config,
+ }, nil
+}
+
+type defaultOptionValues struct {
+ defaultExport string
+}
+
+type instanceID struct {
+ scriptID scriptID
+ instanceID string
+}
+
+func (i instanceID) String() string {
+ return i.scriptID.String() + "_" + i.instanceID
+}
+
+type isBuiltOrTouched struct {
+ built buildIDs
+ touched buildIDs
+}
+
+func (i isBuiltOrTouched) setBuilt(id uint32) {
+ i.built.Set(id)
+}
+
+func (i isBuiltOrTouched) isBuilt(id uint32) bool {
+ return i.built.Has(id)
+}
+
+func (i isBuiltOrTouched) setTouched(id uint32) {
+ i.touched.Set(id)
+}
+
+func (i isBuiltOrTouched) isTouched(id uint32) bool {
+ return i.touched.Has(id)
+}
+
+type isBuiltOrTouchedProvider interface {
+ isBuilt(uint32) bool
+ isTouched(uint32) bool
+}
+
+type key interface {
+ comparable
+ fmt.Stringer
+}
+
+type optionsCompiler[C any] interface {
+ isStaleCompiled(C) bool
+ compileOptions(map[string]any, defaultOptionValues) (C, error)
+}
+
+type optionsGetSetter[K, C any] interface {
+ isBuiltOrTouchedProvider
+ identity.IdentityProvider
+ // resource.StaleInfo
+
+ Compiled() C
+ Key() K
+ Reset()
+
+ Get(uint32) js.OptionsSetter
+ isStale() bool
+ currPrev() (map[string]any, map[string]any)
+}
+
+type optionsGetSetters[K key, C any] []optionsGetSetter[K, C]
+
+type optionsMap[K key, C any] map[K]optionsGetSetter[K, C]
+
+type opts[K any, C optionsCompiler[C]] struct {
+ key K
+ h *optsHolder[C]
+ once lazy.OnceMore
+}
+
+type optsHolder[C optionsCompiler[C]] struct {
+ optionsID string
+
+ defaults defaultOptionValues
+
+ // Keep track of one generation so we can detect changes.
+ // Note that most of this tracking is performed on the options/map level.
+ compiled C
+ compiledPrev C
+ compileErr error
+
+ resetCounter uint32
+ optsSetCounter uint32
+ optsCurr map[string]any
+ optsPrev map[string]any
+
+ isBuiltOrTouched
+}
+
+type paramsOptions struct {
+ Params json.RawMessage
+}
+
+func (s paramsOptions) isStaleCompiled(prev paramsOptions) bool {
+ return false
+}
+
+func (s paramsOptions) compileOptions(m map[string]any, defaults defaultOptionValues) (paramsOptions, error) {
+ v := struct {
+ Params map[string]any
+ }{}
+
+ if err := mapstructure.WeakDecode(m, &v); err != nil {
+ return paramsOptions{}, err
+ }
+
+ paramsJSON, err := json.Marshal(v.Params)
+ if err != nil {
+ return paramsOptions{}, err
+ }
+
+ return paramsOptions{
+ Params: paramsJSON,
+ }, nil
+}
+
+type scriptBatchTemplateContext struct {
+ opts optionsGetSetter[scriptID, scriptOptions]
+ Import string
+ Instances []scriptInstanceBatchTemplateContext
+}
+
+func (s *scriptBatchTemplateContext) Export() string {
+ return s.opts.Compiled().Export
+}
+
+func (c scriptBatchTemplateContext) MarshalJSON() (b []byte, err error) {
+ return json.Marshal(&struct {
+ ID string `json:"id"`
+ Instances []scriptInstanceBatchTemplateContext `json:"instances"`
+ }{
+ ID: c.opts.Key().String(),
+ Instances: c.Instances,
+ })
+}
+
+func (b scriptBatchTemplateContext) RunnerJSON(i int) string {
+ script := fmt.Sprintf("Script%d", i)
+
+ v := struct {
+ ID string `json:"id"`
+
+ // Read-only live JavaScript binding.
+ Binding string `json:"binding"`
+ Instances []scriptInstanceBatchTemplateContext `json:"instances"`
+ }{
+ b.opts.Key().String(),
+ script,
+ b.Instances,
+ }
+
+ bb, err := json.Marshal(v)
+ if err != nil {
+ panic(err)
+ }
+ s := string(bb)
+
+ // Remove the quotes to make it a valid JS object.
+ s = strings.ReplaceAll(s, fmt.Sprintf("%q", script), script)
+
+ return s
+}
+
+type scriptGroup struct {
+ mu sync.Mutex
+ id string
+ b *batcher
+ isBuiltOrTouched
+ dependencyManager identity.Manager
+
+ scriptsOptions optionsMap[scriptID, scriptOptions]
+ instancesOptions optionsMap[instanceID, paramsOptions]
+ runnersOptions optionsMap[scriptID, scriptOptions]
+}
+
+// For internal use only.
+func (b *scriptGroup) GetDependencyManager() identity.Manager {
+ return b.dependencyManager
+}
+
+// For internal use only.
+func (b *scriptGroup) IdentifierBase() string {
+ return b.id
+}
+
+func (s *scriptGroup) Instance(sid, id string) js.OptionsSetter {
+ if err := ValidateBatchID(sid, false); err != nil {
+ panic(err)
+ }
+ if err := ValidateBatchID(id, false); err != nil {
+ panic(err)
+ }
+
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ iid := instanceID{scriptID: scriptID(sid), instanceID: id}
+ if v, found := s.instancesOptions[iid]; found {
+ return v.Get(s.b.buildCount)
+ }
+
+ fullID := "instance_" + s.key() + "_" + iid.String()
+
+ s.instancesOptions[iid] = newOpts[instanceID, paramsOptions](
+ iid,
+ fullID,
+ defaultOptionValues{},
+ )
+
+ return s.instancesOptions[iid].Get(s.b.buildCount)
+}
+
+func (g *scriptGroup) Reset() {
+ for _, v := range g.scriptsOptions {
+ v.Reset()
+ }
+ for _, v := range g.instancesOptions {
+ v.Reset()
+ }
+ for _, v := range g.runnersOptions {
+ v.Reset()
+ }
+}
+
+func (s *scriptGroup) Runner(id string) js.OptionsSetter {
+ if err := ValidateBatchID(id, false); err != nil {
+ panic(err)
+ }
+
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ sid := scriptID(id)
+ if v, found := s.runnersOptions[sid]; found {
+ return v.Get(s.b.buildCount)
+ }
+
+ runnerIdentity := "runner_" + s.key() + "_" + id
+
+ // A typical signature for a runner would be:
+ // export default function Run(scripts) {}
+ // The user can override the default export in the templates.
+
+ s.runnersOptions[sid] = newOpts[scriptID, scriptOptions](
+ sid,
+ runnerIdentity,
+ defaultOptionValues{
+ defaultExport: "default",
+ },
+ )
+
+ return s.runnersOptions[sid].Get(s.b.buildCount)
+}
+
+func (s *scriptGroup) Script(id string) js.OptionsSetter {
+ if err := ValidateBatchID(id, false); err != nil {
+ panic(err)
+ }
+
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ sid := scriptID(id)
+ if v, found := s.scriptsOptions[sid]; found {
+ return v.Get(s.b.buildCount)
+ }
+
+ scriptIdentity := "script_" + s.key() + "_" + id
+
+ s.scriptsOptions[sid] = newOpts[scriptID, scriptOptions](
+ sid,
+ scriptIdentity,
+ defaultOptionValues{
+ defaultExport: "*",
+ },
+ )
+
+ return s.scriptsOptions[sid].Get(s.b.buildCount)
+}
+
+func (s *scriptGroup) isStale() bool {
+ for _, v := range s.scriptsOptions {
+ if v.isStale() {
+ return true
+ }
+ }
+
+ for _, v := range s.instancesOptions {
+ if v.isStale() {
+ return true
+ }
+ }
+
+ for _, v := range s.runnersOptions {
+ if v.isStale() {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (v *scriptGroup) forEachIdentity(
+ f func(id identity.Identity) bool,
+) bool {
+ if f(v) {
+ return true
+ }
+ for _, vv := range v.instancesOptions {
+ if f(vv.GetIdentity()) {
+ return true
+ }
+ }
+
+ for _, vv := range v.scriptsOptions {
+ if f(vv.GetIdentity()) {
+ return true
+ }
+ }
+
+ for _, vv := range v.runnersOptions {
+ if f(vv.GetIdentity()) {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (s *scriptGroup) key() string {
+ return s.b.id + "_" + s.id
+}
+
+func (g *scriptGroup) removeNotSet() bool {
+ currentBuildID := g.b.buildCount
+ if !g.isBuilt(currentBuildID) {
+ // This group was never accessed in this build.
+ return false
+ }
+ var removed bool
+
+ if g.instancesOptions.isBuilt(currentBuildID) {
+ // A new instance has been set in this group for this build.
+ // Remove any instance that has not been set in this build.
+ for k, v := range g.instancesOptions {
+ if v.isBuilt(currentBuildID) {
+ continue
+ }
+ delete(g.instancesOptions, k)
+ removed = true
+ }
+ }
+
+ if g.runnersOptions.isBuilt(currentBuildID) {
+ // A new runner has been set in this group for this build.
+ // Remove any runner that has not been set in this build.
+ for k, v := range g.runnersOptions {
+ if v.isBuilt(currentBuildID) {
+ continue
+ }
+ delete(g.runnersOptions, k)
+ removed = true
+ }
+ }
+
+ if g.scriptsOptions.isBuilt(currentBuildID) {
+ // A new script has been set in this group for this build.
+ // Remove any script that has not been set in this build.
+ for k, v := range g.scriptsOptions {
+ if v.isBuilt(currentBuildID) {
+ continue
+ }
+ delete(g.scriptsOptions, k)
+
+ // Also remove any instance with this ID.
+ for kk := range g.instancesOptions {
+ if kk.scriptID == k {
+ delete(g.instancesOptions, kk)
+ }
+ }
+ removed = true
+ }
+ }
+
+ return removed
+}
+
+func (g *scriptGroup) removeTouchedButNotSet() bool {
+ currentBuildID := g.b.buildCount
+ var removed bool
+ for k, v := range g.instancesOptions {
+ if v.isBuilt(currentBuildID) {
+ continue
+ }
+ if v.isTouched(currentBuildID) {
+ delete(g.instancesOptions, k)
+ removed = true
+ }
+ }
+ for k, v := range g.runnersOptions {
+ if v.isBuilt(currentBuildID) {
+ continue
+ }
+ if v.isTouched(currentBuildID) {
+ delete(g.runnersOptions, k)
+ removed = true
+ }
+ }
+ for k, v := range g.scriptsOptions {
+ if v.isBuilt(currentBuildID) {
+ continue
+ }
+ if v.isTouched(currentBuildID) {
+ delete(g.scriptsOptions, k)
+ removed = true
+
+ // Also remove any instance with this ID.
+ for kk := range g.instancesOptions {
+ if kk.scriptID == k {
+ delete(g.instancesOptions, kk)
+ }
+ }
+ }
+
+ }
+ return removed
+}
+
+type scriptGroups map[string]*scriptGroup
+
+func (s scriptGroups) Sorted() []*scriptGroup {
+ var a []*scriptGroup
+ for _, v := range s {
+ a = append(a, v)
+ }
+ sort.Slice(a, func(i, j int) bool {
+ return a[i].id < a[j].id
+ })
+ return a
+}
+
+type scriptID string
+
+func (s scriptID) String() string {
+ return string(s)
+}
+
+type scriptInstanceBatchTemplateContext struct {
+ opts optionsGetSetter[instanceID, paramsOptions]
+}
+
+func (c scriptInstanceBatchTemplateContext) ID() string {
+ return c.opts.Key().instanceID
+}
+
+func (c scriptInstanceBatchTemplateContext) MarshalJSON() (b []byte, err error) {
+ return json.Marshal(&struct {
+ ID string `json:"id"`
+ Params json.RawMessage `json:"params"`
+ }{
+ ID: c.opts.Key().instanceID,
+ Params: c.opts.Compiled().Params,
+ })
+}
+
+type scriptOptions struct {
+ // The script to build.
+ Resource resource.Resource
+
+ // The import context to use.
+ // Note that we will always fall back to the resource's own import context.
+ ImportContext resource.ResourceGetter
+
+ // The export name to use for this script's group's runners (if any).
+ // If not set, the default export will be used.
+ Export string
+
+ // Params marshaled to JSON.
+ Params json.RawMessage
+}
+
+func (o *scriptOptions) Dir() string {
+ return path.Dir(resources.InternalResourceTargetPath(o.Resource))
+}
+
+func (s scriptOptions) IsZero() bool {
+ return s.Resource == nil
+}
+
+func (s scriptOptions) isStaleCompiled(prev scriptOptions) bool {
+ if prev.IsZero() {
+ return false
+ }
+
+ // All but the ImportContext are checked at the options/map level.
+ i1nil, i2nil := prev.ImportContext == nil, s.ImportContext == nil
+ if i1nil && i2nil {
+ return false
+ }
+ if i1nil || i2nil {
+ return true
+ }
+ // On its own this check would have too many false positives, but combined with the other checks it should be fine.
+ // We cannot do equality checking here.
+ if !prev.ImportContext.(resource.IsProbablySameResourceGetter).IsProbablySameResourceGetter(s.ImportContext) {
+ return true
+ }
+
+ return false
+}
+
+func (s scriptOptions) compileOptions(m map[string]any, defaults defaultOptionValues) (scriptOptions, error) {
+ v := struct {
+ Resource resource.Resource
+ ImportContext any
+ Export string
+ Params map[string]any
+ }{}
+
+ if err := mapstructure.WeakDecode(m, &v); err != nil {
+ panic(err)
+ }
+
+ var paramsJSON []byte
+ if v.Params != nil {
+ var err error
+ paramsJSON, err = json.Marshal(v.Params)
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ if v.Export == "" {
+ v.Export = defaults.defaultExport
+ }
+
+ compiled := scriptOptions{
+ Resource: v.Resource,
+ Export: v.Export,
+ ImportContext: resource.NewCachedResourceGetter(v.ImportContext),
+ Params: paramsJSON,
+ }
+
+ if compiled.Resource == nil {
+ return scriptOptions{}, fmt.Errorf("resource not set")
+ }
+
+ return compiled, nil
+}
+
+type scriptRunnerTemplateContext struct {
+ opts optionsGetSetter[scriptID, scriptOptions]
+ Import string
+}
+
+func (s *scriptRunnerTemplateContext) Export() string {
+ return s.opts.Compiled().Export
+}
+
+func (c scriptRunnerTemplateContext) MarshalJSON() (b []byte, err error) {
+ return json.Marshal(&struct {
+ ID string `json:"id"`
+ }{
+ ID: c.opts.Key().String(),
+ })
+}
+
+func (o optionsMap[K, C]) isBuilt(id uint32) bool {
+ for _, v := range o {
+ if v.isBuilt(id) {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (o *opts[K, C]) isBuilt(id uint32) bool {
+ return o.h.isBuilt(id)
+}
+
+func (o *opts[K, C]) isStale() bool {
+ if o.h.isStaleOpts() {
+ return true
+ }
+ if o.h.compiled.isStaleCompiled(o.h.compiledPrev) {
+ return true
+ }
+ return false
+}
+
+func (o *optsHolder[C]) isStaleOpts() bool {
+ if o.optsSetCounter == 1 && o.resetCounter > 0 {
+ return false
+ }
+ isStale := func() bool {
+ if len(o.optsCurr) != len(o.optsPrev) {
+ return true
+ }
+ for k, v := range o.optsPrev {
+ vv, found := o.optsCurr[k]
+ if !found {
+ return true
+ }
+ if strings.EqualFold(k, propsKeyImportContext) {
+ // This is checked later.
+ } else if si, ok := vv.(resource.StaleInfo); ok {
+ if si.StaleVersion() > 0 {
+ return true
+ }
+ } else {
+ if !reflect.DeepEqual(v, vv) {
+ return true
+ }
+ }
+ }
+ return false
+ }()
+
+ return isStale
+}
+
+func (o *opts[K, C]) isTouched(id uint32) bool {
+ return o.h.isTouched(id)
+}
+
+func (o *optsHolder[C]) checkCompileErr() {
+ if o.compileErr != nil {
+ panic(o.compileErr)
+ }
+}
+
+func (o *opts[K, C]) currPrev() (map[string]any, map[string]any) {
+ return o.h.optsCurr, o.h.optsPrev
+}
+
+func init() {
+ // We don't want any dependencies/change tracking on the top level Package,
+ // we want finer grained control via Package.Group.
+ var p any = &Package{}
+ if _, ok := p.(identity.Identity); ok {
+ panic("esbuid.Package should not implement identity.Identity")
+ }
+ if _, ok := p.(identity.DependencyManagerProvider); ok {
+ panic("esbuid.Package should not implement identity.DependencyManagerProvider")
+ }
+}
diff --git a/internal/js/esbuild/batch_integration_test.go b/internal/js/esbuild/batch_integration_test.go
new file mode 100644
index 000000000..b4a2454ac
--- /dev/null
+++ b/internal/js/esbuild/batch_integration_test.go
@@ -0,0 +1,723 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package js provides functions for building JavaScript resources
+package esbuild_test
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+
+ "github.com/bep/logg"
+ "github.com/gohugoio/hugo/common/paths"
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/internal/js/esbuild"
+)
+
+// Used to test misc. error situations etc.
+const jsBatchFilesTemplate = `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "section"]
+disableLiveReload = true
+-- assets/js/styles.css --
+body {
+ background-color: red;
+}
+-- assets/js/main.js --
+import './styles.css';
+import * as params from '@params';
+import * as foo from 'mylib';
+console.log("Hello, Main!");
+console.log("params.p1", params.p1);
+export default function Main() {};
+-- assets/js/runner.js --
+console.log("Hello, Runner!");
+-- node_modules/mylib/index.js --
+console.log("Hello, My Lib!");
+-- layouts/shortcodes/hdx.html --
+{{ $path := .Get "r" }}
+{{ $r := or (.Page.Resources.Get $path) (resources.Get $path) }}
+{{ $batch := (js.Batch "mybatch") }}
+{{ $scriptID := $path | anchorize }}
+{{ $instanceID := .Ordinal | string }}
+{{ $group := .Page.RelPermalink | anchorize }}
+{{ $params := .Params | default dict }}
+{{ $export := .Get "export" | default "default" }}
+{{ with $batch.Group $group }}
+ {{ with .Runner "create-elements" }}
+ {{ .SetOptions (dict "resource" (resources.Get "js/runner.js")) }}
+ {{ end }}
+ {{ with .Script $scriptID }}
+ {{ .SetOptions (dict
+ "resource" $r
+ "export" $export
+ "importContext" (slice $.Page)
+ )
+ }}
+ {{ end }}
+ {{ with .Instance $scriptID $instanceID }}
+ {{ .SetOptions (dict "params" $params) }}
+ {{ end }}
+{{ end }}
+hdx-instance: {{ $scriptID }}: {{ $instanceID }}|
+-- layouts/_default/baseof.html --
+Base.
+{{ $batch := (js.Batch "mybatch") }}
+ {{ with $batch.Config }}
+ {{ .SetOptions (dict
+ "params" (dict "id" "config")
+ "sourceMap" ""
+ )
+ }}
+{{ end }}
+{{ with (templates.Defer (dict "key" "global")) }}
+Defer:
+{{ $batch := (js.Batch "mybatch") }}
+{{ range $k, $v := $batch.Build.Groups }}
+ {{ range $kk, $vv := . -}}
+ {{ $k }}: {{ .RelPermalink }}
+ {{ end }}
+{{ end -}}
+{{ end }}
+{{ block "main" . }}Main{{ end }}
+End.
+-- layouts/_default/single.html --
+{{ define "main" }}
+==> Single Template Content: {{ .Content }}$
+{{ $batch := (js.Batch "mybatch") }}
+{{ with $batch.Group "mygroup" }}
+ {{ with .Runner "run" }}
+ {{ .SetOptions (dict "resource" (resources.Get "js/runner.js")) }}
+ {{ end }}
+ {{ with .Script "main" }}
+ {{ .SetOptions (dict "resource" (resources.Get "js/main.js") "params" (dict "p1" "param-p1-main" )) }}
+ {{ end }}
+ {{ with .Instance "main" "i1" }}
+ {{ .SetOptions (dict "params" (dict "title" "Instance 1")) }}
+ {{ end }}
+{{ end }}
+{{ end }}
+-- layouts/index.html --
+{{ define "main" }}
+Home.
+{{ end }}
+-- content/p1/index.md --
+---
+title: "P1"
+---
+
+Some content.
+
+{{< hdx r="p1script.js" myparam="p1-param-1" >}}
+{{< hdx r="p1script.js" myparam="p1-param-2" >}}
+
+-- content/p1/p1script.js --
+console.log("P1 Script");
+
+
+`
+
+// Just to verify that the above file setup works.
+func TestBatchTemplateOKBuild(t *testing.T) {
+ b := hugolib.Test(t, jsBatchFilesTemplate, hugolib.TestOptWithOSFs())
+ b.AssertPublishDir("mybatch/mygroup.js", "mybatch/mygroup.css")
+}
+
+func TestBatchRemoveAllInGroup(t *testing.T) {
+ files := jsBatchFilesTemplate
+ b := hugolib.TestRunning(t, files, hugolib.TestOptWithOSFs())
+
+ b.AssertFileContent("public/p1/index.html", "p1: /mybatch/p1.js")
+
+ b.EditFiles("content/p1/index.md", `
+---
+title: "P1"
+---
+Empty.
+`)
+ b.Build()
+
+ b.AssertFileContent("public/p1/index.html", "! p1: /mybatch/p1.js")
+
+ // Add one script back.
+ b.EditFiles("content/p1/index.md", `
+---
+title: "P1"
+---
+
+{{< hdx r="p1script.js" myparam="p1-param-1-new" >}}
+`)
+ b.Build()
+
+ b.AssertFileContent("public/mybatch/p1.js",
+ "p1-param-1-new",
+ "p1script.js")
+}
+
+func TestBatchEditInstance(t *testing.T) {
+ files := jsBatchFilesTemplate
+ b := hugolib.TestRunning(t, files, hugolib.TestOptWithOSFs())
+ b.AssertFileContent("public/mybatch/mygroup.js", "Instance 1")
+ b.EditFileReplaceAll("layouts/_default/single.html", "Instance 1", "Instance 1 Edit").Build()
+ b.AssertFileContent("public/mybatch/mygroup.js", "Instance 1 Edit")
+}
+
+func TestBatchEditScriptParam(t *testing.T) {
+ files := jsBatchFilesTemplate
+ b := hugolib.TestRunning(t, files, hugolib.TestOptWithOSFs())
+ b.AssertFileContent("public/mybatch/mygroup.js", "param-p1-main")
+ b.EditFileReplaceAll("layouts/_default/single.html", "param-p1-main", "param-p1-main-edited").Build()
+ b.AssertFileContent("public/mybatch/mygroup.js", "param-p1-main-edited")
+}
+
+func TestBatchMultiHost(t *testing.T) {
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "section"]
+[languages]
+[languages.en]
+weight = 1
+baseURL = "https://example.com/en"
+[languages.fr]
+weight = 2
+baseURL = "https://example.com/fr"
+disableLiveReload = true
+-- assets/js/styles.css --
+body {
+ background-color: red;
+}
+-- assets/js/main.js --
+import * as foo from 'mylib';
+console.log("Hello, Main!");
+-- assets/js/runner.js --
+console.log("Hello, Runner!");
+-- node_modules/mylib/index.js --
+console.log("Hello, My Lib!");
+-- layouts/index.html --
+Home.
+{{ $batch := (js.Batch "mybatch") }}
+ {{ with $batch.Config }}
+ {{ .SetOptions (dict
+ "params" (dict "id" "config")
+ "sourceMap" ""
+ )
+ }}
+{{ end }}
+{{ with (templates.Defer (dict "key" "global")) }}
+Defer:
+{{ $batch := (js.Batch "mybatch") }}
+{{ range $k, $v := $batch.Build.Groups }}
+ {{ range $kk, $vv := . -}}
+ {{ $k }}: {{ .RelPermalink }}
+ {{ end }}
+{{ end -}}
+{{ end }}
+{{ $batch := (js.Batch "mybatch") }}
+{{ with $batch.Group "mygroup" }}
+ {{ with .Runner "run" }}
+ {{ .SetOptions (dict "resource" (resources.Get "js/runner.js")) }}
+ {{ end }}
+ {{ with .Script "main" }}
+ {{ .SetOptions (dict "resource" (resources.Get "js/main.js") "params" (dict "p1" "param-p1-main" )) }}
+ {{ end }}
+ {{ with .Instance "main" "i1" }}
+ {{ .SetOptions (dict "params" (dict "title" "Instance 1")) }}
+ {{ end }}
+{{ end }}
+
+
+`
+ b := hugolib.Test(t, files, hugolib.TestOptWithOSFs())
+ b.AssertPublishDir(
+ "en/mybatch/chunk-TOZKWCDE.js", "en/mybatch/mygroup.js ",
+ "fr/mybatch/mygroup.js", "fr/mybatch/chunk-TOZKWCDE.js")
+}
+
+func TestBatchRenameBundledScript(t *testing.T) {
+ files := jsBatchFilesTemplate
+ b := hugolib.TestRunning(t, files, hugolib.TestOptWithOSFs())
+ b.AssertFileContent("public/mybatch/p1.js", "P1 Script")
+ b.RenameFile("content/p1/p1script.js", "content/p1/p1script2.js")
+ _, err := b.BuildE()
+ b.Assert(err, qt.IsNotNil)
+ b.Assert(err.Error(), qt.Contains, "resource not set")
+
+ // Rename it back.
+ b.RenameFile("content/p1/p1script2.js", "content/p1/p1script.js")
+ b.Build()
+}
+
+func TestBatchErrorScriptResourceNotSet(t *testing.T) {
+ files := strings.Replace(jsBatchFilesTemplate, `(resources.Get "js/main.js")`, `(resources.Get "js/doesnotexist.js")`, 1)
+ b, err := hugolib.TestE(t, files, hugolib.TestOptWithOSFs())
+ b.Assert(err, qt.IsNotNil)
+ b.Assert(err.Error(), qt.Contains, `error calling SetOptions: resource not set`)
+}
+
+func TestBatchSlashInBatchID(t *testing.T) {
+ files := strings.ReplaceAll(jsBatchFilesTemplate, `"mybatch"`, `"my/batch"`)
+ b, err := hugolib.TestE(t, files, hugolib.TestOptWithOSFs())
+ b.Assert(err, qt.IsNil)
+ b.AssertPublishDir("my/batch/mygroup.js")
+}
+
+func TestBatchSourceMaps(t *testing.T) {
+ filesTemplate := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "section"]
+disableLiveReload = true
+-- assets/js/styles.css --
+body {
+ background-color: red;
+}
+-- assets/js/main.js --
+import * as foo from 'mylib';
+console.log("Hello, Main!");
+-- assets/js/runner.js --
+console.log("Hello, Runner!");
+-- node_modules/mylib/index.js --
+console.log("Hello, My Lib!");
+-- layouts/shortcodes/hdx.html --
+{{ $path := .Get "r" }}
+{{ $r := or (.Page.Resources.Get $path) (resources.Get $path) }}
+{{ $batch := (js.Batch "mybatch") }}
+{{ $scriptID := $path | anchorize }}
+{{ $instanceID := .Ordinal | string }}
+{{ $group := .Page.RelPermalink | anchorize }}
+{{ $params := .Params | default dict }}
+{{ $export := .Get "export" | default "default" }}
+{{ with $batch.Group $group }}
+ {{ with .Runner "create-elements" }}
+ {{ .SetOptions (dict "resource" (resources.Get "js/runner.js")) }}
+ {{ end }}
+ {{ with .Script $scriptID }}
+ {{ .SetOptions (dict
+ "resource" $r
+ "export" $export
+ "importContext" (slice $.Page)
+ )
+ }}
+ {{ end }}
+ {{ with .Instance $scriptID $instanceID }}
+ {{ .SetOptions (dict "params" $params) }}
+ {{ end }}
+{{ end }}
+hdx-instance: {{ $scriptID }}: {{ $instanceID }}|
+-- layouts/_default/baseof.html --
+Base.
+{{ $batch := (js.Batch "mybatch") }}
+ {{ with $batch.Config }}
+ {{ .SetOptions (dict
+ "params" (dict "id" "config")
+ "sourceMap" ""
+ )
+ }}
+{{ end }}
+{{ with (templates.Defer (dict "key" "global")) }}
+Defer:
+{{ $batch := (js.Batch "mybatch") }}
+{{ range $k, $v := $batch.Build.Groups }}
+ {{ range $kk, $vv := . -}}
+ {{ $k }}: {{ .RelPermalink }}
+ {{ end }}
+{{ end -}}
+{{ end }}
+{{ block "main" . }}Main{{ end }}
+End.
+-- layouts/_default/single.html --
+{{ define "main" }}
+==> Single Template Content: {{ .Content }}$
+{{ $batch := (js.Batch "mybatch") }}
+{{ with $batch.Group "mygroup" }}
+ {{ with .Runner "run" }}
+ {{ .SetOptions (dict "resource" (resources.Get "js/runner.js")) }}
+ {{ end }}
+ {{ with .Script "main" }}
+ {{ .SetOptions (dict "resource" (resources.Get "js/main.js") "params" (dict "p1" "param-p1-main" )) }}
+ {{ end }}
+ {{ with .Instance "main" "i1" }}
+ {{ .SetOptions (dict "params" (dict "title" "Instance 1")) }}
+ {{ end }}
+{{ end }}
+{{ end }}
+-- layouts/index.html --
+{{ define "main" }}
+Home.
+{{ end }}
+-- content/p1/index.md --
+---
+title: "P1"
+---
+
+Some content.
+
+{{< hdx r="p1script.js" myparam="p1-param-1" >}}
+{{< hdx r="p1script.js" myparam="p1-param-2" >}}
+
+-- content/p1/p1script.js --
+import * as foo from 'mylib';
+console.lg("Foo", foo);
+console.log("P1 Script");
+export default function P1Script() {};
+
+
+`
+ files := strings.Replace(filesTemplate, `"sourceMap" ""`, `"sourceMap" "linked"`, 1)
+ b := hugolib.TestRunning(t, files, hugolib.TestOptWithOSFs())
+ b.AssertFileContent("public/mybatch/mygroup.js.map", "main.js", "! ns-hugo")
+ b.AssertFileContent("public/mybatch/mygroup.js", "sourceMappingURL=mygroup.js.map")
+ b.AssertFileContent("public/mybatch/p1.js", "sourceMappingURL=p1.js.map")
+ b.AssertFileContent("public/mybatch/mygroup_run_runner.js", "sourceMappingURL=mygroup_run_runner.js.map")
+ b.AssertFileContent("public/mybatch/chunk-UQKPPNA6.js", "sourceMappingURL=chunk-UQKPPNA6.js.map")
+
+ checkMap := func(p string, expectLen int) {
+ s := b.FileContent(p)
+ sources := esbuild.SourcesFromSourceMap(s)
+ b.Assert(sources, qt.HasLen, expectLen)
+
+ // Check that all source files exist.
+ for _, src := range sources {
+ filename, ok := paths.UrlStringToFilename(src)
+ b.Assert(ok, qt.IsTrue)
+ _, err := os.Stat(filename)
+ b.Assert(err, qt.IsNil)
+ }
+ }
+
+ checkMap("public/mybatch/mygroup.js.map", 1)
+ checkMap("public/mybatch/p1.js.map", 1)
+ checkMap("public/mybatch/mygroup_run_runner.js.map", 0)
+ checkMap("public/mybatch/chunk-UQKPPNA6.js.map", 1)
+}
+
+func TestBatchErrorRunnerResourceNotSet(t *testing.T) {
+ files := strings.Replace(jsBatchFilesTemplate, `(resources.Get "js/runner.js")`, `(resources.Get "js/doesnotexist.js")`, 1)
+ b, err := hugolib.TestE(t, files, hugolib.TestOptWithOSFs())
+ b.Assert(err, qt.IsNotNil)
+ b.Assert(err.Error(), qt.Contains, `resource not set`)
+}
+
+func TestBatchErrorScriptResourceInAssetsSyntaxError(t *testing.T) {
+ // Introduce JS syntax error in assets/js/main.js
+ files := strings.Replace(jsBatchFilesTemplate, `console.log("Hello, Main!");`, `console.log("Hello, Main!"`, 1)
+ b, err := hugolib.TestE(t, files, hugolib.TestOptWithOSFs())
+ b.Assert(err, qt.IsNotNil)
+ b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`assets/js/main.js:5:0": Expected ")" but found "console"`))
+}
+
+func TestBatchErrorScriptResourceInBundleSyntaxError(t *testing.T) {
+ // Introduce JS syntax error in content/p1/p1script.js
+ files := strings.Replace(jsBatchFilesTemplate, `console.log("P1 Script");`, `console.log("P1 Script"`, 1)
+ b, err := hugolib.TestE(t, files, hugolib.TestOptWithOSFs())
+ b.Assert(err, qt.IsNotNil)
+ b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`/content/p1/p1script.js:3:0": Expected ")" but found end of file`))
+}
+
+func TestBatch(t *testing.T) {
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+disableLiveReload = true
+baseURL = "https://example.com"
+-- package.json --
+{
+ "devDependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ }
+}
+-- assets/js/shims/react.js --
+-- assets/js/shims/react-dom.js --
+module.exports = window.ReactDOM;
+module.exports = window.React;
+-- content/mybundle/index.md --
+---
+title: "My Bundle"
+---
+-- content/mybundle/mybundlestyles.css --
+@import './foo.css';
+@import './bar.css';
+@import './otherbundlestyles.css';
+
+.mybundlestyles {
+ background-color: blue;
+}
+-- content/mybundle/bundlereact.jsx --
+import * as React from "react";
+import './foo.css';
+import './mybundlestyles.css';
+window.React1 = React;
+
+let text = 'Click me, too!'
+
+export default function MyBundleButton() {
+ return (
+ ${text}
+ )
+}
+
+-- assets/js/reactrunner.js --
+import * as ReactDOM from 'react-dom/client';
+import * as React from 'react';
+
+export default function Run(group) {
+ for (const module of group.scripts) {
+ for (const instance of module.instances) {
+ /* This is a convention in this project. */
+ let elId = §§${module.id}-${instance.id}§§;
+ let el = document.getElementById(elId);
+ if (!el) {
+ console.warn(§§Element with id ${elId} not found§§);
+ continue;
+ }
+ const root = ReactDOM.createRoot(el);
+ const reactEl = React.createElement(module.mod, instance.params);
+ root.render(reactEl);
+ }
+ }
+}
+-- assets/other/otherbundlestyles.css --
+.otherbundlestyles {
+ background-color: red;
+}
+-- assets/other/foo.css --
+@import './bar.css';
+
+.foo {
+ background-color: blue;
+}
+-- assets/other/bar.css --
+.bar {
+ background-color: red;
+}
+-- assets/js/button.css --
+button {
+ background-color: red;
+}
+-- assets/js/bar.css --
+.bar-assets {
+ background-color: red;
+}
+-- assets/js/helper.js --
+import './bar.css'
+
+export function helper() {
+ console.log('helper');
+}
+
+-- assets/js/react1styles_nested.css --
+.react1styles_nested {
+ background-color: red;
+}
+-- assets/js/react1styles.css --
+@import './react1styles_nested.css';
+.react1styles {
+ background-color: red;
+}
+-- assets/js/react1.jsx --
+import * as React from "react";
+import './button.css'
+import './foo.css'
+import './react1styles.css'
+
+window.React1 = React;
+
+let text = 'Click me'
+
+export default function MyButton() {
+ return (
+ ${text}
+ )
+}
+
+-- assets/js/react2.jsx --
+import * as React from "react";
+import { helper } from './helper.js'
+import './foo.css'
+
+window.React2 = React;
+
+let text = 'Click me, too!'
+
+export function MyOtherButton() {
+ return (
+ ${text}
+ )
+}
+-- assets/js/main1.js --
+import * as React from "react";
+import * as params from '@params';
+
+console.log('main1.React', React)
+console.log('main1.params.id', params.id)
+
+-- assets/js/main2.js --
+import * as React from "react";
+import * as params from '@params';
+
+console.log('main2.React', React)
+console.log('main2.params.id', params.id)
+
+export default function Main2() {};
+
+-- assets/js/main3.js --
+import * as React from "react";
+import * as params from '@params';
+import * as config from '@params/config';
+
+console.log('main3.params.id', params.id)
+console.log('config.params.id', config.id)
+
+export default function Main3() {};
+
+-- layouts/_default/single.html --
+Single.
+
+{{ $r := .Resources.GetMatch "*.jsx" }}
+{{ $batch := (js.Batch "mybundle") }}
+{{ $otherCSS := (resources.Match "/other/*.css").Mount "/other" "." }}
+ {{ with $batch.Config }}
+ {{ $shims := dict "react" "js/shims/react.js" "react-dom/client" "js/shims/react-dom.js" }}
+ {{ .SetOptions (dict
+ "target" "es2018"
+ "params" (dict "id" "config")
+ "shims" $shims
+ )
+ }}
+{{ end }}
+{{ with $batch.Group "reactbatch" }}
+ {{ with .Script "r3" }}
+ {{ .SetOptions (dict
+ "resource" $r
+ "importContext" (slice $ $otherCSS)
+ "params" (dict "id" "r3")
+ )
+ }}
+ {{ end }}
+ {{ with .Instance "r3" "r2i1" }}
+ {{ .SetOptions (dict "title" "r2 instance 1")}}
+ {{ end }}
+{{ end }}
+-- layouts/index.html --
+Home.
+{{ with (templates.Defer (dict "key" "global")) }}
+{{ $batch := (js.Batch "mybundle") }}
+{{ range $k, $v := $batch.Build.Groups }}
+ {{ range $kk, $vv := . }}
+ {{ $k }}: {{ $kk }}: {{ .RelPermalink }}
+ {{ end }}
+ {{ end }}
+{{ end }}
+{{ $myContentBundle := site.GetPage "mybundle" }}
+{{ $batch := (js.Batch "mybundle") }}
+{{ $otherCSS := (resources.Match "/other/*.css").Mount "/other" "." }}
+{{ with $batch.Group "mains" }}
+ {{ with .Script "main1" }}
+ {{ .SetOptions (dict
+ "resource" (resources.Get "js/main1.js")
+ "params" (dict "id" "main1")
+ )
+ }}
+ {{ end }}
+ {{ with .Script "main2" }}
+ {{ .SetOptions (dict
+ "resource" (resources.Get "js/main2.js")
+ "params" (dict "id" "main2")
+ )
+ }}
+ {{ end }}
+ {{ with .Script "main3" }}
+ {{ .SetOptions (dict
+ "resource" (resources.Get "js/main3.js")
+ )
+ }}
+ {{ end }}
+{{ with .Instance "main1" "m1i1" }}{{ .SetOptions (dict "params" (dict "title" "Main1 Instance 1"))}}{{ end }}
+{{ with .Instance "main1" "m1i2" }}{{ .SetOptions (dict "params" (dict "title" "Main1 Instance 2"))}}{{ end }}
+{{ end }}
+{{ with $batch.Group "reactbatch" }}
+ {{ with .Runner "reactrunner" }}
+ {{ .SetOptions ( dict "resource" (resources.Get "js/reactrunner.js") )}}
+ {{ end }}
+ {{ with .Script "r1" }}
+ {{ .SetOptions (dict
+ "resource" (resources.Get "js/react1.jsx")
+ "importContext" (slice $myContentBundle $otherCSS)
+ "params" (dict "id" "r1")
+ )
+ }}
+ {{ end }}
+ {{ with .Instance "r1" "i1" }}{{ .SetOptions (dict "params" (dict "title" "Instance 1"))}}{{ end }}
+ {{ with .Instance "r1" "i2" }}{{ .SetOptions (dict "params" (dict "title" "Instance 2"))}}{{ end }}
+ {{ with .Script "r2" }}
+ {{ .SetOptions (dict
+ "resource" (resources.Get "js/react2.jsx")
+ "export" "MyOtherButton"
+ "importContext" $otherCSS
+ "params" (dict "id" "r2")
+ )
+ }}
+ {{ end }}
+ {{ with .Instance "r2" "i1" }}{{ .SetOptions (dict "params" (dict "title" "Instance 2-1"))}}{{ end }}
+{{ end }}
+
+`
+
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ NeedsOsFS: true,
+ NeedsNpmInstall: true,
+ TxtarString: files,
+ Running: true,
+ LogLevel: logg.LevelWarn,
+ // PrintAndKeepTempDir: true,
+ }).Build()
+
+ b.AssertFileContent("public/index.html",
+ "mains: 0: /mybundle/mains.js",
+ "reactbatch: 2: /mybundle/reactbatch.css",
+ )
+
+ b.AssertFileContent("public/mybundle/reactbatch.css",
+ ".bar {",
+ )
+
+ // Verify params resolution.
+ b.AssertFileContent("public/mybundle/mains.js",
+ `
+var id = "main1";
+console.log("main1.params.id", id);
+var id2 = "main2";
+console.log("main2.params.id", id2);
+
+
+# Params from top level config.
+var id3 = "config";
+console.log("main3.params.id", void 0);
+console.log("config.params.id", id3);
+`)
+
+ b.EditFileReplaceAll("content/mybundle/mybundlestyles.css", ".mybundlestyles", ".mybundlestyles-edit").Build()
+ b.AssertFileContent("public/mybundle/reactbatch.css", ".mybundlestyles-edit {")
+
+ b.EditFileReplaceAll("assets/other/bar.css", ".bar {", ".bar-edit {").Build()
+ b.AssertFileContent("public/mybundle/reactbatch.css", ".bar-edit {")
+
+ b.EditFileReplaceAll("assets/other/bar.css", ".bar-edit {", ".bar-edit2 {").Build()
+ b.AssertFileContent("public/mybundle/reactbatch.css", ".bar-edit2 {")
+}
diff --git a/internal/js/esbuild/build.go b/internal/js/esbuild/build.go
new file mode 100644
index 000000000..33b91eafc
--- /dev/null
+++ b/internal/js/esbuild/build.go
@@ -0,0 +1,236 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package esbuild provides functions for building JavaScript resources.
+package esbuild
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/evanw/esbuild/pkg/api"
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/hugio"
+ "github.com/gohugoio/hugo/common/text"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/hugolib/filesystems"
+ "github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/resources"
+)
+
+// NewBuildClient creates a new BuildClient.
+func NewBuildClient(fs *filesystems.SourceFilesystem, rs *resources.Spec) *BuildClient {
+ return &BuildClient{
+ rs: rs,
+ sfs: fs,
+ }
+}
+
+// BuildClient is a client for building JavaScript resources using esbuild.
+type BuildClient struct {
+ rs *resources.Spec
+ sfs *filesystems.SourceFilesystem
+}
+
+// Build builds the given JavaScript resources using esbuild with the given options.
+func (c *BuildClient) Build(opts Options) (api.BuildResult, error) {
+ dependencyManager := opts.DependencyManager
+ if dependencyManager == nil {
+ dependencyManager = identity.NopManager
+ }
+
+ opts.OutDir = c.rs.AbsPublishDir
+ opts.ResolveDir = c.rs.Cfg.BaseConfig().WorkingDir // where node_modules gets resolved
+ opts.AbsWorkingDir = opts.ResolveDir
+ opts.TsConfig = c.rs.ResolveJSConfigFile("tsconfig.json")
+ assetsResolver := newFSResolver(c.rs.Assets.Fs)
+
+ if err := opts.validate(); err != nil {
+ return api.BuildResult{}, err
+ }
+
+ if err := opts.compile(); err != nil {
+ return api.BuildResult{}, err
+ }
+
+ var err error
+ opts.compiled.Plugins, err = createBuildPlugins(c.rs, assetsResolver, dependencyManager, opts)
+ if err != nil {
+ return api.BuildResult{}, err
+ }
+
+ if opts.Inject != nil {
+ // Resolve the absolute filenames.
+ for i, ext := range opts.Inject {
+ impPath := filepath.FromSlash(ext)
+ if filepath.IsAbs(impPath) {
+ return api.BuildResult{}, fmt.Errorf("inject: absolute paths not supported, must be relative to /assets")
+ }
+
+ m := assetsResolver.resolveComponent(impPath)
+
+ if m == nil {
+ return api.BuildResult{}, fmt.Errorf("inject: file %q not found", ext)
+ }
+
+ opts.Inject[i] = m.Filename
+
+ }
+
+ opts.compiled.Inject = opts.Inject
+
+ }
+
+ result := api.Build(opts.compiled)
+
+ if len(result.Errors) > 0 {
+ createErr := func(msg api.Message) error {
+ if msg.Location == nil {
+ return errors.New(msg.Text)
+ }
+ var (
+ contentr hugio.ReadSeekCloser
+ errorMessage string
+ loc = msg.Location
+ errorPath = loc.File
+ err error
+ )
+
+ var resolvedError *ErrorMessageResolved
+
+ if opts.ErrorMessageResolveFunc != nil {
+ resolvedError = opts.ErrorMessageResolveFunc(msg)
+ }
+
+ if resolvedError == nil {
+ if errorPath == stdinImporter {
+ errorPath = opts.StdinSourcePath
+ }
+
+ errorMessage = msg.Text
+
+ var namespace string
+ for _, ns := range hugoNamespaces {
+ if strings.HasPrefix(errorPath, ns) {
+ namespace = ns
+ break
+ }
+ }
+
+ if namespace != "" {
+ namespace += ":"
+ errorMessage = strings.ReplaceAll(errorMessage, namespace, "")
+ errorPath = strings.TrimPrefix(errorPath, namespace)
+ contentr, err = hugofs.Os.Open(errorPath)
+ } else {
+ var fi os.FileInfo
+ fi, err = c.sfs.Fs.Stat(errorPath)
+ if err == nil {
+ m := fi.(hugofs.FileMetaInfo).Meta()
+ errorPath = m.Filename
+ contentr, err = m.Open()
+ }
+ }
+ } else {
+ contentr = resolvedError.Content
+ errorPath = resolvedError.Path
+ errorMessage = resolvedError.Message
+ }
+
+ if contentr != nil {
+ defer contentr.Close()
+ }
+
+ if err == nil {
+ fe := herrors.
+ NewFileErrorFromName(errors.New(errorMessage), errorPath).
+ UpdatePosition(text.Position{Offset: -1, LineNumber: loc.Line, ColumnNumber: loc.Column}).
+ UpdateContent(contentr, nil)
+
+ return fe
+ }
+
+ return fmt.Errorf("%s", errorMessage)
+ }
+
+ var errors []error
+
+ for _, msg := range result.Errors {
+ errors = append(errors, createErr(msg))
+ }
+
+ // Return 1, log the rest.
+ for i, err := range errors {
+ if i > 0 {
+ c.rs.Logger.Errorf("js.Build failed: %s", err)
+ }
+ }
+
+ return result, errors[0]
+ }
+
+ inOutputPathToAbsFilename := opts.ResolveSourceMapSource
+ opts.ResolveSourceMapSource = func(s string) string {
+ if inOutputPathToAbsFilename != nil {
+ if filename := inOutputPathToAbsFilename(s); filename != "" {
+ return filename
+ }
+ }
+
+ if m := assetsResolver.resolveComponent(s); m != nil {
+ return m.Filename
+ }
+
+ return ""
+ }
+
+ for i, o := range result.OutputFiles {
+ if err := fixOutputFile(&o, func(s string) string {
+ if s == "" {
+ return opts.ResolveSourceMapSource(opts.StdinSourcePath)
+ }
+ var isNsHugo bool
+ if strings.HasPrefix(s, "ns-hugo") {
+ isNsHugo = true
+ idxColon := strings.Index(s, ":")
+ s = s[idxColon+1:]
+ }
+
+ if !strings.HasPrefix(s, PrefixHugoVirtual) {
+ if !filepath.IsAbs(s) {
+ s = filepath.Join(opts.OutDir, s)
+ }
+ }
+
+ if isNsHugo {
+ if ss := opts.ResolveSourceMapSource(s); ss != "" {
+ if strings.HasPrefix(ss, PrefixHugoMemory) {
+ // File not on disk, mark it for removal from the sources slice.
+ return ""
+ }
+ return ss
+ }
+ return ""
+ }
+ return s
+ }); err != nil {
+ return result, err
+ }
+ result.OutputFiles[i] = o
+ }
+
+ return result, nil
+}
diff --git a/internal/js/esbuild/helpers.go b/internal/js/esbuild/helpers.go
new file mode 100644
index 000000000..b4cb565b8
--- /dev/null
+++ b/internal/js/esbuild/helpers.go
@@ -0,0 +1,15 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package esbuild provides functions for building JavaScript resources.
+package esbuild
diff --git a/internal/js/esbuild/options.go b/internal/js/esbuild/options.go
new file mode 100644
index 000000000..21f9e31cd
--- /dev/null
+++ b/internal/js/esbuild/options.go
@@ -0,0 +1,411 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package esbuild
+
+import (
+ "encoding/json"
+ "fmt"
+ "path/filepath"
+ "strings"
+
+ "github.com/gohugoio/hugo/common/hugio"
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/paths"
+ "github.com/gohugoio/hugo/identity"
+
+ "github.com/evanw/esbuild/pkg/api"
+
+ "github.com/gohugoio/hugo/media"
+ "github.com/mitchellh/mapstructure"
+)
+
+var (
+ nameTarget = map[string]api.Target{
+ "": api.ESNext,
+ "esnext": api.ESNext,
+ "es5": api.ES5,
+ "es6": api.ES2015,
+ "es2015": api.ES2015,
+ "es2016": api.ES2016,
+ "es2017": api.ES2017,
+ "es2018": api.ES2018,
+ "es2019": api.ES2019,
+ "es2020": api.ES2020,
+ "es2021": api.ES2021,
+ "es2022": api.ES2022,
+ "es2023": api.ES2023,
+ "es2024": api.ES2024,
+ }
+
+ // source names: https://github.com/evanw/esbuild/blob/9eca46464ed5615cb36a3beb3f7a7b9a8ffbe7cf/internal/config/config.go#L208
+ nameLoader = map[string]api.Loader{
+ "none": api.LoaderNone,
+ "base64": api.LoaderBase64,
+ "binary": api.LoaderBinary,
+ "copy": api.LoaderFile,
+ "css": api.LoaderCSS,
+ "dataurl": api.LoaderDataURL,
+ "default": api.LoaderDefault,
+ "empty": api.LoaderEmpty,
+ "file": api.LoaderFile,
+ "global-css": api.LoaderGlobalCSS,
+ "js": api.LoaderJS,
+ "json": api.LoaderJSON,
+ "jsx": api.LoaderJSX,
+ "local-css": api.LoaderLocalCSS,
+ "text": api.LoaderText,
+ "ts": api.LoaderTS,
+ "tsx": api.LoaderTSX,
+ }
+)
+
+// DecodeExternalOptions decodes the given map into ExternalOptions.
+func DecodeExternalOptions(m map[string]any) (ExternalOptions, error) {
+ opts := ExternalOptions{
+ SourcesContent: true,
+ }
+
+ if err := mapstructure.WeakDecode(m, &opts); err != nil {
+ return opts, err
+ }
+
+ if opts.TargetPath != "" {
+ opts.TargetPath = paths.ToSlashTrimLeading(opts.TargetPath)
+ }
+
+ opts.Target = strings.ToLower(opts.Target)
+ opts.Format = strings.ToLower(opts.Format)
+
+ return opts, nil
+}
+
+// ErrorMessageResolved holds a resolved error message.
+type ErrorMessageResolved struct {
+ Path string
+ Message string
+ Content hugio.ReadSeekCloser
+}
+
+// ExternalOptions holds user facing options for the js.Build template function.
+type ExternalOptions struct {
+ // If not set, the source path will be used as the base target path.
+ // Note that the target path's extension may change if the target MIME type
+ // is different, e.g. when the source is TypeScript.
+ TargetPath string
+
+ // Whether to minify to output.
+ Minify bool
+
+ // One of "inline", "external", "linked" or "none".
+ SourceMap string
+
+ SourcesContent bool
+
+ // The language target.
+ // One of: es2015, es2016, es2017, es2018, es2019, es2020 or esnext.
+ // Default is esnext.
+ Target string
+
+ // The output format.
+ // One of: iife, cjs, esm
+ // Default is to esm.
+ Format string
+
+ // One of browser, node, neutral.
+ // Default is browser.
+ // See https://esbuild.github.io/api/#platform
+ Platform string
+
+ // External dependencies, e.g. "react".
+ Externals []string
+
+ // This option allows you to automatically replace a global variable with an import from another file.
+ // The filenames must be relative to /assets.
+ // See https://esbuild.github.io/api/#inject
+ Inject []string
+
+ // User defined symbols.
+ Defines map[string]any
+
+ // This tells esbuild to edit your source code before building to drop certain constructs.
+ // See https://esbuild.github.io/api/#drop
+ Drop string
+
+ // Maps a component import to another.
+ Shims map[string]string
+
+ // Configuring a loader for a given file type lets you load that file type with an
+ // import statement or a require call. For example, configuring the .png file extension
+ // to use the data URL loader means importing a .png file gives you a data URL
+ // containing the contents of that image
+ //
+ // See https://esbuild.github.io/api/#loader
+ Loaders map[string]string
+
+ // User defined params. Will be marshaled to JSON and available as "@params", e.g.
+ // import * as params from '@params';
+ Params any
+
+ // What to use instead of React.createElement.
+ JSXFactory string
+
+ // What to use instead of React.Fragment.
+ JSXFragment string
+
+ // What to do about JSX syntax.
+ // See https://esbuild.github.io/api/#jsx
+ JSX string
+
+ // Which library to use to automatically import JSX helper functions from. Only works if JSX is set to automatic.
+ // See https://esbuild.github.io/api/#jsx-import-source
+ JSXImportSource string
+
+ // There is/was a bug in WebKit with severe performance issue with the tracking
+ // of TDZ checks in JavaScriptCore.
+ //
+ // Enabling this flag removes the TDZ and `const` assignment checks and
+ // may improve performance of larger JS codebases until the WebKit fix
+ // is in widespread use.
+ //
+ // See https://bugs.webkit.org/show_bug.cgi?id=199866
+ // Deprecated: This no longer have any effect and will be removed.
+ // TODO(bep) remove. See https://github.com/evanw/esbuild/commit/869e8117b499ca1dbfc5b3021938a53ffe934dba
+ AvoidTDZ bool
+}
+
+// InternalOptions holds internal options for the js.Build template function.
+type InternalOptions struct {
+ MediaType media.Type
+ OutDir string
+ Contents string
+ SourceDir string
+ ResolveDir string
+ AbsWorkingDir string
+ Metafile bool
+
+ StdinSourcePath string
+
+ DependencyManager identity.Manager
+
+ Stdin bool // Set to true to pass in the entry point as a byte slice.
+ Splitting bool
+ TsConfig string
+ EntryPoints []string
+ ImportOnResolveFunc func(string, api.OnResolveArgs) string
+ ImportOnLoadFunc func(api.OnLoadArgs) string
+ ImportParamsOnLoadFunc func(args api.OnLoadArgs) json.RawMessage
+ ErrorMessageResolveFunc func(api.Message) *ErrorMessageResolved
+ ResolveSourceMapSource func(string) string // Used to resolve paths in error source maps.
+}
+
+// Options holds the options passed to Build.
+type Options struct {
+ ExternalOptions
+ InternalOptions
+
+ compiled api.BuildOptions
+}
+
+func (opts *Options) compile() (err error) {
+ target, found := nameTarget[opts.Target]
+ if !found {
+ err = fmt.Errorf("invalid target: %q", opts.Target)
+ return
+ }
+
+ var loaders map[string]api.Loader
+ if opts.Loaders != nil {
+ loaders = make(map[string]api.Loader)
+ for k, v := range opts.Loaders {
+ loader, found := nameLoader[v]
+ if !found {
+ err = fmt.Errorf("invalid loader: %q", v)
+ return
+ }
+ loaders[k] = loader
+ }
+ }
+
+ mediaType := opts.MediaType
+ if mediaType.IsZero() {
+ mediaType = media.Builtin.JavascriptType
+ }
+
+ var loader api.Loader
+ switch mediaType.SubType {
+ case media.Builtin.JavascriptType.SubType:
+ loader = api.LoaderJS
+ case media.Builtin.TypeScriptType.SubType:
+ loader = api.LoaderTS
+ case media.Builtin.TSXType.SubType:
+ loader = api.LoaderTSX
+ case media.Builtin.JSXType.SubType:
+ loader = api.LoaderJSX
+ default:
+ err = fmt.Errorf("unsupported Media Type: %q", opts.MediaType)
+ return
+ }
+
+ var format api.Format
+ // One of: iife, cjs, esm
+ switch opts.Format {
+ case "", "iife":
+ format = api.FormatIIFE
+ case "esm":
+ format = api.FormatESModule
+ case "cjs":
+ format = api.FormatCommonJS
+ default:
+ err = fmt.Errorf("unsupported script output format: %q", opts.Format)
+ return
+ }
+
+ var jsx api.JSX
+ switch opts.JSX {
+ case "", "transform":
+ jsx = api.JSXTransform
+ case "preserve":
+ jsx = api.JSXPreserve
+ case "automatic":
+ jsx = api.JSXAutomatic
+ default:
+ err = fmt.Errorf("unsupported jsx type: %q", opts.JSX)
+ return
+ }
+
+ var platform api.Platform
+ switch opts.Platform {
+ case "", "browser":
+ platform = api.PlatformBrowser
+ case "node":
+ platform = api.PlatformNode
+ case "neutral":
+ platform = api.PlatformNeutral
+ default:
+ err = fmt.Errorf("unsupported platform type: %q", opts.Platform)
+ return
+ }
+
+ var defines map[string]string
+ if opts.Defines != nil {
+ defines = maps.ToStringMapString(opts.Defines)
+ }
+
+ var drop api.Drop
+ switch opts.Drop {
+ case "":
+ case "console":
+ drop = api.DropConsole
+ case "debugger":
+ drop = api.DropDebugger
+ default:
+ err = fmt.Errorf("unsupported drop type: %q", opts.Drop)
+ }
+
+ // By default we only need to specify outDir and no outFile
+ outDir := opts.OutDir
+ outFile := ""
+ var sourceMap api.SourceMap
+ switch opts.SourceMap {
+ case "inline":
+ sourceMap = api.SourceMapInline
+ case "external":
+ sourceMap = api.SourceMapExternal
+ case "linked":
+ sourceMap = api.SourceMapLinked
+ case "", "none":
+ sourceMap = api.SourceMapNone
+ default:
+ err = fmt.Errorf("unsupported sourcemap type: %q", opts.SourceMap)
+ return
+ }
+
+ sourcesContent := api.SourcesContentInclude
+ if !opts.SourcesContent {
+ sourcesContent = api.SourcesContentExclude
+ }
+
+ opts.compiled = api.BuildOptions{
+ Outfile: outFile,
+ Bundle: true,
+ Metafile: opts.Metafile,
+ AbsWorkingDir: opts.AbsWorkingDir,
+
+ Target: target,
+ Format: format,
+ Platform: platform,
+ Sourcemap: sourceMap,
+ SourcesContent: sourcesContent,
+
+ Loader: loaders,
+
+ MinifyWhitespace: opts.Minify,
+ MinifyIdentifiers: opts.Minify,
+ MinifySyntax: opts.Minify,
+
+ Outdir: outDir,
+ Splitting: opts.Splitting,
+
+ Define: defines,
+ External: opts.Externals,
+ Drop: drop,
+
+ JSXFactory: opts.JSXFactory,
+ JSXFragment: opts.JSXFragment,
+
+ JSX: jsx,
+ JSXImportSource: opts.JSXImportSource,
+
+ Tsconfig: opts.TsConfig,
+
+ EntryPoints: opts.EntryPoints,
+ }
+
+ if opts.Stdin {
+ // This makes ESBuild pass `stdin` as the Importer to the import.
+ opts.compiled.Stdin = &api.StdinOptions{
+ Contents: opts.Contents,
+ ResolveDir: opts.ResolveDir,
+ Loader: loader,
+ }
+ }
+ return
+}
+
+func (o Options) loaderFromFilename(filename string) api.Loader {
+ ext := filepath.Ext(filename)
+ if optsLoaders := o.compiled.Loader; optsLoaders != nil {
+ if l, found := optsLoaders[ext]; found {
+ return l
+ }
+ }
+ l, found := extensionToLoaderMap[ext]
+ if found {
+ return l
+ }
+ return api.LoaderJS
+}
+
+func (opts *Options) validate() error {
+ if opts.ImportOnResolveFunc != nil && opts.ImportOnLoadFunc == nil {
+ return fmt.Errorf("ImportOnLoadFunc must be set if ImportOnResolveFunc is set")
+ }
+ if opts.ImportOnResolveFunc == nil && opts.ImportOnLoadFunc != nil {
+ return fmt.Errorf("ImportOnResolveFunc must be set if ImportOnLoadFunc is set")
+ }
+ if opts.AbsWorkingDir == "" {
+ return fmt.Errorf("AbsWorkingDir must be set")
+ }
+ return nil
+}
diff --git a/internal/js/esbuild/options_test.go b/internal/js/esbuild/options_test.go
new file mode 100644
index 000000000..e92c3bea6
--- /dev/null
+++ b/internal/js/esbuild/options_test.go
@@ -0,0 +1,262 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package esbuild
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/media"
+
+ "github.com/evanw/esbuild/pkg/api"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestToBuildOptions(t *testing.T) {
+ c := qt.New(t)
+
+ opts := Options{
+ InternalOptions: InternalOptions{
+ MediaType: media.Builtin.JavascriptType,
+ Stdin: true,
+ },
+ }
+
+ c.Assert(opts.compile(), qt.IsNil)
+ c.Assert(opts.compiled, qt.DeepEquals, api.BuildOptions{
+ Bundle: true,
+ Target: api.ESNext,
+ Format: api.FormatIIFE,
+ Platform: api.PlatformBrowser,
+ SourcesContent: 1,
+ Stdin: &api.StdinOptions{
+ Loader: api.LoaderJS,
+ },
+ })
+
+ opts = Options{
+ ExternalOptions: ExternalOptions{
+ Target: "es2018",
+ Format: "cjs",
+ Minify: true,
+ AvoidTDZ: true,
+ },
+ InternalOptions: InternalOptions{
+ MediaType: media.Builtin.JavascriptType,
+ Stdin: true,
+ },
+ }
+
+ c.Assert(opts.compile(), qt.IsNil)
+ c.Assert(opts.compiled, qt.DeepEquals, api.BuildOptions{
+ Bundle: true,
+ Target: api.ES2018,
+ Format: api.FormatCommonJS,
+ Platform: api.PlatformBrowser,
+ SourcesContent: 1,
+ MinifyIdentifiers: true,
+ MinifySyntax: true,
+ MinifyWhitespace: true,
+ Stdin: &api.StdinOptions{
+ Loader: api.LoaderJS,
+ },
+ })
+
+ opts = Options{
+ ExternalOptions: ExternalOptions{
+ Target: "es2018", Format: "cjs", Minify: true,
+ SourceMap: "inline",
+ },
+ InternalOptions: InternalOptions{
+ MediaType: media.Builtin.JavascriptType,
+ Stdin: true,
+ },
+ }
+
+ c.Assert(opts.compile(), qt.IsNil)
+ c.Assert(opts.compiled, qt.DeepEquals, api.BuildOptions{
+ Bundle: true,
+ Target: api.ES2018,
+ Format: api.FormatCommonJS,
+ Platform: api.PlatformBrowser,
+ MinifyIdentifiers: true,
+ MinifySyntax: true,
+ MinifyWhitespace: true,
+ SourcesContent: 1,
+ Sourcemap: api.SourceMapInline,
+ Stdin: &api.StdinOptions{
+ Loader: api.LoaderJS,
+ },
+ })
+
+ opts = Options{
+ ExternalOptions: ExternalOptions{
+ Target: "es2018", Format: "cjs", Minify: true,
+ SourceMap: "inline",
+ },
+ InternalOptions: InternalOptions{
+ MediaType: media.Builtin.JavascriptType,
+ Stdin: true,
+ },
+ }
+
+ c.Assert(opts.compile(), qt.IsNil)
+ c.Assert(opts.compiled, qt.DeepEquals, api.BuildOptions{
+ Bundle: true,
+ Target: api.ES2018,
+ Format: api.FormatCommonJS,
+ Platform: api.PlatformBrowser,
+ MinifyIdentifiers: true,
+ MinifySyntax: true,
+ MinifyWhitespace: true,
+ Sourcemap: api.SourceMapInline,
+ SourcesContent: 1,
+ Stdin: &api.StdinOptions{
+ Loader: api.LoaderJS,
+ },
+ })
+
+ opts = Options{
+ ExternalOptions: ExternalOptions{
+ Target: "es2018", Format: "cjs", Minify: true,
+ SourceMap: "external",
+ },
+ InternalOptions: InternalOptions{
+ MediaType: media.Builtin.JavascriptType,
+ Stdin: true,
+ },
+ }
+
+ c.Assert(opts.compile(), qt.IsNil)
+ c.Assert(opts.compiled, qt.DeepEquals, api.BuildOptions{
+ Bundle: true,
+ Target: api.ES2018,
+ Format: api.FormatCommonJS,
+ Platform: api.PlatformBrowser,
+ MinifyIdentifiers: true,
+ MinifySyntax: true,
+ MinifyWhitespace: true,
+ Sourcemap: api.SourceMapExternal,
+ SourcesContent: 1,
+ Stdin: &api.StdinOptions{
+ Loader: api.LoaderJS,
+ },
+ })
+
+ opts = Options{
+ ExternalOptions: ExternalOptions{
+ JSX: "automatic", JSXImportSource: "preact",
+ },
+ InternalOptions: InternalOptions{
+ MediaType: media.Builtin.JavascriptType,
+ Stdin: true,
+ },
+ }
+
+ c.Assert(opts.compile(), qt.IsNil)
+ c.Assert(opts.compiled, qt.DeepEquals, api.BuildOptions{
+ Bundle: true,
+ Target: api.ESNext,
+ Format: api.FormatIIFE,
+ Platform: api.PlatformBrowser,
+ SourcesContent: 1,
+ Stdin: &api.StdinOptions{
+ Loader: api.LoaderJS,
+ },
+ JSX: api.JSXAutomatic,
+ JSXImportSource: "preact",
+ })
+
+ opts = Options{
+ ExternalOptions: ExternalOptions{
+ Drop: "console",
+ },
+ }
+ c.Assert(opts.compile(), qt.IsNil)
+ c.Assert(opts.compiled.Drop, qt.Equals, api.DropConsole)
+ opts = Options{
+ ExternalOptions: ExternalOptions{
+ Drop: "debugger",
+ },
+ }
+ c.Assert(opts.compile(), qt.IsNil)
+ c.Assert(opts.compiled.Drop, qt.Equals, api.DropDebugger)
+
+ opts = Options{
+ ExternalOptions: ExternalOptions{
+ Drop: "adsfadsf",
+ },
+ }
+ c.Assert(opts.compile(), qt.ErrorMatches, `unsupported drop type: "adsfadsf"`)
+}
+
+func TestToBuildOptionsTarget(t *testing.T) {
+ c := qt.New(t)
+
+ for _, test := range []struct {
+ target string
+ expect api.Target
+ }{
+ {"es2015", api.ES2015},
+ {"es2016", api.ES2016},
+ {"es2017", api.ES2017},
+ {"es2018", api.ES2018},
+ {"es2019", api.ES2019},
+ {"es2020", api.ES2020},
+ {"es2021", api.ES2021},
+ {"es2022", api.ES2022},
+ {"es2023", api.ES2023},
+ {"", api.ESNext},
+ {"esnext", api.ESNext},
+ } {
+ c.Run(test.target, func(c *qt.C) {
+ opts := Options{
+ ExternalOptions: ExternalOptions{
+ Target: test.target,
+ },
+ InternalOptions: InternalOptions{
+ MediaType: media.Builtin.JavascriptType,
+ },
+ }
+
+ c.Assert(opts.compile(), qt.IsNil)
+ c.Assert(opts.compiled.Target, qt.Equals, test.expect)
+ })
+ }
+}
+
+func TestDecodeExternalOptions(t *testing.T) {
+ c := qt.New(t)
+ m := map[string]any{
+ "platform": "node",
+ }
+ ext, err := DecodeExternalOptions(m)
+ c.Assert(err, qt.IsNil)
+ c.Assert(ext, qt.DeepEquals, ExternalOptions{
+ SourcesContent: true,
+ Platform: "node",
+ })
+
+ opts := Options{
+ ExternalOptions: ext,
+ }
+ c.Assert(opts.compile(), qt.IsNil)
+ c.Assert(opts.compiled, qt.DeepEquals, api.BuildOptions{
+ Bundle: true,
+ Target: api.ESNext,
+ Format: api.FormatIIFE,
+ Platform: api.PlatformNode,
+ SourcesContent: api.SourcesContentInclude,
+ })
+}
diff --git a/internal/js/esbuild/resolve.go b/internal/js/esbuild/resolve.go
new file mode 100644
index 000000000..a2516dbd2
--- /dev/null
+++ b/internal/js/esbuild/resolve.go
@@ -0,0 +1,323 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package esbuild
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/evanw/esbuild/pkg/api"
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/spf13/afero"
+ "slices"
+)
+
+const (
+ NsHugoImport = "ns-hugo-imp"
+ NsHugoImportResolveFunc = "ns-hugo-imp-func"
+ nsHugoParams = "ns-hugo-params"
+ pathHugoConfigParams = "@params/config"
+
+ stdinImporter = ""
+)
+
+var hugoNamespaces = []string{NsHugoImport, NsHugoImportResolveFunc, nsHugoParams}
+
+const (
+ PrefixHugoVirtual = "__hu_v"
+ PrefixHugoMemory = "__hu_m"
+)
+
+var extensionToLoaderMap = map[string]api.Loader{
+ ".js": api.LoaderJS,
+ ".mjs": api.LoaderJS,
+ ".cjs": api.LoaderJS,
+ ".jsx": api.LoaderJSX,
+ ".ts": api.LoaderTS,
+ ".tsx": api.LoaderTSX,
+ ".css": api.LoaderCSS,
+ ".json": api.LoaderJSON,
+ ".txt": api.LoaderText,
+}
+
+// This is a common sub-set of ESBuild's default extensions.
+// We assume that imports of JSON, CSS etc. will be using their full
+// name with extension.
+var commonExtensions = []string{".js", ".ts", ".tsx", ".jsx"}
+
+// ResolveComponent resolves a component using the given resolver.
+func ResolveComponent[T any](impPath string, resolve func(string) (v T, found, isDir bool)) (v T, found bool) {
+ findFirst := func(base string) (v T, found, isDir bool) {
+ for _, ext := range commonExtensions {
+ if strings.HasSuffix(impPath, ext) {
+ // Import of foo.js.js need the full name.
+ continue
+ }
+ if v, found, isDir = resolve(base + ext); found {
+ return
+ }
+ }
+
+ // Not found.
+ return
+ }
+
+ // We need to check if this is a regular file imported without an extension.
+ // There may be ambiguous situations where both foo.js and foo/index.js exists.
+ // This import order is in line with both how Node and ESBuild's native
+ // import resolver works.
+
+ // It may be a regular file imported without an extension, e.g.
+ // foo or foo/index.
+ v, found, _ = findFirst(impPath)
+ if found {
+ return v, found
+ }
+
+ base := filepath.Base(impPath)
+ if base == "index" {
+ // try index.esm.js etc.
+ v, found, _ = findFirst(impPath + ".esm")
+ if found {
+ return v, found
+ }
+ }
+
+ // Check the path as is.
+ var isDir bool
+ v, found, isDir = resolve(impPath)
+ if found && isDir {
+ v, found, _ = findFirst(filepath.Join(impPath, "index"))
+ if !found {
+ v, found, _ = findFirst(filepath.Join(impPath, "index.esm"))
+ }
+ }
+
+ if !found && strings.HasSuffix(base, ".js") {
+ v, found, _ = findFirst(strings.TrimSuffix(impPath, ".js"))
+ }
+
+ return
+}
+
+// ResolveResource resolves a resource using the given resourceGetter.
+func ResolveResource(impPath string, resourceGetter resource.ResourceGetter) (r resource.Resource) {
+ resolve := func(name string) (v resource.Resource, found, isDir bool) {
+ r := resourceGetter.Get(name)
+ return r, r != nil, false
+ }
+ r, found := ResolveComponent(impPath, resolve)
+ if !found {
+ return nil
+ }
+ return r
+}
+
+func newFSResolver(fs afero.Fs) *fsResolver {
+ return &fsResolver{fs: fs, resolved: maps.NewCache[string, *hugofs.FileMeta]()}
+}
+
+type fsResolver struct {
+ fs afero.Fs
+ resolved *maps.Cache[string, *hugofs.FileMeta]
+}
+
+func (r *fsResolver) resolveComponent(impPath string) *hugofs.FileMeta {
+ v, _ := r.resolved.GetOrCreate(impPath, func() (*hugofs.FileMeta, error) {
+ resolve := func(name string) (*hugofs.FileMeta, bool, bool) {
+ if fi, err := r.fs.Stat(name); err == nil {
+ return fi.(hugofs.FileMetaInfo).Meta(), true, fi.IsDir()
+ }
+ return nil, false, false
+ }
+ v, _ := ResolveComponent(impPath, resolve)
+ return v, nil
+ })
+ return v
+}
+
+func createBuildPlugins(rs *resources.Spec, assetsResolver *fsResolver, depsManager identity.Manager, opts Options) ([]api.Plugin, error) {
+ fs := rs.Assets
+
+ resolveImport := func(args api.OnResolveArgs) (api.OnResolveResult, error) {
+ impPath := args.Path
+ shimmed := false
+ if opts.Shims != nil {
+ override, found := opts.Shims[impPath]
+ if found {
+ impPath = override
+ shimmed = true
+ }
+ }
+
+ if slices.Contains(opts.Externals, impPath) {
+ return api.OnResolveResult{
+ Path: impPath,
+ External: true,
+ }, nil
+ }
+
+ if opts.ImportOnResolveFunc != nil {
+ if s := opts.ImportOnResolveFunc(impPath, args); s != "" {
+ return api.OnResolveResult{Path: s, Namespace: NsHugoImportResolveFunc}, nil
+ }
+ }
+
+ importer := args.Importer
+
+ isStdin := importer == stdinImporter
+ var relDir string
+ if !isStdin {
+ if strings.HasPrefix(importer, PrefixHugoVirtual) {
+ relDir = filepath.Dir(strings.TrimPrefix(importer, PrefixHugoVirtual))
+ } else {
+ rel, found := fs.MakePathRelative(importer, true)
+
+ if !found {
+ if shimmed {
+ relDir = opts.SourceDir
+ } else {
+ // Not in any of the /assets folders.
+ // This is an import from a node_modules, let
+ // ESBuild resolve this.
+ return api.OnResolveResult{}, nil
+ }
+ } else {
+ relDir = filepath.Dir(rel)
+ }
+ }
+ } else {
+ relDir = opts.SourceDir
+ }
+
+ // Imports not starting with a "." is assumed to live relative to /assets.
+ // Hugo makes no assumptions about the directory structure below /assets.
+ if relDir != "" && strings.HasPrefix(impPath, ".") {
+ impPath = filepath.Join(relDir, impPath)
+ }
+
+ m := assetsResolver.resolveComponent(impPath)
+
+ if m != nil {
+ depsManager.AddIdentity(m.PathInfo)
+
+ // Store the source root so we can create a jsconfig.json
+ // to help IntelliSense when the build is done.
+ // This should be a small number of elements, and when
+ // in server mode, we may get stale entries on renames etc.,
+ // but that shouldn't matter too much.
+ rs.JSConfigBuilder.AddSourceRoot(m.SourceRoot)
+ return api.OnResolveResult{Path: m.Filename, Namespace: NsHugoImport}, nil
+ }
+
+ // Fall back to ESBuild's resolve.
+ return api.OnResolveResult{}, nil
+ }
+
+ importResolver := api.Plugin{
+ Name: "hugo-import-resolver",
+ Setup: func(build api.PluginBuild) {
+ build.OnResolve(api.OnResolveOptions{Filter: `.*`},
+ func(args api.OnResolveArgs) (api.OnResolveResult, error) {
+ return resolveImport(args)
+ })
+ build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: NsHugoImport},
+ func(args api.OnLoadArgs) (api.OnLoadResult, error) {
+ b, err := os.ReadFile(args.Path)
+ if err != nil {
+ return api.OnLoadResult{}, fmt.Errorf("failed to read %q: %w", args.Path, err)
+ }
+ c := string(b)
+
+ return api.OnLoadResult{
+ // See https://github.com/evanw/esbuild/issues/502
+ // This allows all modules to resolve dependencies
+ // in the main project's node_modules.
+ ResolveDir: opts.ResolveDir,
+ Contents: &c,
+ Loader: opts.loaderFromFilename(args.Path),
+ }, nil
+ })
+ build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: NsHugoImportResolveFunc},
+ func(args api.OnLoadArgs) (api.OnLoadResult, error) {
+ c := opts.ImportOnLoadFunc(args)
+ if c == "" {
+ return api.OnLoadResult{}, fmt.Errorf("ImportOnLoadFunc failed to resolve %q", args.Path)
+ }
+
+ return api.OnLoadResult{
+ ResolveDir: opts.ResolveDir,
+ Contents: &c,
+ Loader: opts.loaderFromFilename(args.Path),
+ }, nil
+ })
+ },
+ }
+
+ params := opts.Params
+ if params == nil {
+ // This way @params will always resolve to something.
+ params = make(map[string]any)
+ }
+
+ b, err := json.Marshal(params)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal params: %w", err)
+ }
+
+ paramsPlugin := api.Plugin{
+ Name: "hugo-params-plugin",
+ Setup: func(build api.PluginBuild) {
+ build.OnResolve(api.OnResolveOptions{Filter: `^@params(/config)?$`},
+ func(args api.OnResolveArgs) (api.OnResolveResult, error) {
+ resolvedPath := args.Importer
+
+ if args.Path == pathHugoConfigParams {
+ resolvedPath = pathHugoConfigParams
+ }
+
+ return api.OnResolveResult{
+ Path: resolvedPath,
+ Namespace: nsHugoParams,
+ }, nil
+ })
+ build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: nsHugoParams},
+ func(args api.OnLoadArgs) (api.OnLoadResult, error) {
+ bb := b
+ if args.Path != pathHugoConfigParams && opts.ImportParamsOnLoadFunc != nil {
+ bb = opts.ImportParamsOnLoadFunc(args)
+ }
+ s := string(bb)
+
+ if s == "" {
+ s = "{}"
+ }
+
+ return api.OnLoadResult{
+ Contents: &s,
+ Loader: api.LoaderJSON,
+ }, nil
+ })
+ },
+ }
+
+ return []api.Plugin{importResolver, paramsPlugin}, nil
+}
diff --git a/internal/js/esbuild/resolve_test.go b/internal/js/esbuild/resolve_test.go
new file mode 100644
index 000000000..86e3138f2
--- /dev/null
+++ b/internal/js/esbuild/resolve_test.go
@@ -0,0 +1,86 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package esbuild
+
+import (
+ "path"
+ "path/filepath"
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/config/testconfig"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/hugolib/filesystems"
+ "github.com/gohugoio/hugo/hugolib/paths"
+ "github.com/spf13/afero"
+)
+
+func TestResolveComponentInAssets(t *testing.T) {
+ c := qt.New(t)
+
+ for _, test := range []struct {
+ name string
+ files []string
+ impPath string
+ expect string
+ }{
+ {"Basic, extension", []string{"foo.js", "bar.js"}, "foo.js", "foo.js"},
+ {"Basic, no extension", []string{"foo.js", "bar.js"}, "foo", "foo.js"},
+ {"Basic, no extension, typescript", []string{"foo.ts", "bar.js"}, "foo", "foo.ts"},
+ {"Not found", []string{"foo.js", "bar.js"}, "moo.js", ""},
+ {"Not found, double js extension", []string{"foo.js.js", "bar.js"}, "foo.js", ""},
+ {"Index file, folder only", []string{"foo/index.js", "bar.js"}, "foo", "foo/index.js"},
+ {"Index file, folder and index", []string{"foo/index.js", "bar.js"}, "foo/index", "foo/index.js"},
+ {"Index file, folder and index and suffix", []string{"foo/index.js", "bar.js"}, "foo/index.js", "foo/index.js"},
+ {"Index ESM file, folder only", []string{"foo/index.esm.js", "bar.js"}, "foo", "foo/index.esm.js"},
+ {"Index ESM file, folder and index", []string{"foo/index.esm.js", "bar.js"}, "foo/index", "foo/index.esm.js"},
+ {"Index ESM file, folder and index and suffix", []string{"foo/index.esm.js", "bar.js"}, "foo/index.esm.js", "foo/index.esm.js"},
+ // We added these index.esm.js cases in v0.101.0. The case below is unlikely to happen in the wild, but add a test
+ // to document Hugo's behavior. We pick the file with the name index.js; anything else would be breaking.
+ {"Index and Index ESM file, folder only", []string{"foo/index.esm.js", "foo/index.js", "bar.js"}, "foo", "foo/index.js"},
+
+ // Issue #8949
+ {"Check file before directory", []string{"foo.js", "foo/index.js"}, "foo", "foo.js"},
+ } {
+ c.Run(test.name, func(c *qt.C) {
+ baseDir := "assets"
+ mfs := afero.NewMemMapFs()
+
+ for _, filename := range test.files {
+ c.Assert(afero.WriteFile(mfs, filepath.Join(baseDir, filename), []byte("let foo='bar';"), 0o777), qt.IsNil)
+ }
+
+ conf := testconfig.GetTestConfig(mfs, config.New())
+ fs := hugofs.NewFrom(mfs, conf.BaseConfig())
+
+ p, err := paths.New(fs, conf)
+ c.Assert(err, qt.IsNil)
+ bfs, err := filesystems.NewBase(p, nil)
+ c.Assert(err, qt.IsNil)
+ resolver := newFSResolver(bfs.Assets.Fs)
+
+ got := resolver.resolveComponent(test.impPath)
+
+ gotPath := ""
+ expect := test.expect
+ if got != nil {
+ gotPath = filepath.ToSlash(got.Filename)
+ expect = path.Join(baseDir, test.expect)
+ }
+
+ c.Assert(gotPath, qt.Equals, expect)
+ })
+ }
+}
diff --git a/internal/js/esbuild/sourcemap.go b/internal/js/esbuild/sourcemap.go
new file mode 100644
index 000000000..647f0c081
--- /dev/null
+++ b/internal/js/esbuild/sourcemap.go
@@ -0,0 +1,80 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package esbuild
+
+import (
+ "encoding/json"
+ "strings"
+
+ "github.com/evanw/esbuild/pkg/api"
+ "github.com/gohugoio/hugo/common/paths"
+)
+
+type sourceMap struct {
+ Version int `json:"version"`
+ Sources []string `json:"sources"`
+ SourcesContent []string `json:"sourcesContent"`
+ Mappings string `json:"mappings"`
+ Names []string `json:"names"`
+}
+
+func fixOutputFile(o *api.OutputFile, resolve func(string) string) error {
+ if strings.HasSuffix(o.Path, ".map") {
+ b, err := fixSourceMap(o.Contents, resolve)
+ if err != nil {
+ return err
+ }
+ o.Contents = b
+ }
+ return nil
+}
+
+func fixSourceMap(s []byte, resolve func(string) string) ([]byte, error) {
+ var sm sourceMap
+ if err := json.Unmarshal([]byte(s), &sm); err != nil {
+ return nil, err
+ }
+
+ sm.Sources = fixSourceMapSources(sm.Sources, resolve)
+
+ b, err := json.Marshal(sm)
+ if err != nil {
+ return nil, err
+ }
+
+ return b, nil
+}
+
+func fixSourceMapSources(s []string, resolve func(string) string) []string {
+ var result []string
+ for _, src := range s {
+ if s := resolve(src); s != "" {
+ // Absolute filenames works fine on U*ix (tested in Chrome on MacOs), but works very poorly on Windows (again Chrome).
+ // So, convert it to a URL.
+ if u, err := paths.UrlFromFilename(s); err == nil {
+ result = append(result, u.String())
+ }
+ }
+ }
+ return result
+}
+
+// Used in tests.
+func SourcesFromSourceMap(s string) []string {
+ var sm sourceMap
+ if err := json.Unmarshal([]byte(s), &sm); err != nil {
+ return nil
+ }
+ return sm.Sources
+}
diff --git a/internal/warpc/build.sh b/internal/warpc/build.sh
new file mode 100755
index 000000000..5e75aa381
--- /dev/null
+++ b/internal/warpc/build.sh
@@ -0,0 +1,5 @@
+# TODO1 clean up when done.
+go generate ./gen
+javy compile js/greet.bundle.js -d -o wasm/greet.wasm
+javy compile js/renderkatex.bundle.js -d -o wasm/renderkatex.wasm
+touch warpc_test.go
\ No newline at end of file
diff --git a/internal/warpc/gen/main.go b/internal/warpc/gen/main.go
new file mode 100644
index 000000000..d3d6562a9
--- /dev/null
+++ b/internal/warpc/gen/main.go
@@ -0,0 +1,68 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:generate go run main.go
+package main
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/evanw/esbuild/pkg/api"
+)
+
+var scripts = []string{
+ "greet.js",
+ "renderkatex.js",
+}
+
+func main() {
+ for _, script := range scripts {
+ filename := filepath.Join("../js", script)
+ err := buildJSBundle(filename)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+}
+
+func buildJSBundle(filename string) error {
+ minify := true
+ result := api.Build(
+ api.BuildOptions{
+ EntryPoints: []string{filename},
+ Bundle: true,
+ MinifyWhitespace: minify,
+ MinifyIdentifiers: minify,
+ MinifySyntax: minify,
+ Target: api.ES2020,
+ Outfile: strings.Replace(filename, ".js", ".bundle.js", 1),
+ SourceRoot: "../js",
+ })
+
+ if len(result.Errors) > 0 {
+ return fmt.Errorf("build failed: %v", result.Errors)
+ }
+ if len(result.OutputFiles) != 1 {
+ return fmt.Errorf("expected 1 output file, got %d", len(result.OutputFiles))
+ }
+
+ of := result.OutputFiles[0]
+ if err := os.WriteFile(filepath.FromSlash(of.Path), of.Contents, 0o644); err != nil {
+ return fmt.Errorf("write file failed: %v", err)
+ }
+ return nil
+}
diff --git a/internal/warpc/js/.gitignore b/internal/warpc/js/.gitignore
new file mode 100644
index 000000000..ccb2c800f
--- /dev/null
+++ b/internal/warpc/js/.gitignore
@@ -0,0 +1,2 @@
+node_modules/
+package-lock.json
\ No newline at end of file
diff --git a/internal/warpc/js/common.js b/internal/warpc/js/common.js
new file mode 100644
index 000000000..61c535fb7
--- /dev/null
+++ b/internal/warpc/js/common.js
@@ -0,0 +1,83 @@
+// Read JSONL from stdin.
+export function readInput(handle) {
+ const buffSize = 1024;
+ let currentLine = [];
+ const buffer = new Uint8Array(buffSize);
+
+ // These are not implemented by QuickJS.
+ console.warn = (value) => {
+ console.log(value);
+ };
+
+ console.error = (value) => {
+ throw new Error(value);
+ };
+
+ // Read all the available bytes
+ while (true) {
+ // Stdin file descriptor
+ const fd = 0;
+ let bytesRead = 0;
+ try {
+ bytesRead = Javy.IO.readSync(fd, buffer);
+ } catch (e) {
+ // IO.readSync fails with os error 29 when stdin closes.
+ if (e.message.includes('os error 29')) {
+ break;
+ }
+ throw new Error('Error reading from stdin');
+ }
+
+ if (bytesRead < 0) {
+ throw new Error('Error reading from stdin');
+ break;
+ }
+
+ if (bytesRead === 0) {
+ break;
+ }
+
+ currentLine = [...currentLine, ...buffer.subarray(0, bytesRead)];
+
+ // Check for newline. If not, we need to read more data.
+ if (!currentLine.includes(10)) {
+ continue;
+ }
+
+ // Split array into chunks by newline.
+ let i = 0;
+ for (let j = 0; i < currentLine.length; i++) {
+ if (currentLine[i] === 10) {
+ const chunk = currentLine.splice(j, i + 1);
+ const arr = new Uint8Array(chunk);
+ let message;
+ try {
+ message = JSON.parse(new TextDecoder().decode(arr));
+ } catch (e) {
+ throw new Error(`Error parsing JSON '${new TextDecoder().decode(arr)}' from stdin: ${e.message}`);
+ }
+
+ try {
+ handle(message);
+ } catch (e) {
+ let header = message.header;
+ header.err = e.message;
+ writeOutput({ header: header });
+ }
+
+ j = i + 1;
+ }
+ }
+ // Remove processed data.
+ currentLine = currentLine.slice(i);
+ }
+}
+
+// Write JSONL to stdout
+export function writeOutput(output) {
+ const encodedOutput = new TextEncoder().encode(JSON.stringify(output) + '\n');
+ const buffer = new Uint8Array(encodedOutput);
+ // Stdout file descriptor
+ const fd = 1;
+ Javy.IO.writeSync(fd, buffer);
+}
diff --git a/internal/warpc/js/greet.bundle.js b/internal/warpc/js/greet.bundle.js
new file mode 100644
index 000000000..6828d582a
--- /dev/null
+++ b/internal/warpc/js/greet.bundle.js
@@ -0,0 +1,2 @@
+(()=>{function w(r){let e=[],c=new Uint8Array(1024);for(console.warn=n=>{console.log(n)},console.error=n=>{throw new Error(n)};;){let o=0;try{o=Javy.IO.readSync(0,c)}catch(a){if(a.message.includes("os error 29"))break;throw new Error("Error reading from stdin")}if(o<0)throw new Error("Error reading from stdin");if(o===0)break;if(e=[...e,...c.subarray(0,o)],!e.includes(10))continue;let t=0;for(let a=0;t{function Wt(r){let t=[],a=new Uint8Array(1024);for(console.warn=n=>{console.log(n)},console.error=n=>{throw new Error(n)};;){let s=0;try{s=Javy.IO.readSync(0,a)}catch(h){if(h.message.includes("os error 29"))break;throw new Error("Error reading from stdin")}if(s<0)throw new Error("Error reading from stdin");if(s===0)break;if(t=[...t,...a.subarray(0,s)],!t.includes(10))continue;let l=0;for(let h=0;l15?f="\u2026"+h.slice(n-15,n):f=h.slice(0,n);var v;s+15":">","<":"<",'"':""","'":"'"},Ba=/[&><"']/g;function Da(r){return String(r).replace(Ba,e=>qa[e])}var zr=function r(e){return e.type==="ordgroup"||e.type==="color"?e.body.length===1?r(e.body[0]):e:e.type==="font"?r(e.body):e},Ca=function(e){var t=zr(e);return t.type==="mathord"||t.type==="textord"||t.type==="atom"},_a=function(e){if(!e)throw new Error("Expected non-null, but got "+String(e));return e},Na=function(e){var t=/^[\x00-\x20]*([^\\/#?]*?)(:|*58|*3a|&colon)/i.exec(e);return t?t[2]!==":"||!/^[a-zA-Z][a-zA-Z0-9+\-.]*$/.test(t[1])?null:t[1].toLowerCase():"_relative"},O={contains:Ma,deflt:za,escape:Da,hyphenate:Ta,getBaseElem:zr,isCharacterBox:Ca,protocolFromUrl:Na},Oe={displayMode:{type:"boolean",description:"Render math in display mode, which puts the math in display style (so \\int and \\sum are large, for example), and centers the math on the page on its own line.",cli:"-d, --display-mode"},output:{type:{enum:["htmlAndMathml","html","mathml"]},description:"Determines the markup language of the output.",cli:"-F, --format "},leqno:{type:"boolean",description:"Render display math in leqno style (left-justified tags)."},fleqn:{type:"boolean",description:"Render display math flush left."},throwOnError:{type:"boolean",default:!0,cli:"-t, --no-throw-on-error",cliDescription:"Render errors (in the color given by --error-color) instead of throwing a ParseError exception when encountering an error."},errorColor:{type:"string",default:"#cc0000",cli:"-c, --error-color ",cliDescription:"A color string given in the format 'rgb' or 'rrggbb' (no #). This option determines the color of errors rendered by the -t option.",cliProcessor:r=>"#"+r},macros:{type:"object",cli:"-m, --macro ",cliDescription:"Define custom macro of the form '\\foo:expansion' (use multiple -m arguments for multiple macros).",cliDefault:[],cliProcessor:(r,e)=>(e.push(r),e)},minRuleThickness:{type:"number",description:"Specifies a minimum thickness, in ems, for fraction lines, `\\sqrt` top lines, `{array}` vertical lines, `\\hline`, `\\hdashline`, `\\underline`, `\\overline`, and the borders of `\\fbox`, `\\boxed`, and `\\fcolorbox`.",processor:r=>Math.max(0,r),cli:"--min-rule-thickness ",cliProcessor:parseFloat},colorIsTextColor:{type:"boolean",description:"Makes \\color behave like LaTeX's 2-argument \\textcolor, instead of LaTeX's one-argument \\color mode change.",cli:"-b, --color-is-text-color"},strict:{type:[{enum:["warn","ignore","error"]},"boolean","function"],description:"Turn on strict / LaTeX faithfulness mode, which throws an error if the input uses features that are not supported by LaTeX.",cli:"-S, --strict",cliDefault:!1},trust:{type:["boolean","function"],description:"Trust the input, enabling all HTML features such as \\url.",cli:"-T, --trust"},maxSize:{type:"number",default:1/0,description:"If non-zero, all user-specified sizes, e.g. in \\rule{500em}{500em}, will be capped to maxSize ems. Otherwise, elements and spaces can be arbitrarily large",processor:r=>Math.max(0,r),cli:"-s, --max-size ",cliProcessor:parseInt},maxExpand:{type:"number",default:1e3,description:"Limit the number of macro expansions to the specified number, to prevent e.g. infinite macro loops. If set to Infinity, the macro expander will try to fully expand as in LaTeX.",processor:r=>Math.max(0,r),cli:"-e, --max-expand ",cliProcessor:r=>r==="Infinity"?1/0:parseInt(r)},globalGroup:{type:"boolean",cli:!1}};function Oa(r){if(r.default)return r.default;var e=r.type,t=Array.isArray(e)?e[0]:e;if(typeof t!="string")return t.enum[0];switch(t){case"boolean":return!1;case"string":return"";case"number":return 0;case"object":return{}}}var de=class{constructor(e){this.displayMode=void 0,this.output=void 0,this.leqno=void 0,this.fleqn=void 0,this.throwOnError=void 0,this.errorColor=void 0,this.macros=void 0,this.minRuleThickness=void 0,this.colorIsTextColor=void 0,this.strict=void 0,this.trust=void 0,this.maxSize=void 0,this.maxExpand=void 0,this.globalGroup=void 0,e=e||{};for(var t in Oe)if(Oe.hasOwnProperty(t)){var a=Oe[t];this[t]=e[t]!==void 0?a.processor?a.processor(e[t]):e[t]:Oa(a)}}reportNonstrict(e,t,a){var n=this.strict;if(typeof n=="function"&&(n=n(e,t,a)),!(!n||n==="ignore")){if(n===!0||n==="error")throw new z("LaTeX-incompatible input and strict mode is set to 'error': "+(t+" ["+e+"]"),a);n==="warn"?typeof console<"u"&&console.warn("LaTeX-incompatible input and strict mode is set to 'warn': "+(t+" ["+e+"]")):typeof console<"u"&&console.warn("LaTeX-incompatible input and strict mode is set to "+("unrecognized '"+n+"': "+t+" ["+e+"]"))}}useStrictBehavior(e,t,a){var n=this.strict;if(typeof n=="function")try{n=n(e,t,a)}catch{n="error"}return!n||n==="ignore"?!1:n===!0||n==="error"?!0:n==="warn"?(typeof console<"u"&&console.warn("LaTeX-incompatible input and strict mode is set to 'warn': "+(t+" ["+e+"]")),!1):(typeof console<"u"&&console.warn("LaTeX-incompatible input and strict mode is set to "+("unrecognized '"+n+"': "+t+" ["+e+"]")),!1)}isTrusted(e){if(e.url&&!e.protocol){var t=O.protocolFromUrl(e.url);if(t==null)return!1;e.protocol=t}var a=typeof this.trust=="function"?this.trust(e):this.trust;return!!a}},k0=class{constructor(e,t,a){this.id=void 0,this.size=void 0,this.cramped=void 0,this.id=e,this.size=t,this.cramped=a}sup(){return M0[Ia[this.id]]}sub(){return M0[Ea[this.id]]}fracNum(){return M0[Ra[this.id]]}fracDen(){return M0[$a[this.id]]}cramp(){return M0[La[this.id]]}text(){return M0[Fa[this.id]]}isTight(){return this.size>=2}},At=0,Ee=1,ae=2,N0=3,pe=4,v0=5,ne=6,o0=7,M0=[new k0(At,0,!1),new k0(Ee,0,!0),new k0(ae,1,!1),new k0(N0,1,!0),new k0(pe,2,!1),new k0(v0,2,!0),new k0(ne,3,!1),new k0(o0,3,!0)],Ia=[pe,v0,pe,v0,ne,o0,ne,o0],Ea=[v0,v0,v0,v0,o0,o0,o0,o0],Ra=[ae,N0,pe,v0,ne,o0,ne,o0],$a=[N0,N0,v0,v0,o0,o0,o0,o0],La=[Ee,Ee,N0,N0,v0,v0,o0,o0],Fa=[At,Ee,ae,N0,ae,N0,ae,N0],E={DISPLAY:M0[At],TEXT:M0[ae],SCRIPT:M0[pe],SCRIPTSCRIPT:M0[ne]},pt=[{name:"latin",blocks:[[256,591],[768,879]]},{name:"cyrillic",blocks:[[1024,1279]]},{name:"armenian",blocks:[[1328,1423]]},{name:"brahmic",blocks:[[2304,4255]]},{name:"georgian",blocks:[[4256,4351]]},{name:"cjk",blocks:[[12288,12543],[19968,40879],[65280,65376]]},{name:"hangul",blocks:[[44032,55215]]}];function Ha(r){for(var e=0;e=n[0]&&r<=n[1])return t.name}return null}var Ie=[];pt.forEach(r=>r.blocks.forEach(e=>Ie.push(...e)));function Ar(r){for(var e=0;e=Ie[e]&&r<=Ie[e+1])return!0;return!1}var re=80,Pa=function(e,t){return"M95,"+(622+e+t)+`
+c-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14
+c0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54
+c44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10
+s173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429
+c69,-144,104.5,-217.7,106.5,-221
+l`+e/2.075+" -"+e+`
+c5.3,-9.3,12,-14,20,-14
+H400000v`+(40+e)+`H845.2724
+s-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7
+c-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z
+M`+(834+e)+" "+t+"h400000v"+(40+e)+"h-400000z"},Ga=function(e,t){return"M263,"+(601+e+t)+`c0.7,0,18,39.7,52,119
+c34,79.3,68.167,158.7,102.5,238c34.3,79.3,51.8,119.3,52.5,120
+c340,-704.7,510.7,-1060.3,512,-1067
+l`+e/2.084+" -"+e+`
+c4.7,-7.3,11,-11,19,-11
+H40000v`+(40+e)+`H1012.3
+s-271.3,567,-271.3,567c-38.7,80.7,-84,175,-136,283c-52,108,-89.167,185.3,-111.5,232
+c-22.3,46.7,-33.8,70.3,-34.5,71c-4.7,4.7,-12.3,7,-23,7s-12,-1,-12,-1
+s-109,-253,-109,-253c-72.7,-168,-109.3,-252,-110,-252c-10.7,8,-22,16.7,-34,26
+c-22,17.3,-33.3,26,-34,26s-26,-26,-26,-26s76,-59,76,-59s76,-60,76,-60z
+M`+(1001+e)+" "+t+"h400000v"+(40+e)+"h-400000z"},Va=function(e,t){return"M983 "+(10+e+t)+`
+l`+e/3.13+" -"+e+`
+c4,-6.7,10,-10,18,-10 H400000v`+(40+e)+`
+H1013.1s-83.4,268,-264.1,840c-180.7,572,-277,876.3,-289,913c-4.7,4.7,-12.7,7,-24,7
+s-12,0,-12,0c-1.3,-3.3,-3.7,-11.7,-7,-25c-35.3,-125.3,-106.7,-373.3,-214,-744
+c-10,12,-21,25,-33,39s-32,39,-32,39c-6,-5.3,-15,-14,-27,-26s25,-30,25,-30
+c26.7,-32.7,52,-63,76,-91s52,-60,52,-60s208,722,208,722
+c56,-175.3,126.3,-397.3,211,-666c84.7,-268.7,153.8,-488.2,207.5,-658.5
+c53.7,-170.3,84.5,-266.8,92.5,-289.5z
+M`+(1001+e)+" "+t+"h400000v"+(40+e)+"h-400000z"},Ua=function(e,t){return"M424,"+(2398+e+t)+`
+c-1.3,-0.7,-38.5,-172,-111.5,-514c-73,-342,-109.8,-513.3,-110.5,-514
+c0,-2,-10.7,14.3,-32,49c-4.7,7.3,-9.8,15.7,-15.5,25c-5.7,9.3,-9.8,16,-12.5,20
+s-5,7,-5,7c-4,-3.3,-8.3,-7.7,-13,-13s-13,-13,-13,-13s76,-122,76,-122s77,-121,77,-121
+s209,968,209,968c0,-2,84.7,-361.7,254,-1079c169.3,-717.3,254.7,-1077.7,256,-1081
+l`+e/4.223+" -"+e+`c4,-6.7,10,-10,18,-10 H400000
+v`+(40+e)+`H1014.6
+s-87.3,378.7,-272.6,1166c-185.3,787.3,-279.3,1182.3,-282,1185
+c-2,6,-10,9,-24,9
+c-8,0,-12,-0.7,-12,-2z M`+(1001+e)+" "+t+`
+h400000v`+(40+e)+"h-400000z"},Xa=function(e,t){return"M473,"+(2713+e+t)+`
+c339.3,-1799.3,509.3,-2700,510,-2702 l`+e/5.298+" -"+e+`
+c3.3,-7.3,9.3,-11,18,-11 H400000v`+(40+e)+`H1017.7
+s-90.5,478,-276.2,1466c-185.7,988,-279.5,1483,-281.5,1485c-2,6,-10,9,-24,9
+c-8,0,-12,-0.7,-12,-2c0,-1.3,-5.3,-32,-16,-92c-50.7,-293.3,-119.7,-693.3,-207,-1200
+c0,-1.3,-5.3,8.7,-16,30c-10.7,21.3,-21.3,42.7,-32,64s-16,33,-16,33s-26,-26,-26,-26
+s76,-153,76,-153s77,-151,77,-151c0.7,0.7,35.7,202,105,604c67.3,400.7,102,602.7,104,
+606zM`+(1001+e)+" "+t+"h400000v"+(40+e)+"H1017.7z"},Wa=function(e){var t=e/2;return"M400000 "+e+" H0 L"+t+" 0 l65 45 L145 "+(e-80)+" H400000z"},Ya=function(e,t,a){var n=a-54-t-e;return"M702 "+(e+t)+"H400000"+(40+e)+`
+H742v`+n+`l-4 4-4 4c-.667.7 -2 1.5-4 2.5s-4.167 1.833-6.5 2.5-5.5 1-9.5 1
+h-12l-28-84c-16.667-52-96.667 -294.333-240-727l-212 -643 -85 170
+c-4-3.333-8.333-7.667-13 -13l-13-13l77-155 77-156c66 199.333 139 419.667
+219 661 l218 661zM702 `+t+"H400000v"+(40+e)+"H742z"},Za=function(e,t,a){t=1e3*t;var n="";switch(e){case"sqrtMain":n=Pa(t,re);break;case"sqrtSize1":n=Ga(t,re);break;case"sqrtSize2":n=Va(t,re);break;case"sqrtSize3":n=Ua(t,re);break;case"sqrtSize4":n=Xa(t,re);break;case"sqrtTall":n=Ya(t,re,a)}return n},ja=function(e,t){switch(e){case"\u239C":return"M291 0 H417 V"+t+" H291z M291 0 H417 V"+t+" H291z";case"\u2223":return"M145 0 H188 V"+t+" H145z M145 0 H188 V"+t+" H145z";case"\u2225":return"M145 0 H188 V"+t+" H145z M145 0 H188 V"+t+" H145z"+("M367 0 H410 V"+t+" H367z M367 0 H410 V"+t+" H367z");case"\u239F":return"M457 0 H583 V"+t+" H457z M457 0 H583 V"+t+" H457z";case"\u23A2":return"M319 0 H403 V"+t+" H319z M319 0 H403 V"+t+" H319z";case"\u23A5":return"M263 0 H347 V"+t+" H263z M263 0 H347 V"+t+" H263z";case"\u23AA":return"M384 0 H504 V"+t+" H384z M384 0 H504 V"+t+" H384z";case"\u23D0":return"M312 0 H355 V"+t+" H312z M312 0 H355 V"+t+" H312z";case"\u2016":return"M257 0 H300 V"+t+" H257z M257 0 H300 V"+t+" H257z"+("M478 0 H521 V"+t+" H478z M478 0 H521 V"+t+" H478z");default:return""}},Yt={doubleleftarrow:`M262 157
+l10-10c34-36 62.7-77 86-123 3.3-8 5-13.3 5-16 0-5.3-6.7-8-20-8-7.3
+ 0-12.2.5-14.5 1.5-2.3 1-4.8 4.5-7.5 10.5-49.3 97.3-121.7 169.3-217 216-28
+ 14-57.3 25-88 33-6.7 2-11 3.8-13 5.5-2 1.7-3 4.2-3 7.5s1 5.8 3 7.5
+c2 1.7 6.3 3.5 13 5.5 68 17.3 128.2 47.8 180.5 91.5 52.3 43.7 93.8 96.2 124.5
+ 157.5 9.3 8 15.3 12.3 18 13h6c12-.7 18-4 18-10 0-2-1.7-7-5-15-23.3-46-52-87
+-86-123l-10-10h399738v-40H218c328 0 0 0 0 0l-10-8c-26.7-20-65.7-43-117-69 2.7
+-2 6-3.7 10-5 36.7-16 72.3-37.3 107-64l10-8h399782v-40z
+m8 0v40h399730v-40zm0 194v40h399730v-40z`,doublerightarrow:`M399738 392l
+-10 10c-34 36-62.7 77-86 123-3.3 8-5 13.3-5 16 0 5.3 6.7 8 20 8 7.3 0 12.2-.5
+ 14.5-1.5 2.3-1 4.8-4.5 7.5-10.5 49.3-97.3 121.7-169.3 217-216 28-14 57.3-25 88
+-33 6.7-2 11-3.8 13-5.5 2-1.7 3-4.2 3-7.5s-1-5.8-3-7.5c-2-1.7-6.3-3.5-13-5.5-68
+-17.3-128.2-47.8-180.5-91.5-52.3-43.7-93.8-96.2-124.5-157.5-9.3-8-15.3-12.3-18
+-13h-6c-12 .7-18 4-18 10 0 2 1.7 7 5 15 23.3 46 52 87 86 123l10 10H0v40h399782
+c-328 0 0 0 0 0l10 8c26.7 20 65.7 43 117 69-2.7 2-6 3.7-10 5-36.7 16-72.3 37.3
+-107 64l-10 8H0v40zM0 157v40h399730v-40zm0 194v40h399730v-40z`,leftarrow:`M400000 241H110l3-3c68.7-52.7 113.7-120
+ 135-202 4-14.7 6-23 6-25 0-7.3-7-11-21-11-8 0-13.2.8-15.5 2.5-2.3 1.7-4.2 5.8
+-5.5 12.5-1.3 4.7-2.7 10.3-4 17-12 48.7-34.8 92-68.5 130S65.3 228.3 18 247
+c-10 4-16 7.7-18 11 0 8.7 6 14.3 18 17 47.3 18.7 87.8 47 121.5 85S196 441.3 208
+ 490c.7 2 1.3 5 2 9s1.2 6.7 1.5 8c.3 1.3 1 3.3 2 6s2.2 4.5 3.5 5.5c1.3 1 3.3
+ 1.8 6 2.5s6 1 10 1c14 0 21-3.7 21-11 0-2-2-10.3-6-25-20-79.3-65-146.7-135-202
+ l-3-3h399890zM100 241v40h399900v-40z`,leftbrace:`M6 548l-6-6v-35l6-11c56-104 135.3-181.3 238-232 57.3-28.7 117
+-45 179-50h399577v120H403c-43.3 7-81 15-113 26-100.7 33-179.7 91-237 174-2.7
+ 5-6 9-10 13-.7 1-7.3 1-20 1H6z`,leftbraceunder:`M0 6l6-6h17c12.688 0 19.313.3 20 1 4 4 7.313 8.3 10 13
+ 35.313 51.3 80.813 93.8 136.5 127.5 55.688 33.7 117.188 55.8 184.5 66.5.688
+ 0 2 .3 4 1 18.688 2.7 76 4.3 172 5h399450v120H429l-6-1c-124.688-8-235-61.7
+-331-161C60.687 138.7 32.312 99.3 7 54L0 41V6z`,leftgroup:`M400000 80
+H435C64 80 168.3 229.4 21 260c-5.9 1.2-18 0-18 0-2 0-3-1-3-3v-38C76 61 257 0
+ 435 0h399565z`,leftgroupunder:`M400000 262
+H435C64 262 168.3 112.6 21 82c-5.9-1.2-18 0-18 0-2 0-3 1-3 3v38c76 158 257 219
+ 435 219h399565z`,leftharpoon:`M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3
+-3.3 10.2-9.5 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5
+-18.3 3-21-1.3-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7
+-196 228-6.7 4.7-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40z`,leftharpoonplus:`M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3-3.3 10.2-9.5
+ 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5-18.3 3-21-1.3
+-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7-196 228-6.7 4.7
+-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40zM0 435v40h400000v-40z
+m0 0v40h400000v-40z`,leftharpoondown:`M7 241c-4 4-6.333 8.667-7 14 0 5.333.667 9 2 11s5.333
+ 5.333 12 10c90.667 54 156 130 196 228 3.333 10.667 6.333 16.333 9 17 2 .667 5
+ 1 9 1h5c10.667 0 16.667-2 18-6 2-2.667 1-9.667-3-21-32-87.333-82.667-157.667
+-152-211l-3-3h399907v-40zM93 281 H400000 v-40L7 241z`,leftharpoondownplus:`M7 435c-4 4-6.3 8.7-7 14 0 5.3.7 9 2 11s5.3 5.3 12
+ 10c90.7 54 156 130 196 228 3.3 10.7 6.3 16.3 9 17 2 .7 5 1 9 1h5c10.7 0 16.7
+-2 18-6 2-2.7 1-9.7-3-21-32-87.3-82.7-157.7-152-211l-3-3h399907v-40H7zm93 0
+v40h399900v-40zM0 241v40h399900v-40zm0 0v40h399900v-40z`,lefthook:`M400000 281 H103s-33-11.2-61-33.5S0 197.3 0 164s14.2-61.2 42.5
+-83.5C70.8 58.2 104 47 142 47 c16.7 0 25 6.7 25 20 0 12-8.7 18.7-26 20-40 3.3
+-68.7 15.7-86 37-10 12-15 25.3-15 40 0 22.7 9.8 40.7 29.5 54 19.7 13.3 43.5 21
+ 71.5 23h399859zM103 281v-40h399897v40z`,leftlinesegment:`M40 281 V428 H0 V94 H40 V241 H400000 v40z
+M40 281 V428 H0 V94 H40 V241 H400000 v40z`,leftmapsto:`M40 281 V448H0V74H40V241H400000v40z
+M40 281 V448H0V74H40V241H400000v40z`,leftToFrom:`M0 147h400000v40H0zm0 214c68 40 115.7 95.7 143 167h22c15.3 0 23
+-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69-70-101l-7-8h399905v-40H95l7-8
+c28.7-32 52-65.7 70-101 10.7-23.3 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 265.3
+ 68 321 0 361zm0-174v-40h399900v40zm100 154v40h399900v-40z`,longequal:`M0 50 h400000 v40H0z m0 194h40000v40H0z
+M0 50 h400000 v40H0z m0 194h40000v40H0z`,midbrace:`M200428 334
+c-100.7-8.3-195.3-44-280-108-55.3-42-101.7-93-139-153l-9-14c-2.7 4-5.7 8.7-9 14
+-53.3 86.7-123.7 153-211 199-66.7 36-137.3 56.3-212 62H0V214h199568c178.3-11.7
+ 311.7-78.3 403-201 6-8 9.7-12 11-12 .7-.7 6.7-1 18-1s17.3.3 18 1c1.3 0 5 4 11
+ 12 44.7 59.3 101.3 106.3 170 141s145.3 54.3 229 60h199572v120z`,midbraceunder:`M199572 214
+c100.7 8.3 195.3 44 280 108 55.3 42 101.7 93 139 153l9 14c2.7-4 5.7-8.7 9-14
+ 53.3-86.7 123.7-153 211-199 66.7-36 137.3-56.3 212-62h199568v120H200432c-178.3
+ 11.7-311.7 78.3-403 201-6 8-9.7 12-11 12-.7.7-6.7 1-18 1s-17.3-.3-18-1c-1.3 0
+-5-4-11-12-44.7-59.3-101.3-106.3-170-141s-145.3-54.3-229-60H0V214z`,oiintSize1:`M512.6 71.6c272.6 0 320.3 106.8 320.3 178.2 0 70.8-47.7 177.6
+-320.3 177.6S193.1 320.6 193.1 249.8c0-71.4 46.9-178.2 319.5-178.2z
+m368.1 178.2c0-86.4-60.9-215.4-368.1-215.4-306.4 0-367.3 129-367.3 215.4 0 85.8
+60.9 214.8 367.3 214.8 307.2 0 368.1-129 368.1-214.8z`,oiintSize2:`M757.8 100.1c384.7 0 451.1 137.6 451.1 230 0 91.3-66.4 228.8
+-451.1 228.8-386.3 0-452.7-137.5-452.7-228.8 0-92.4 66.4-230 452.7-230z
+m502.4 230c0-111.2-82.4-277.2-502.4-277.2s-504 166-504 277.2
+c0 110 84 276 504 276s502.4-166 502.4-276z`,oiiintSize1:`M681.4 71.6c408.9 0 480.5 106.8 480.5 178.2 0 70.8-71.6 177.6
+-480.5 177.6S202.1 320.6 202.1 249.8c0-71.4 70.5-178.2 479.3-178.2z
+m525.8 178.2c0-86.4-86.8-215.4-525.7-215.4-437.9 0-524.7 129-524.7 215.4 0
+85.8 86.8 214.8 524.7 214.8 438.9 0 525.7-129 525.7-214.8z`,oiiintSize2:`M1021.2 53c603.6 0 707.8 165.8 707.8 277.2 0 110-104.2 275.8
+-707.8 275.8-606 0-710.2-165.8-710.2-275.8C311 218.8 415.2 53 1021.2 53z
+m770.4 277.1c0-131.2-126.4-327.6-770.5-327.6S248.4 198.9 248.4 330.1
+c0 130 128.8 326.4 772.7 326.4s770.5-196.4 770.5-326.4z`,rightarrow:`M0 241v40h399891c-47.3 35.3-84 78-110 128
+-16.7 32-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20
+ 11 8 0 13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7
+ 39-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85
+-40.5-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5
+-12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67
+ 151.7 139 205zm0 0v40h399900v-40z`,rightbrace:`M400000 542l
+-6 6h-17c-12.7 0-19.3-.3-20-1-4-4-7.3-8.3-10-13-35.3-51.3-80.8-93.8-136.5-127.5
+s-117.2-55.8-184.5-66.5c-.7 0-2-.3-4-1-18.7-2.7-76-4.3-172-5H0V214h399571l6 1
+c124.7 8 235 61.7 331 161 31.3 33.3 59.7 72.7 85 118l7 13v35z`,rightbraceunder:`M399994 0l6 6v35l-6 11c-56 104-135.3 181.3-238 232-57.3
+ 28.7-117 45-179 50H-300V214h399897c43.3-7 81-15 113-26 100.7-33 179.7-91 237
+-174 2.7-5 6-9 10-13 .7-1 7.3-1 20-1h17z`,rightgroup:`M0 80h399565c371 0 266.7 149.4 414 180 5.9 1.2 18 0 18 0 2 0
+ 3-1 3-3v-38c-76-158-257-219-435-219H0z`,rightgroupunder:`M0 262h399565c371 0 266.7-149.4 414-180 5.9-1.2 18 0 18
+ 0 2 0 3 1 3 3v38c-76 158-257 219-435 219H0z`,rightharpoon:`M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3
+-3.7-15.3-11-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2
+-10.7 0-16.7 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58
+ 69.2 92 94.5zm0 0v40h399900v-40z`,rightharpoonplus:`M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3-3.7-15.3-11
+-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2-10.7 0-16.7
+ 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58 69.2 92 94.5z
+m0 0v40h399900v-40z m100 194v40h399900v-40zm0 0v40h399900v-40z`,rightharpoondown:`M399747 511c0 7.3 6.7 11 20 11 8 0 13-.8 15-2.5s4.7-6.8
+ 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3 8.5-5.8 9.5
+-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3-64.7 57-92 95
+-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 241v40h399900v-40z`,rightharpoondownplus:`M399747 705c0 7.3 6.7 11 20 11 8 0 13-.8
+ 15-2.5s4.7-6.8 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3
+ 8.5-5.8 9.5-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3
+-64.7 57-92 95-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 435v40h399900v-40z
+m0-194v40h400000v-40zm0 0v40h400000v-40z`,righthook:`M399859 241c-764 0 0 0 0 0 40-3.3 68.7-15.7 86-37 10-12 15-25.3
+ 15-40 0-22.7-9.8-40.7-29.5-54-19.7-13.3-43.5-21-71.5-23-17.3-1.3-26-8-26-20 0
+-13.3 8.7-20 26-20 38 0 71 11.2 99 33.5 0 0 7 5.6 21 16.7 14 11.2 21 33.5 21
+ 66.8s-14 61.2-42 83.5c-28 22.3-61 33.5-99 33.5L0 241z M0 281v-40h399859v40z`,rightlinesegment:`M399960 241 V94 h40 V428 h-40 V281 H0 v-40z
+M399960 241 V94 h40 V428 h-40 V281 H0 v-40z`,rightToFrom:`M400000 167c-70.7-42-118-97.7-142-167h-23c-15.3 0-23 .3-23
+ 1 0 1.3 5.3 13.7 16 37 18 35.3 41.3 69 70 101l7 8H0v40h399905l-7 8c-28.7 32
+-52 65.7-70 101-10.7 23.3-16 35.7-16 37 0 .7 7.7 1 23 1h23c24-69.3 71.3-125 142
+-167z M100 147v40h399900v-40zM0 341v40h399900v-40z`,twoheadleftarrow:`M0 167c68 40
+ 115.7 95.7 143 167h22c15.3 0 23-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69
+-70-101l-7-8h125l9 7c50.7 39.3 85 86 103 140h46c0-4.7-6.3-18.7-19-42-18-35.3
+-40-67.3-66-96l-9-9h399716v-40H284l9-9c26-28.7 48-60.7 66-96 12.7-23.333 19
+-37.333 19-42h-46c-18 54-52.3 100.7-103 140l-9 7H95l7-8c28.7-32 52-65.7 70-101
+ 10.7-23.333 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 71.3 68 127 0 167z`,twoheadrightarrow:`M400000 167
+c-68-40-115.7-95.7-143-167h-22c-15.3 0-23 .3-23 1 0 1.3 5.3 13.7 16 37 18 35.3
+ 41.3 69 70 101l7 8h-125l-9-7c-50.7-39.3-85-86-103-140h-46c0 4.7 6.3 18.7 19 42
+ 18 35.3 40 67.3 66 96l9 9H0v40h399716l-9 9c-26 28.7-48 60.7-66 96-12.7 23.333
+-19 37.333-19 42h46c18-54 52.3-100.7 103-140l9-7h125l-7 8c-28.7 32-52 65.7-70
+ 101-10.7 23.333-16 35.7-16 37 0 .7 7.7 1 23 1h22c27.3-71.3 75-127 143-167z`,tilde1:`M200 55.538c-77 0-168 73.953-177 73.953-3 0-7
+-2.175-9-5.437L2 97c-1-2-2-4-2-6 0-4 2-7 5-9l20-12C116 12 171 0 207 0c86 0
+ 114 68 191 68 78 0 168-68 177-68 4 0 7 2 9 5l12 19c1 2.175 2 4.35 2 6.525 0
+ 4.35-2 7.613-5 9.788l-19 13.05c-92 63.077-116.937 75.308-183 76.128
+-68.267.847-113-73.952-191-73.952z`,tilde2:`M344 55.266c-142 0-300.638 81.316-311.5 86.418
+-8.01 3.762-22.5 10.91-23.5 5.562L1 120c-1-2-1-3-1-4 0-5 3-9 8-10l18.4-9C160.9
+ 31.9 283 0 358 0c148 0 188 122 331 122s314-97 326-97c4 0 8 2 10 7l7 21.114
+c1 2.14 1 3.21 1 4.28 0 5.347-3 9.626-7 10.696l-22.3 12.622C852.6 158.372 751
+ 181.476 676 181.476c-149 0-189-126.21-332-126.21z`,tilde3:`M786 59C457 59 32 175.242 13 175.242c-6 0-10-3.457
+-11-10.37L.15 138c-1-7 3-12 10-13l19.2-6.4C378.4 40.7 634.3 0 804.3 0c337 0
+ 411.8 157 746.8 157 328 0 754-112 773-112 5 0 10 3 11 9l1 14.075c1 8.066-.697
+ 16.595-6.697 17.492l-21.052 7.31c-367.9 98.146-609.15 122.696-778.15 122.696
+ -338 0-409-156.573-744-156.573z`,tilde4:`M786 58C457 58 32 177.487 13 177.487c-6 0-10-3.345
+-11-10.035L.15 143c-1-7 3-12 10-13l22-6.7C381.2 35 637.15 0 807.15 0c337 0 409
+ 177 744 177 328 0 754-127 773-127 5 0 10 3 11 9l1 14.794c1 7.805-3 13.38-9
+ 14.495l-20.7 5.574c-366.85 99.79-607.3 139.372-776.3 139.372-338 0-409
+ -175.236-744-175.236z`,vec:`M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5
+3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11
+10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63
+-1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1
+-7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59
+H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359
+c-16-25.333-24-45-24-59z`,widehat1:`M529 0h5l519 115c5 1 9 5 9 10 0 1-1 2-1 3l-4 22
+c-1 5-5 9-11 9h-2L532 67 19 159h-2c-5 0-9-4-11-9l-5-22c-1-6 2-12 8-13z`,widehat2:`M1181 0h2l1171 176c6 0 10 5 10 11l-2 23c-1 6-5 10
+-11 10h-1L1182 67 15 220h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z`,widehat3:`M1181 0h2l1171 236c6 0 10 5 10 11l-2 23c-1 6-5 10
+-11 10h-1L1182 67 15 280h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z`,widehat4:`M1181 0h2l1171 296c6 0 10 5 10 11l-2 23c-1 6-5 10
+-11 10h-1L1182 67 15 340h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z`,widecheck1:`M529,159h5l519,-115c5,-1,9,-5,9,-10c0,-1,-1,-2,-1,-3l-4,-22c-1,
+-5,-5,-9,-11,-9h-2l-512,92l-513,-92h-2c-5,0,-9,4,-11,9l-5,22c-1,6,2,12,8,13z`,widecheck2:`M1181,220h2l1171,-176c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,
+-11,-10h-1l-1168,153l-1167,-153h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z`,widecheck3:`M1181,280h2l1171,-236c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,
+-11,-10h-1l-1168,213l-1167,-213h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z`,widecheck4:`M1181,340h2l1171,-296c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,
+-11,-10h-1l-1168,273l-1167,-273h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z`,baraboveleftarrow:`M400000 620h-399890l3 -3c68.7 -52.7 113.7 -120 135 -202
+c4 -14.7 6 -23 6 -25c0 -7.3 -7 -11 -21 -11c-8 0 -13.2 0.8 -15.5 2.5
+c-2.3 1.7 -4.2 5.8 -5.5 12.5c-1.3 4.7 -2.7 10.3 -4 17c-12 48.7 -34.8 92 -68.5 130
+s-74.2 66.3 -121.5 85c-10 4 -16 7.7 -18 11c0 8.7 6 14.3 18 17c47.3 18.7 87.8 47
+121.5 85s56.5 81.3 68.5 130c0.7 2 1.3 5 2 9s1.2 6.7 1.5 8c0.3 1.3 1 3.3 2 6
+s2.2 4.5 3.5 5.5c1.3 1 3.3 1.8 6 2.5s6 1 10 1c14 0 21 -3.7 21 -11
+c0 -2 -2 -10.3 -6 -25c-20 -79.3 -65 -146.7 -135 -202l-3 -3h399890z
+M100 620v40h399900v-40z M0 241v40h399900v-40zM0 241v40h399900v-40z`,rightarrowabovebar:`M0 241v40h399891c-47.3 35.3-84 78-110 128-16.7 32
+-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20 11 8 0
+13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7 39
+-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85-40.5
+-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5
+-12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67
+151.7 139 205zm96 379h399894v40H0zm0 0h399904v40H0z`,baraboveshortleftharpoon:`M507,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11
+c1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17
+c2,0.7,5,1,9,1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21
+c-32,-87.3,-82.7,-157.7,-152,-211c0,0,-3,-3,-3,-3l399351,0l0,-40
+c-398570,0,-399437,0,-399437,0z M593 435 v40 H399500 v-40z
+M0 281 v-40 H399908 v40z M0 281 v-40 H399908 v40z`,rightharpoonaboveshortbar:`M0,241 l0,40c399126,0,399993,0,399993,0
+c4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199,
+-231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6
+c-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z
+M0 241 v40 H399908 v-40z M0 475 v-40 H399500 v40z M0 475 v-40 H399500 v40z`,shortbaraboveleftharpoon:`M7,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11
+c1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17c2,0.7,5,1,9,
+1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21c-32,-87.3,-82.7,-157.7,
+-152,-211c0,0,-3,-3,-3,-3l399907,0l0,-40c-399126,0,-399993,0,-399993,0z
+M93 435 v40 H400000 v-40z M500 241 v40 H400000 v-40z M500 241 v40 H400000 v-40z`,shortrightharpoonabovebar:`M53,241l0,40c398570,0,399437,0,399437,0
+c4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199,
+-231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6
+c-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z
+M500 241 v40 H399408 v-40z M500 435 v40 H400000 v-40z`},Ka=function(e,t){switch(e){case"lbrack":return"M403 1759 V84 H666 V0 H319 V1759 v"+t+` v1759 h347 v-84
+H403z M403 1759 V0 H319 V1759 v`+t+" v1759 h84z";case"rbrack":return"M347 1759 V0 H0 V84 H263 V1759 v"+t+` v1759 H0 v84 H347z
+M347 1759 V0 H263 V1759 v`+t+" v1759 h84z";case"vert":return"M145 15 v585 v"+t+` v585 c2.667,10,9.667,15,21,15
+c10,0,16.667,-5,20,-15 v-585 v`+-t+` v-585 c-2.667,-10,-9.667,-15,-21,-15
+c-10,0,-16.667,5,-20,15z M188 15 H145 v585 v`+t+" v585 h43z";case"doublevert":return"M145 15 v585 v"+t+` v585 c2.667,10,9.667,15,21,15
+c10,0,16.667,-5,20,-15 v-585 v`+-t+` v-585 c-2.667,-10,-9.667,-15,-21,-15
+c-10,0,-16.667,5,-20,15z M188 15 H145 v585 v`+t+` v585 h43z
+M367 15 v585 v`+t+` v585 c2.667,10,9.667,15,21,15
+c10,0,16.667,-5,20,-15 v-585 v`+-t+` v-585 c-2.667,-10,-9.667,-15,-21,-15
+c-10,0,-16.667,5,-20,15z M410 15 H367 v585 v`+t+" v585 h43z";case"lfloor":return"M319 602 V0 H403 V602 v"+t+` v1715 h263 v84 H319z
+MM319 602 V0 H403 V602 v`+t+" v1715 H319z";case"rfloor":return"M319 602 V0 H403 V602 v"+t+` v1799 H0 v-84 H319z
+MM319 602 V0 H403 V602 v`+t+" v1715 H319z";case"lceil":return"M403 1759 V84 H666 V0 H319 V1759 v"+t+` v602 h84z
+M403 1759 V0 H319 V1759 v`+t+" v602 h84z";case"rceil":return"M347 1759 V0 H0 V84 H263 V1759 v"+t+` v602 h84z
+M347 1759 V0 h-84 V1759 v`+t+" v602 h84z";case"lparen":return`M863,9c0,-2,-2,-5,-6,-9c0,0,-17,0,-17,0c-12.7,0,-19.3,0.3,-20,1
+c-5.3,5.3,-10.3,11,-15,17c-242.7,294.7,-395.3,682,-458,1162c-21.3,163.3,-33.3,349,
+-36,557 l0,`+(t+84)+`c0.2,6,0,26,0,60c2,159.3,10,310.7,24,454c53.3,528,210,
+949.7,470,1265c4.7,6,9.7,11.7,15,17c0.7,0.7,7,1,19,1c0,0,18,0,18,0c4,-4,6,-7,6,-9
+c0,-2.7,-3.3,-8.7,-10,-18c-135.3,-192.7,-235.5,-414.3,-300.5,-665c-65,-250.7,-102.5,
+-544.7,-112.5,-882c-2,-104,-3,-167,-3,-189
+l0,-`+(t+92)+`c0,-162.7,5.7,-314,17,-454c20.7,-272,63.7,-513,129,-723c65.3,
+-210,155.3,-396.3,270,-559c6.7,-9.3,10,-15.3,10,-18z`;case"rparen":return`M76,0c-16.7,0,-25,3,-25,9c0,2,2,6.3,6,13c21.3,28.7,42.3,60.3,
+63,95c96.7,156.7,172.8,332.5,228.5,527.5c55.7,195,92.8,416.5,111.5,664.5
+c11.3,139.3,17,290.7,17,454c0,28,1.7,43,3.3,45l0,`+(t+9)+`
+c-3,4,-3.3,16.7,-3.3,38c0,162,-5.7,313.7,-17,455c-18.7,248,-55.8,469.3,-111.5,664
+c-55.7,194.7,-131.8,370.3,-228.5,527c-20.7,34.7,-41.7,66.3,-63,95c-2,3.3,-4,7,-6,11
+c0,7.3,5.7,11,17,11c0,0,11,0,11,0c9.3,0,14.3,-0.3,15,-1c5.3,-5.3,10.3,-11,15,-17
+c242.7,-294.7,395.3,-681.7,458,-1161c21.3,-164.7,33.3,-350.7,36,-558
+l0,-`+(t+144)+`c-2,-159.3,-10,-310.7,-24,-454c-53.3,-528,-210,-949.7,
+-470,-1265c-4.7,-6,-9.7,-11.7,-15,-17c-0.7,-0.7,-6.7,-1,-18,-1z`;default:throw new Error("Unknown stretchy delimiter.")}},Y0=class{constructor(e){this.children=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,this.children=e,this.classes=[],this.height=0,this.depth=0,this.maxFontSize=0,this.style={}}hasClass(e){return O.contains(this.classes,e)}toNode(){for(var e=document.createDocumentFragment(),t=0;tt.toText();return this.children.map(e).join("")}},z0={"AMS-Regular":{32:[0,0,0,0,.25],65:[0,.68889,0,0,.72222],66:[0,.68889,0,0,.66667],67:[0,.68889,0,0,.72222],68:[0,.68889,0,0,.72222],69:[0,.68889,0,0,.66667],70:[0,.68889,0,0,.61111],71:[0,.68889,0,0,.77778],72:[0,.68889,0,0,.77778],73:[0,.68889,0,0,.38889],74:[.16667,.68889,0,0,.5],75:[0,.68889,0,0,.77778],76:[0,.68889,0,0,.66667],77:[0,.68889,0,0,.94445],78:[0,.68889,0,0,.72222],79:[.16667,.68889,0,0,.77778],80:[0,.68889,0,0,.61111],81:[.16667,.68889,0,0,.77778],82:[0,.68889,0,0,.72222],83:[0,.68889,0,0,.55556],84:[0,.68889,0,0,.66667],85:[0,.68889,0,0,.72222],86:[0,.68889,0,0,.72222],87:[0,.68889,0,0,1],88:[0,.68889,0,0,.72222],89:[0,.68889,0,0,.72222],90:[0,.68889,0,0,.66667],107:[0,.68889,0,0,.55556],160:[0,0,0,0,.25],165:[0,.675,.025,0,.75],174:[.15559,.69224,0,0,.94666],240:[0,.68889,0,0,.55556],295:[0,.68889,0,0,.54028],710:[0,.825,0,0,2.33334],732:[0,.9,0,0,2.33334],770:[0,.825,0,0,2.33334],771:[0,.9,0,0,2.33334],989:[.08167,.58167,0,0,.77778],1008:[0,.43056,.04028,0,.66667],8245:[0,.54986,0,0,.275],8463:[0,.68889,0,0,.54028],8487:[0,.68889,0,0,.72222],8498:[0,.68889,0,0,.55556],8502:[0,.68889,0,0,.66667],8503:[0,.68889,0,0,.44445],8504:[0,.68889,0,0,.66667],8513:[0,.68889,0,0,.63889],8592:[-.03598,.46402,0,0,.5],8594:[-.03598,.46402,0,0,.5],8602:[-.13313,.36687,0,0,1],8603:[-.13313,.36687,0,0,1],8606:[.01354,.52239,0,0,1],8608:[.01354,.52239,0,0,1],8610:[.01354,.52239,0,0,1.11111],8611:[.01354,.52239,0,0,1.11111],8619:[0,.54986,0,0,1],8620:[0,.54986,0,0,1],8621:[-.13313,.37788,0,0,1.38889],8622:[-.13313,.36687,0,0,1],8624:[0,.69224,0,0,.5],8625:[0,.69224,0,0,.5],8630:[0,.43056,0,0,1],8631:[0,.43056,0,0,1],8634:[.08198,.58198,0,0,.77778],8635:[.08198,.58198,0,0,.77778],8638:[.19444,.69224,0,0,.41667],8639:[.19444,.69224,0,0,.41667],8642:[.19444,.69224,0,0,.41667],8643:[.19444,.69224,0,0,.41667],8644:[.1808,.675,0,0,1],8646:[.1808,.675,0,0,1],8647:[.1808,.675,0,0,1],8648:[.19444,.69224,0,0,.83334],8649:[.1808,.675,0,0,1],8650:[.19444,.69224,0,0,.83334],8651:[.01354,.52239,0,0,1],8652:[.01354,.52239,0,0,1],8653:[-.13313,.36687,0,0,1],8654:[-.13313,.36687,0,0,1],8655:[-.13313,.36687,0,0,1],8666:[.13667,.63667,0,0,1],8667:[.13667,.63667,0,0,1],8669:[-.13313,.37788,0,0,1],8672:[-.064,.437,0,0,1.334],8674:[-.064,.437,0,0,1.334],8705:[0,.825,0,0,.5],8708:[0,.68889,0,0,.55556],8709:[.08167,.58167,0,0,.77778],8717:[0,.43056,0,0,.42917],8722:[-.03598,.46402,0,0,.5],8724:[.08198,.69224,0,0,.77778],8726:[.08167,.58167,0,0,.77778],8733:[0,.69224,0,0,.77778],8736:[0,.69224,0,0,.72222],8737:[0,.69224,0,0,.72222],8738:[.03517,.52239,0,0,.72222],8739:[.08167,.58167,0,0,.22222],8740:[.25142,.74111,0,0,.27778],8741:[.08167,.58167,0,0,.38889],8742:[.25142,.74111,0,0,.5],8756:[0,.69224,0,0,.66667],8757:[0,.69224,0,0,.66667],8764:[-.13313,.36687,0,0,.77778],8765:[-.13313,.37788,0,0,.77778],8769:[-.13313,.36687,0,0,.77778],8770:[-.03625,.46375,0,0,.77778],8774:[.30274,.79383,0,0,.77778],8776:[-.01688,.48312,0,0,.77778],8778:[.08167,.58167,0,0,.77778],8782:[.06062,.54986,0,0,.77778],8783:[.06062,.54986,0,0,.77778],8785:[.08198,.58198,0,0,.77778],8786:[.08198,.58198,0,0,.77778],8787:[.08198,.58198,0,0,.77778],8790:[0,.69224,0,0,.77778],8791:[.22958,.72958,0,0,.77778],8796:[.08198,.91667,0,0,.77778],8806:[.25583,.75583,0,0,.77778],8807:[.25583,.75583,0,0,.77778],8808:[.25142,.75726,0,0,.77778],8809:[.25142,.75726,0,0,.77778],8812:[.25583,.75583,0,0,.5],8814:[.20576,.70576,0,0,.77778],8815:[.20576,.70576,0,0,.77778],8816:[.30274,.79383,0,0,.77778],8817:[.30274,.79383,0,0,.77778],8818:[.22958,.72958,0,0,.77778],8819:[.22958,.72958,0,0,.77778],8822:[.1808,.675,0,0,.77778],8823:[.1808,.675,0,0,.77778],8828:[.13667,.63667,0,0,.77778],8829:[.13667,.63667,0,0,.77778],8830:[.22958,.72958,0,0,.77778],8831:[.22958,.72958,0,0,.77778],8832:[.20576,.70576,0,0,.77778],8833:[.20576,.70576,0,0,.77778],8840:[.30274,.79383,0,0,.77778],8841:[.30274,.79383,0,0,.77778],8842:[.13597,.63597,0,0,.77778],8843:[.13597,.63597,0,0,.77778],8847:[.03517,.54986,0,0,.77778],8848:[.03517,.54986,0,0,.77778],8858:[.08198,.58198,0,0,.77778],8859:[.08198,.58198,0,0,.77778],8861:[.08198,.58198,0,0,.77778],8862:[0,.675,0,0,.77778],8863:[0,.675,0,0,.77778],8864:[0,.675,0,0,.77778],8865:[0,.675,0,0,.77778],8872:[0,.69224,0,0,.61111],8873:[0,.69224,0,0,.72222],8874:[0,.69224,0,0,.88889],8876:[0,.68889,0,0,.61111],8877:[0,.68889,0,0,.61111],8878:[0,.68889,0,0,.72222],8879:[0,.68889,0,0,.72222],8882:[.03517,.54986,0,0,.77778],8883:[.03517,.54986,0,0,.77778],8884:[.13667,.63667,0,0,.77778],8885:[.13667,.63667,0,0,.77778],8888:[0,.54986,0,0,1.11111],8890:[.19444,.43056,0,0,.55556],8891:[.19444,.69224,0,0,.61111],8892:[.19444,.69224,0,0,.61111],8901:[0,.54986,0,0,.27778],8903:[.08167,.58167,0,0,.77778],8905:[.08167,.58167,0,0,.77778],8906:[.08167,.58167,0,0,.77778],8907:[0,.69224,0,0,.77778],8908:[0,.69224,0,0,.77778],8909:[-.03598,.46402,0,0,.77778],8910:[0,.54986,0,0,.76042],8911:[0,.54986,0,0,.76042],8912:[.03517,.54986,0,0,.77778],8913:[.03517,.54986,0,0,.77778],8914:[0,.54986,0,0,.66667],8915:[0,.54986,0,0,.66667],8916:[0,.69224,0,0,.66667],8918:[.0391,.5391,0,0,.77778],8919:[.0391,.5391,0,0,.77778],8920:[.03517,.54986,0,0,1.33334],8921:[.03517,.54986,0,0,1.33334],8922:[.38569,.88569,0,0,.77778],8923:[.38569,.88569,0,0,.77778],8926:[.13667,.63667,0,0,.77778],8927:[.13667,.63667,0,0,.77778],8928:[.30274,.79383,0,0,.77778],8929:[.30274,.79383,0,0,.77778],8934:[.23222,.74111,0,0,.77778],8935:[.23222,.74111,0,0,.77778],8936:[.23222,.74111,0,0,.77778],8937:[.23222,.74111,0,0,.77778],8938:[.20576,.70576,0,0,.77778],8939:[.20576,.70576,0,0,.77778],8940:[.30274,.79383,0,0,.77778],8941:[.30274,.79383,0,0,.77778],8994:[.19444,.69224,0,0,.77778],8995:[.19444,.69224,0,0,.77778],9416:[.15559,.69224,0,0,.90222],9484:[0,.69224,0,0,.5],9488:[0,.69224,0,0,.5],9492:[0,.37788,0,0,.5],9496:[0,.37788,0,0,.5],9585:[.19444,.68889,0,0,.88889],9586:[.19444,.74111,0,0,.88889],9632:[0,.675,0,0,.77778],9633:[0,.675,0,0,.77778],9650:[0,.54986,0,0,.72222],9651:[0,.54986,0,0,.72222],9654:[.03517,.54986,0,0,.77778],9660:[0,.54986,0,0,.72222],9661:[0,.54986,0,0,.72222],9664:[.03517,.54986,0,0,.77778],9674:[.11111,.69224,0,0,.66667],9733:[.19444,.69224,0,0,.94445],10003:[0,.69224,0,0,.83334],10016:[0,.69224,0,0,.83334],10731:[.11111,.69224,0,0,.66667],10846:[.19444,.75583,0,0,.61111],10877:[.13667,.63667,0,0,.77778],10878:[.13667,.63667,0,0,.77778],10885:[.25583,.75583,0,0,.77778],10886:[.25583,.75583,0,0,.77778],10887:[.13597,.63597,0,0,.77778],10888:[.13597,.63597,0,0,.77778],10889:[.26167,.75726,0,0,.77778],10890:[.26167,.75726,0,0,.77778],10891:[.48256,.98256,0,0,.77778],10892:[.48256,.98256,0,0,.77778],10901:[.13667,.63667,0,0,.77778],10902:[.13667,.63667,0,0,.77778],10933:[.25142,.75726,0,0,.77778],10934:[.25142,.75726,0,0,.77778],10935:[.26167,.75726,0,0,.77778],10936:[.26167,.75726,0,0,.77778],10937:[.26167,.75726,0,0,.77778],10938:[.26167,.75726,0,0,.77778],10949:[.25583,.75583,0,0,.77778],10950:[.25583,.75583,0,0,.77778],10955:[.28481,.79383,0,0,.77778],10956:[.28481,.79383,0,0,.77778],57350:[.08167,.58167,0,0,.22222],57351:[.08167,.58167,0,0,.38889],57352:[.08167,.58167,0,0,.77778],57353:[0,.43056,.04028,0,.66667],57356:[.25142,.75726,0,0,.77778],57357:[.25142,.75726,0,0,.77778],57358:[.41951,.91951,0,0,.77778],57359:[.30274,.79383,0,0,.77778],57360:[.30274,.79383,0,0,.77778],57361:[.41951,.91951,0,0,.77778],57366:[.25142,.75726,0,0,.77778],57367:[.25142,.75726,0,0,.77778],57368:[.25142,.75726,0,0,.77778],57369:[.25142,.75726,0,0,.77778],57370:[.13597,.63597,0,0,.77778],57371:[.13597,.63597,0,0,.77778]},"Caligraphic-Regular":{32:[0,0,0,0,.25],65:[0,.68333,0,.19445,.79847],66:[0,.68333,.03041,.13889,.65681],67:[0,.68333,.05834,.13889,.52653],68:[0,.68333,.02778,.08334,.77139],69:[0,.68333,.08944,.11111,.52778],70:[0,.68333,.09931,.11111,.71875],71:[.09722,.68333,.0593,.11111,.59487],72:[0,.68333,.00965,.11111,.84452],73:[0,.68333,.07382,0,.54452],74:[.09722,.68333,.18472,.16667,.67778],75:[0,.68333,.01445,.05556,.76195],76:[0,.68333,0,.13889,.68972],77:[0,.68333,0,.13889,1.2009],78:[0,.68333,.14736,.08334,.82049],79:[0,.68333,.02778,.11111,.79611],80:[0,.68333,.08222,.08334,.69556],81:[.09722,.68333,0,.11111,.81667],82:[0,.68333,0,.08334,.8475],83:[0,.68333,.075,.13889,.60556],84:[0,.68333,.25417,0,.54464],85:[0,.68333,.09931,.08334,.62583],86:[0,.68333,.08222,0,.61278],87:[0,.68333,.08222,.08334,.98778],88:[0,.68333,.14643,.13889,.7133],89:[.09722,.68333,.08222,.08334,.66834],90:[0,.68333,.07944,.13889,.72473],160:[0,0,0,0,.25]},"Fraktur-Regular":{32:[0,0,0,0,.25],33:[0,.69141,0,0,.29574],34:[0,.69141,0,0,.21471],38:[0,.69141,0,0,.73786],39:[0,.69141,0,0,.21201],40:[.24982,.74947,0,0,.38865],41:[.24982,.74947,0,0,.38865],42:[0,.62119,0,0,.27764],43:[.08319,.58283,0,0,.75623],44:[0,.10803,0,0,.27764],45:[.08319,.58283,0,0,.75623],46:[0,.10803,0,0,.27764],47:[.24982,.74947,0,0,.50181],48:[0,.47534,0,0,.50181],49:[0,.47534,0,0,.50181],50:[0,.47534,0,0,.50181],51:[.18906,.47534,0,0,.50181],52:[.18906,.47534,0,0,.50181],53:[.18906,.47534,0,0,.50181],54:[0,.69141,0,0,.50181],55:[.18906,.47534,0,0,.50181],56:[0,.69141,0,0,.50181],57:[.18906,.47534,0,0,.50181],58:[0,.47534,0,0,.21606],59:[.12604,.47534,0,0,.21606],61:[-.13099,.36866,0,0,.75623],63:[0,.69141,0,0,.36245],65:[0,.69141,0,0,.7176],66:[0,.69141,0,0,.88397],67:[0,.69141,0,0,.61254],68:[0,.69141,0,0,.83158],69:[0,.69141,0,0,.66278],70:[.12604,.69141,0,0,.61119],71:[0,.69141,0,0,.78539],72:[.06302,.69141,0,0,.7203],73:[0,.69141,0,0,.55448],74:[.12604,.69141,0,0,.55231],75:[0,.69141,0,0,.66845],76:[0,.69141,0,0,.66602],77:[0,.69141,0,0,1.04953],78:[0,.69141,0,0,.83212],79:[0,.69141,0,0,.82699],80:[.18906,.69141,0,0,.82753],81:[.03781,.69141,0,0,.82699],82:[0,.69141,0,0,.82807],83:[0,.69141,0,0,.82861],84:[0,.69141,0,0,.66899],85:[0,.69141,0,0,.64576],86:[0,.69141,0,0,.83131],87:[0,.69141,0,0,1.04602],88:[0,.69141,0,0,.71922],89:[.18906,.69141,0,0,.83293],90:[.12604,.69141,0,0,.60201],91:[.24982,.74947,0,0,.27764],93:[.24982,.74947,0,0,.27764],94:[0,.69141,0,0,.49965],97:[0,.47534,0,0,.50046],98:[0,.69141,0,0,.51315],99:[0,.47534,0,0,.38946],100:[0,.62119,0,0,.49857],101:[0,.47534,0,0,.40053],102:[.18906,.69141,0,0,.32626],103:[.18906,.47534,0,0,.5037],104:[.18906,.69141,0,0,.52126],105:[0,.69141,0,0,.27899],106:[0,.69141,0,0,.28088],107:[0,.69141,0,0,.38946],108:[0,.69141,0,0,.27953],109:[0,.47534,0,0,.76676],110:[0,.47534,0,0,.52666],111:[0,.47534,0,0,.48885],112:[.18906,.52396,0,0,.50046],113:[.18906,.47534,0,0,.48912],114:[0,.47534,0,0,.38919],115:[0,.47534,0,0,.44266],116:[0,.62119,0,0,.33301],117:[0,.47534,0,0,.5172],118:[0,.52396,0,0,.5118],119:[0,.52396,0,0,.77351],120:[.18906,.47534,0,0,.38865],121:[.18906,.47534,0,0,.49884],122:[.18906,.47534,0,0,.39054],160:[0,0,0,0,.25],8216:[0,.69141,0,0,.21471],8217:[0,.69141,0,0,.21471],58112:[0,.62119,0,0,.49749],58113:[0,.62119,0,0,.4983],58114:[.18906,.69141,0,0,.33328],58115:[.18906,.69141,0,0,.32923],58116:[.18906,.47534,0,0,.50343],58117:[0,.69141,0,0,.33301],58118:[0,.62119,0,0,.33409],58119:[0,.47534,0,0,.50073]},"Main-Bold":{32:[0,0,0,0,.25],33:[0,.69444,0,0,.35],34:[0,.69444,0,0,.60278],35:[.19444,.69444,0,0,.95833],36:[.05556,.75,0,0,.575],37:[.05556,.75,0,0,.95833],38:[0,.69444,0,0,.89444],39:[0,.69444,0,0,.31944],40:[.25,.75,0,0,.44722],41:[.25,.75,0,0,.44722],42:[0,.75,0,0,.575],43:[.13333,.63333,0,0,.89444],44:[.19444,.15556,0,0,.31944],45:[0,.44444,0,0,.38333],46:[0,.15556,0,0,.31944],47:[.25,.75,0,0,.575],48:[0,.64444,0,0,.575],49:[0,.64444,0,0,.575],50:[0,.64444,0,0,.575],51:[0,.64444,0,0,.575],52:[0,.64444,0,0,.575],53:[0,.64444,0,0,.575],54:[0,.64444,0,0,.575],55:[0,.64444,0,0,.575],56:[0,.64444,0,0,.575],57:[0,.64444,0,0,.575],58:[0,.44444,0,0,.31944],59:[.19444,.44444,0,0,.31944],60:[.08556,.58556,0,0,.89444],61:[-.10889,.39111,0,0,.89444],62:[.08556,.58556,0,0,.89444],63:[0,.69444,0,0,.54305],64:[0,.69444,0,0,.89444],65:[0,.68611,0,0,.86944],66:[0,.68611,0,0,.81805],67:[0,.68611,0,0,.83055],68:[0,.68611,0,0,.88194],69:[0,.68611,0,0,.75555],70:[0,.68611,0,0,.72361],71:[0,.68611,0,0,.90416],72:[0,.68611,0,0,.9],73:[0,.68611,0,0,.43611],74:[0,.68611,0,0,.59444],75:[0,.68611,0,0,.90138],76:[0,.68611,0,0,.69166],77:[0,.68611,0,0,1.09166],78:[0,.68611,0,0,.9],79:[0,.68611,0,0,.86388],80:[0,.68611,0,0,.78611],81:[.19444,.68611,0,0,.86388],82:[0,.68611,0,0,.8625],83:[0,.68611,0,0,.63889],84:[0,.68611,0,0,.8],85:[0,.68611,0,0,.88472],86:[0,.68611,.01597,0,.86944],87:[0,.68611,.01597,0,1.18888],88:[0,.68611,0,0,.86944],89:[0,.68611,.02875,0,.86944],90:[0,.68611,0,0,.70277],91:[.25,.75,0,0,.31944],92:[.25,.75,0,0,.575],93:[.25,.75,0,0,.31944],94:[0,.69444,0,0,.575],95:[.31,.13444,.03194,0,.575],97:[0,.44444,0,0,.55902],98:[0,.69444,0,0,.63889],99:[0,.44444,0,0,.51111],100:[0,.69444,0,0,.63889],101:[0,.44444,0,0,.52708],102:[0,.69444,.10903,0,.35139],103:[.19444,.44444,.01597,0,.575],104:[0,.69444,0,0,.63889],105:[0,.69444,0,0,.31944],106:[.19444,.69444,0,0,.35139],107:[0,.69444,0,0,.60694],108:[0,.69444,0,0,.31944],109:[0,.44444,0,0,.95833],110:[0,.44444,0,0,.63889],111:[0,.44444,0,0,.575],112:[.19444,.44444,0,0,.63889],113:[.19444,.44444,0,0,.60694],114:[0,.44444,0,0,.47361],115:[0,.44444,0,0,.45361],116:[0,.63492,0,0,.44722],117:[0,.44444,0,0,.63889],118:[0,.44444,.01597,0,.60694],119:[0,.44444,.01597,0,.83055],120:[0,.44444,0,0,.60694],121:[.19444,.44444,.01597,0,.60694],122:[0,.44444,0,0,.51111],123:[.25,.75,0,0,.575],124:[.25,.75,0,0,.31944],125:[.25,.75,0,0,.575],126:[.35,.34444,0,0,.575],160:[0,0,0,0,.25],163:[0,.69444,0,0,.86853],168:[0,.69444,0,0,.575],172:[0,.44444,0,0,.76666],176:[0,.69444,0,0,.86944],177:[.13333,.63333,0,0,.89444],184:[.17014,0,0,0,.51111],198:[0,.68611,0,0,1.04166],215:[.13333,.63333,0,0,.89444],216:[.04861,.73472,0,0,.89444],223:[0,.69444,0,0,.59722],230:[0,.44444,0,0,.83055],247:[.13333,.63333,0,0,.89444],248:[.09722,.54167,0,0,.575],305:[0,.44444,0,0,.31944],338:[0,.68611,0,0,1.16944],339:[0,.44444,0,0,.89444],567:[.19444,.44444,0,0,.35139],710:[0,.69444,0,0,.575],711:[0,.63194,0,0,.575],713:[0,.59611,0,0,.575],714:[0,.69444,0,0,.575],715:[0,.69444,0,0,.575],728:[0,.69444,0,0,.575],729:[0,.69444,0,0,.31944],730:[0,.69444,0,0,.86944],732:[0,.69444,0,0,.575],733:[0,.69444,0,0,.575],915:[0,.68611,0,0,.69166],916:[0,.68611,0,0,.95833],920:[0,.68611,0,0,.89444],923:[0,.68611,0,0,.80555],926:[0,.68611,0,0,.76666],928:[0,.68611,0,0,.9],931:[0,.68611,0,0,.83055],933:[0,.68611,0,0,.89444],934:[0,.68611,0,0,.83055],936:[0,.68611,0,0,.89444],937:[0,.68611,0,0,.83055],8211:[0,.44444,.03194,0,.575],8212:[0,.44444,.03194,0,1.14999],8216:[0,.69444,0,0,.31944],8217:[0,.69444,0,0,.31944],8220:[0,.69444,0,0,.60278],8221:[0,.69444,0,0,.60278],8224:[.19444,.69444,0,0,.51111],8225:[.19444,.69444,0,0,.51111],8242:[0,.55556,0,0,.34444],8407:[0,.72444,.15486,0,.575],8463:[0,.69444,0,0,.66759],8465:[0,.69444,0,0,.83055],8467:[0,.69444,0,0,.47361],8472:[.19444,.44444,0,0,.74027],8476:[0,.69444,0,0,.83055],8501:[0,.69444,0,0,.70277],8592:[-.10889,.39111,0,0,1.14999],8593:[.19444,.69444,0,0,.575],8594:[-.10889,.39111,0,0,1.14999],8595:[.19444,.69444,0,0,.575],8596:[-.10889,.39111,0,0,1.14999],8597:[.25,.75,0,0,.575],8598:[.19444,.69444,0,0,1.14999],8599:[.19444,.69444,0,0,1.14999],8600:[.19444,.69444,0,0,1.14999],8601:[.19444,.69444,0,0,1.14999],8636:[-.10889,.39111,0,0,1.14999],8637:[-.10889,.39111,0,0,1.14999],8640:[-.10889,.39111,0,0,1.14999],8641:[-.10889,.39111,0,0,1.14999],8656:[-.10889,.39111,0,0,1.14999],8657:[.19444,.69444,0,0,.70277],8658:[-.10889,.39111,0,0,1.14999],8659:[.19444,.69444,0,0,.70277],8660:[-.10889,.39111,0,0,1.14999],8661:[.25,.75,0,0,.70277],8704:[0,.69444,0,0,.63889],8706:[0,.69444,.06389,0,.62847],8707:[0,.69444,0,0,.63889],8709:[.05556,.75,0,0,.575],8711:[0,.68611,0,0,.95833],8712:[.08556,.58556,0,0,.76666],8715:[.08556,.58556,0,0,.76666],8722:[.13333,.63333,0,0,.89444],8723:[.13333,.63333,0,0,.89444],8725:[.25,.75,0,0,.575],8726:[.25,.75,0,0,.575],8727:[-.02778,.47222,0,0,.575],8728:[-.02639,.47361,0,0,.575],8729:[-.02639,.47361,0,0,.575],8730:[.18,.82,0,0,.95833],8733:[0,.44444,0,0,.89444],8734:[0,.44444,0,0,1.14999],8736:[0,.69224,0,0,.72222],8739:[.25,.75,0,0,.31944],8741:[.25,.75,0,0,.575],8743:[0,.55556,0,0,.76666],8744:[0,.55556,0,0,.76666],8745:[0,.55556,0,0,.76666],8746:[0,.55556,0,0,.76666],8747:[.19444,.69444,.12778,0,.56875],8764:[-.10889,.39111,0,0,.89444],8768:[.19444,.69444,0,0,.31944],8771:[.00222,.50222,0,0,.89444],8773:[.027,.638,0,0,.894],8776:[.02444,.52444,0,0,.89444],8781:[.00222,.50222,0,0,.89444],8801:[.00222,.50222,0,0,.89444],8804:[.19667,.69667,0,0,.89444],8805:[.19667,.69667,0,0,.89444],8810:[.08556,.58556,0,0,1.14999],8811:[.08556,.58556,0,0,1.14999],8826:[.08556,.58556,0,0,.89444],8827:[.08556,.58556,0,0,.89444],8834:[.08556,.58556,0,0,.89444],8835:[.08556,.58556,0,0,.89444],8838:[.19667,.69667,0,0,.89444],8839:[.19667,.69667,0,0,.89444],8846:[0,.55556,0,0,.76666],8849:[.19667,.69667,0,0,.89444],8850:[.19667,.69667,0,0,.89444],8851:[0,.55556,0,0,.76666],8852:[0,.55556,0,0,.76666],8853:[.13333,.63333,0,0,.89444],8854:[.13333,.63333,0,0,.89444],8855:[.13333,.63333,0,0,.89444],8856:[.13333,.63333,0,0,.89444],8857:[.13333,.63333,0,0,.89444],8866:[0,.69444,0,0,.70277],8867:[0,.69444,0,0,.70277],8868:[0,.69444,0,0,.89444],8869:[0,.69444,0,0,.89444],8900:[-.02639,.47361,0,0,.575],8901:[-.02639,.47361,0,0,.31944],8902:[-.02778,.47222,0,0,.575],8968:[.25,.75,0,0,.51111],8969:[.25,.75,0,0,.51111],8970:[.25,.75,0,0,.51111],8971:[.25,.75,0,0,.51111],8994:[-.13889,.36111,0,0,1.14999],8995:[-.13889,.36111,0,0,1.14999],9651:[.19444,.69444,0,0,1.02222],9657:[-.02778,.47222,0,0,.575],9661:[.19444,.69444,0,0,1.02222],9667:[-.02778,.47222,0,0,.575],9711:[.19444,.69444,0,0,1.14999],9824:[.12963,.69444,0,0,.89444],9825:[.12963,.69444,0,0,.89444],9826:[.12963,.69444,0,0,.89444],9827:[.12963,.69444,0,0,.89444],9837:[0,.75,0,0,.44722],9838:[.19444,.69444,0,0,.44722],9839:[.19444,.69444,0,0,.44722],10216:[.25,.75,0,0,.44722],10217:[.25,.75,0,0,.44722],10815:[0,.68611,0,0,.9],10927:[.19667,.69667,0,0,.89444],10928:[.19667,.69667,0,0,.89444],57376:[.19444,.69444,0,0,0]},"Main-BoldItalic":{32:[0,0,0,0,.25],33:[0,.69444,.11417,0,.38611],34:[0,.69444,.07939,0,.62055],35:[.19444,.69444,.06833,0,.94444],37:[.05556,.75,.12861,0,.94444],38:[0,.69444,.08528,0,.88555],39:[0,.69444,.12945,0,.35555],40:[.25,.75,.15806,0,.47333],41:[.25,.75,.03306,0,.47333],42:[0,.75,.14333,0,.59111],43:[.10333,.60333,.03306,0,.88555],44:[.19444,.14722,0,0,.35555],45:[0,.44444,.02611,0,.41444],46:[0,.14722,0,0,.35555],47:[.25,.75,.15806,0,.59111],48:[0,.64444,.13167,0,.59111],49:[0,.64444,.13167,0,.59111],50:[0,.64444,.13167,0,.59111],51:[0,.64444,.13167,0,.59111],52:[.19444,.64444,.13167,0,.59111],53:[0,.64444,.13167,0,.59111],54:[0,.64444,.13167,0,.59111],55:[.19444,.64444,.13167,0,.59111],56:[0,.64444,.13167,0,.59111],57:[0,.64444,.13167,0,.59111],58:[0,.44444,.06695,0,.35555],59:[.19444,.44444,.06695,0,.35555],61:[-.10889,.39111,.06833,0,.88555],63:[0,.69444,.11472,0,.59111],64:[0,.69444,.09208,0,.88555],65:[0,.68611,0,0,.86555],66:[0,.68611,.0992,0,.81666],67:[0,.68611,.14208,0,.82666],68:[0,.68611,.09062,0,.87555],69:[0,.68611,.11431,0,.75666],70:[0,.68611,.12903,0,.72722],71:[0,.68611,.07347,0,.89527],72:[0,.68611,.17208,0,.8961],73:[0,.68611,.15681,0,.47166],74:[0,.68611,.145,0,.61055],75:[0,.68611,.14208,0,.89499],76:[0,.68611,0,0,.69777],77:[0,.68611,.17208,0,1.07277],78:[0,.68611,.17208,0,.8961],79:[0,.68611,.09062,0,.85499],80:[0,.68611,.0992,0,.78721],81:[.19444,.68611,.09062,0,.85499],82:[0,.68611,.02559,0,.85944],83:[0,.68611,.11264,0,.64999],84:[0,.68611,.12903,0,.7961],85:[0,.68611,.17208,0,.88083],86:[0,.68611,.18625,0,.86555],87:[0,.68611,.18625,0,1.15999],88:[0,.68611,.15681,0,.86555],89:[0,.68611,.19803,0,.86555],90:[0,.68611,.14208,0,.70888],91:[.25,.75,.1875,0,.35611],93:[.25,.75,.09972,0,.35611],94:[0,.69444,.06709,0,.59111],95:[.31,.13444,.09811,0,.59111],97:[0,.44444,.09426,0,.59111],98:[0,.69444,.07861,0,.53222],99:[0,.44444,.05222,0,.53222],100:[0,.69444,.10861,0,.59111],101:[0,.44444,.085,0,.53222],102:[.19444,.69444,.21778,0,.4],103:[.19444,.44444,.105,0,.53222],104:[0,.69444,.09426,0,.59111],105:[0,.69326,.11387,0,.35555],106:[.19444,.69326,.1672,0,.35555],107:[0,.69444,.11111,0,.53222],108:[0,.69444,.10861,0,.29666],109:[0,.44444,.09426,0,.94444],110:[0,.44444,.09426,0,.64999],111:[0,.44444,.07861,0,.59111],112:[.19444,.44444,.07861,0,.59111],113:[.19444,.44444,.105,0,.53222],114:[0,.44444,.11111,0,.50167],115:[0,.44444,.08167,0,.48694],116:[0,.63492,.09639,0,.385],117:[0,.44444,.09426,0,.62055],118:[0,.44444,.11111,0,.53222],119:[0,.44444,.11111,0,.76777],120:[0,.44444,.12583,0,.56055],121:[.19444,.44444,.105,0,.56166],122:[0,.44444,.13889,0,.49055],126:[.35,.34444,.11472,0,.59111],160:[0,0,0,0,.25],168:[0,.69444,.11473,0,.59111],176:[0,.69444,0,0,.94888],184:[.17014,0,0,0,.53222],198:[0,.68611,.11431,0,1.02277],216:[.04861,.73472,.09062,0,.88555],223:[.19444,.69444,.09736,0,.665],230:[0,.44444,.085,0,.82666],248:[.09722,.54167,.09458,0,.59111],305:[0,.44444,.09426,0,.35555],338:[0,.68611,.11431,0,1.14054],339:[0,.44444,.085,0,.82666],567:[.19444,.44444,.04611,0,.385],710:[0,.69444,.06709,0,.59111],711:[0,.63194,.08271,0,.59111],713:[0,.59444,.10444,0,.59111],714:[0,.69444,.08528,0,.59111],715:[0,.69444,0,0,.59111],728:[0,.69444,.10333,0,.59111],729:[0,.69444,.12945,0,.35555],730:[0,.69444,0,0,.94888],732:[0,.69444,.11472,0,.59111],733:[0,.69444,.11472,0,.59111],915:[0,.68611,.12903,0,.69777],916:[0,.68611,0,0,.94444],920:[0,.68611,.09062,0,.88555],923:[0,.68611,0,0,.80666],926:[0,.68611,.15092,0,.76777],928:[0,.68611,.17208,0,.8961],931:[0,.68611,.11431,0,.82666],933:[0,.68611,.10778,0,.88555],934:[0,.68611,.05632,0,.82666],936:[0,.68611,.10778,0,.88555],937:[0,.68611,.0992,0,.82666],8211:[0,.44444,.09811,0,.59111],8212:[0,.44444,.09811,0,1.18221],8216:[0,.69444,.12945,0,.35555],8217:[0,.69444,.12945,0,.35555],8220:[0,.69444,.16772,0,.62055],8221:[0,.69444,.07939,0,.62055]},"Main-Italic":{32:[0,0,0,0,.25],33:[0,.69444,.12417,0,.30667],34:[0,.69444,.06961,0,.51444],35:[.19444,.69444,.06616,0,.81777],37:[.05556,.75,.13639,0,.81777],38:[0,.69444,.09694,0,.76666],39:[0,.69444,.12417,0,.30667],40:[.25,.75,.16194,0,.40889],41:[.25,.75,.03694,0,.40889],42:[0,.75,.14917,0,.51111],43:[.05667,.56167,.03694,0,.76666],44:[.19444,.10556,0,0,.30667],45:[0,.43056,.02826,0,.35778],46:[0,.10556,0,0,.30667],47:[.25,.75,.16194,0,.51111],48:[0,.64444,.13556,0,.51111],49:[0,.64444,.13556,0,.51111],50:[0,.64444,.13556,0,.51111],51:[0,.64444,.13556,0,.51111],52:[.19444,.64444,.13556,0,.51111],53:[0,.64444,.13556,0,.51111],54:[0,.64444,.13556,0,.51111],55:[.19444,.64444,.13556,0,.51111],56:[0,.64444,.13556,0,.51111],57:[0,.64444,.13556,0,.51111],58:[0,.43056,.0582,0,.30667],59:[.19444,.43056,.0582,0,.30667],61:[-.13313,.36687,.06616,0,.76666],63:[0,.69444,.1225,0,.51111],64:[0,.69444,.09597,0,.76666],65:[0,.68333,0,0,.74333],66:[0,.68333,.10257,0,.70389],67:[0,.68333,.14528,0,.71555],68:[0,.68333,.09403,0,.755],69:[0,.68333,.12028,0,.67833],70:[0,.68333,.13305,0,.65277],71:[0,.68333,.08722,0,.77361],72:[0,.68333,.16389,0,.74333],73:[0,.68333,.15806,0,.38555],74:[0,.68333,.14028,0,.525],75:[0,.68333,.14528,0,.76888],76:[0,.68333,0,0,.62722],77:[0,.68333,.16389,0,.89666],78:[0,.68333,.16389,0,.74333],79:[0,.68333,.09403,0,.76666],80:[0,.68333,.10257,0,.67833],81:[.19444,.68333,.09403,0,.76666],82:[0,.68333,.03868,0,.72944],83:[0,.68333,.11972,0,.56222],84:[0,.68333,.13305,0,.71555],85:[0,.68333,.16389,0,.74333],86:[0,.68333,.18361,0,.74333],87:[0,.68333,.18361,0,.99888],88:[0,.68333,.15806,0,.74333],89:[0,.68333,.19383,0,.74333],90:[0,.68333,.14528,0,.61333],91:[.25,.75,.1875,0,.30667],93:[.25,.75,.10528,0,.30667],94:[0,.69444,.06646,0,.51111],95:[.31,.12056,.09208,0,.51111],97:[0,.43056,.07671,0,.51111],98:[0,.69444,.06312,0,.46],99:[0,.43056,.05653,0,.46],100:[0,.69444,.10333,0,.51111],101:[0,.43056,.07514,0,.46],102:[.19444,.69444,.21194,0,.30667],103:[.19444,.43056,.08847,0,.46],104:[0,.69444,.07671,0,.51111],105:[0,.65536,.1019,0,.30667],106:[.19444,.65536,.14467,0,.30667],107:[0,.69444,.10764,0,.46],108:[0,.69444,.10333,0,.25555],109:[0,.43056,.07671,0,.81777],110:[0,.43056,.07671,0,.56222],111:[0,.43056,.06312,0,.51111],112:[.19444,.43056,.06312,0,.51111],113:[.19444,.43056,.08847,0,.46],114:[0,.43056,.10764,0,.42166],115:[0,.43056,.08208,0,.40889],116:[0,.61508,.09486,0,.33222],117:[0,.43056,.07671,0,.53666],118:[0,.43056,.10764,0,.46],119:[0,.43056,.10764,0,.66444],120:[0,.43056,.12042,0,.46389],121:[.19444,.43056,.08847,0,.48555],122:[0,.43056,.12292,0,.40889],126:[.35,.31786,.11585,0,.51111],160:[0,0,0,0,.25],168:[0,.66786,.10474,0,.51111],176:[0,.69444,0,0,.83129],184:[.17014,0,0,0,.46],198:[0,.68333,.12028,0,.88277],216:[.04861,.73194,.09403,0,.76666],223:[.19444,.69444,.10514,0,.53666],230:[0,.43056,.07514,0,.71555],248:[.09722,.52778,.09194,0,.51111],338:[0,.68333,.12028,0,.98499],339:[0,.43056,.07514,0,.71555],710:[0,.69444,.06646,0,.51111],711:[0,.62847,.08295,0,.51111],713:[0,.56167,.10333,0,.51111],714:[0,.69444,.09694,0,.51111],715:[0,.69444,0,0,.51111],728:[0,.69444,.10806,0,.51111],729:[0,.66786,.11752,0,.30667],730:[0,.69444,0,0,.83129],732:[0,.66786,.11585,0,.51111],733:[0,.69444,.1225,0,.51111],915:[0,.68333,.13305,0,.62722],916:[0,.68333,0,0,.81777],920:[0,.68333,.09403,0,.76666],923:[0,.68333,0,0,.69222],926:[0,.68333,.15294,0,.66444],928:[0,.68333,.16389,0,.74333],931:[0,.68333,.12028,0,.71555],933:[0,.68333,.11111,0,.76666],934:[0,.68333,.05986,0,.71555],936:[0,.68333,.11111,0,.76666],937:[0,.68333,.10257,0,.71555],8211:[0,.43056,.09208,0,.51111],8212:[0,.43056,.09208,0,1.02222],8216:[0,.69444,.12417,0,.30667],8217:[0,.69444,.12417,0,.30667],8220:[0,.69444,.1685,0,.51444],8221:[0,.69444,.06961,0,.51444],8463:[0,.68889,0,0,.54028]},"Main-Regular":{32:[0,0,0,0,.25],33:[0,.69444,0,0,.27778],34:[0,.69444,0,0,.5],35:[.19444,.69444,0,0,.83334],36:[.05556,.75,0,0,.5],37:[.05556,.75,0,0,.83334],38:[0,.69444,0,0,.77778],39:[0,.69444,0,0,.27778],40:[.25,.75,0,0,.38889],41:[.25,.75,0,0,.38889],42:[0,.75,0,0,.5],43:[.08333,.58333,0,0,.77778],44:[.19444,.10556,0,0,.27778],45:[0,.43056,0,0,.33333],46:[0,.10556,0,0,.27778],47:[.25,.75,0,0,.5],48:[0,.64444,0,0,.5],49:[0,.64444,0,0,.5],50:[0,.64444,0,0,.5],51:[0,.64444,0,0,.5],52:[0,.64444,0,0,.5],53:[0,.64444,0,0,.5],54:[0,.64444,0,0,.5],55:[0,.64444,0,0,.5],56:[0,.64444,0,0,.5],57:[0,.64444,0,0,.5],58:[0,.43056,0,0,.27778],59:[.19444,.43056,0,0,.27778],60:[.0391,.5391,0,0,.77778],61:[-.13313,.36687,0,0,.77778],62:[.0391,.5391,0,0,.77778],63:[0,.69444,0,0,.47222],64:[0,.69444,0,0,.77778],65:[0,.68333,0,0,.75],66:[0,.68333,0,0,.70834],67:[0,.68333,0,0,.72222],68:[0,.68333,0,0,.76389],69:[0,.68333,0,0,.68056],70:[0,.68333,0,0,.65278],71:[0,.68333,0,0,.78472],72:[0,.68333,0,0,.75],73:[0,.68333,0,0,.36111],74:[0,.68333,0,0,.51389],75:[0,.68333,0,0,.77778],76:[0,.68333,0,0,.625],77:[0,.68333,0,0,.91667],78:[0,.68333,0,0,.75],79:[0,.68333,0,0,.77778],80:[0,.68333,0,0,.68056],81:[.19444,.68333,0,0,.77778],82:[0,.68333,0,0,.73611],83:[0,.68333,0,0,.55556],84:[0,.68333,0,0,.72222],85:[0,.68333,0,0,.75],86:[0,.68333,.01389,0,.75],87:[0,.68333,.01389,0,1.02778],88:[0,.68333,0,0,.75],89:[0,.68333,.025,0,.75],90:[0,.68333,0,0,.61111],91:[.25,.75,0,0,.27778],92:[.25,.75,0,0,.5],93:[.25,.75,0,0,.27778],94:[0,.69444,0,0,.5],95:[.31,.12056,.02778,0,.5],97:[0,.43056,0,0,.5],98:[0,.69444,0,0,.55556],99:[0,.43056,0,0,.44445],100:[0,.69444,0,0,.55556],101:[0,.43056,0,0,.44445],102:[0,.69444,.07778,0,.30556],103:[.19444,.43056,.01389,0,.5],104:[0,.69444,0,0,.55556],105:[0,.66786,0,0,.27778],106:[.19444,.66786,0,0,.30556],107:[0,.69444,0,0,.52778],108:[0,.69444,0,0,.27778],109:[0,.43056,0,0,.83334],110:[0,.43056,0,0,.55556],111:[0,.43056,0,0,.5],112:[.19444,.43056,0,0,.55556],113:[.19444,.43056,0,0,.52778],114:[0,.43056,0,0,.39167],115:[0,.43056,0,0,.39445],116:[0,.61508,0,0,.38889],117:[0,.43056,0,0,.55556],118:[0,.43056,.01389,0,.52778],119:[0,.43056,.01389,0,.72222],120:[0,.43056,0,0,.52778],121:[.19444,.43056,.01389,0,.52778],122:[0,.43056,0,0,.44445],123:[.25,.75,0,0,.5],124:[.25,.75,0,0,.27778],125:[.25,.75,0,0,.5],126:[.35,.31786,0,0,.5],160:[0,0,0,0,.25],163:[0,.69444,0,0,.76909],167:[.19444,.69444,0,0,.44445],168:[0,.66786,0,0,.5],172:[0,.43056,0,0,.66667],176:[0,.69444,0,0,.75],177:[.08333,.58333,0,0,.77778],182:[.19444,.69444,0,0,.61111],184:[.17014,0,0,0,.44445],198:[0,.68333,0,0,.90278],215:[.08333,.58333,0,0,.77778],216:[.04861,.73194,0,0,.77778],223:[0,.69444,0,0,.5],230:[0,.43056,0,0,.72222],247:[.08333,.58333,0,0,.77778],248:[.09722,.52778,0,0,.5],305:[0,.43056,0,0,.27778],338:[0,.68333,0,0,1.01389],339:[0,.43056,0,0,.77778],567:[.19444,.43056,0,0,.30556],710:[0,.69444,0,0,.5],711:[0,.62847,0,0,.5],713:[0,.56778,0,0,.5],714:[0,.69444,0,0,.5],715:[0,.69444,0,0,.5],728:[0,.69444,0,0,.5],729:[0,.66786,0,0,.27778],730:[0,.69444,0,0,.75],732:[0,.66786,0,0,.5],733:[0,.69444,0,0,.5],915:[0,.68333,0,0,.625],916:[0,.68333,0,0,.83334],920:[0,.68333,0,0,.77778],923:[0,.68333,0,0,.69445],926:[0,.68333,0,0,.66667],928:[0,.68333,0,0,.75],931:[0,.68333,0,0,.72222],933:[0,.68333,0,0,.77778],934:[0,.68333,0,0,.72222],936:[0,.68333,0,0,.77778],937:[0,.68333,0,0,.72222],8211:[0,.43056,.02778,0,.5],8212:[0,.43056,.02778,0,1],8216:[0,.69444,0,0,.27778],8217:[0,.69444,0,0,.27778],8220:[0,.69444,0,0,.5],8221:[0,.69444,0,0,.5],8224:[.19444,.69444,0,0,.44445],8225:[.19444,.69444,0,0,.44445],8230:[0,.123,0,0,1.172],8242:[0,.55556,0,0,.275],8407:[0,.71444,.15382,0,.5],8463:[0,.68889,0,0,.54028],8465:[0,.69444,0,0,.72222],8467:[0,.69444,0,.11111,.41667],8472:[.19444,.43056,0,.11111,.63646],8476:[0,.69444,0,0,.72222],8501:[0,.69444,0,0,.61111],8592:[-.13313,.36687,0,0,1],8593:[.19444,.69444,0,0,.5],8594:[-.13313,.36687,0,0,1],8595:[.19444,.69444,0,0,.5],8596:[-.13313,.36687,0,0,1],8597:[.25,.75,0,0,.5],8598:[.19444,.69444,0,0,1],8599:[.19444,.69444,0,0,1],8600:[.19444,.69444,0,0,1],8601:[.19444,.69444,0,0,1],8614:[.011,.511,0,0,1],8617:[.011,.511,0,0,1.126],8618:[.011,.511,0,0,1.126],8636:[-.13313,.36687,0,0,1],8637:[-.13313,.36687,0,0,1],8640:[-.13313,.36687,0,0,1],8641:[-.13313,.36687,0,0,1],8652:[.011,.671,0,0,1],8656:[-.13313,.36687,0,0,1],8657:[.19444,.69444,0,0,.61111],8658:[-.13313,.36687,0,0,1],8659:[.19444,.69444,0,0,.61111],8660:[-.13313,.36687,0,0,1],8661:[.25,.75,0,0,.61111],8704:[0,.69444,0,0,.55556],8706:[0,.69444,.05556,.08334,.5309],8707:[0,.69444,0,0,.55556],8709:[.05556,.75,0,0,.5],8711:[0,.68333,0,0,.83334],8712:[.0391,.5391,0,0,.66667],8715:[.0391,.5391,0,0,.66667],8722:[.08333,.58333,0,0,.77778],8723:[.08333,.58333,0,0,.77778],8725:[.25,.75,0,0,.5],8726:[.25,.75,0,0,.5],8727:[-.03472,.46528,0,0,.5],8728:[-.05555,.44445,0,0,.5],8729:[-.05555,.44445,0,0,.5],8730:[.2,.8,0,0,.83334],8733:[0,.43056,0,0,.77778],8734:[0,.43056,0,0,1],8736:[0,.69224,0,0,.72222],8739:[.25,.75,0,0,.27778],8741:[.25,.75,0,0,.5],8743:[0,.55556,0,0,.66667],8744:[0,.55556,0,0,.66667],8745:[0,.55556,0,0,.66667],8746:[0,.55556,0,0,.66667],8747:[.19444,.69444,.11111,0,.41667],8764:[-.13313,.36687,0,0,.77778],8768:[.19444,.69444,0,0,.27778],8771:[-.03625,.46375,0,0,.77778],8773:[-.022,.589,0,0,.778],8776:[-.01688,.48312,0,0,.77778],8781:[-.03625,.46375,0,0,.77778],8784:[-.133,.673,0,0,.778],8801:[-.03625,.46375,0,0,.77778],8804:[.13597,.63597,0,0,.77778],8805:[.13597,.63597,0,0,.77778],8810:[.0391,.5391,0,0,1],8811:[.0391,.5391,0,0,1],8826:[.0391,.5391,0,0,.77778],8827:[.0391,.5391,0,0,.77778],8834:[.0391,.5391,0,0,.77778],8835:[.0391,.5391,0,0,.77778],8838:[.13597,.63597,0,0,.77778],8839:[.13597,.63597,0,0,.77778],8846:[0,.55556,0,0,.66667],8849:[.13597,.63597,0,0,.77778],8850:[.13597,.63597,0,0,.77778],8851:[0,.55556,0,0,.66667],8852:[0,.55556,0,0,.66667],8853:[.08333,.58333,0,0,.77778],8854:[.08333,.58333,0,0,.77778],8855:[.08333,.58333,0,0,.77778],8856:[.08333,.58333,0,0,.77778],8857:[.08333,.58333,0,0,.77778],8866:[0,.69444,0,0,.61111],8867:[0,.69444,0,0,.61111],8868:[0,.69444,0,0,.77778],8869:[0,.69444,0,0,.77778],8872:[.249,.75,0,0,.867],8900:[-.05555,.44445,0,0,.5],8901:[-.05555,.44445,0,0,.27778],8902:[-.03472,.46528,0,0,.5],8904:[.005,.505,0,0,.9],8942:[.03,.903,0,0,.278],8943:[-.19,.313,0,0,1.172],8945:[-.1,.823,0,0,1.282],8968:[.25,.75,0,0,.44445],8969:[.25,.75,0,0,.44445],8970:[.25,.75,0,0,.44445],8971:[.25,.75,0,0,.44445],8994:[-.14236,.35764,0,0,1],8995:[-.14236,.35764,0,0,1],9136:[.244,.744,0,0,.412],9137:[.244,.745,0,0,.412],9651:[.19444,.69444,0,0,.88889],9657:[-.03472,.46528,0,0,.5],9661:[.19444,.69444,0,0,.88889],9667:[-.03472,.46528,0,0,.5],9711:[.19444,.69444,0,0,1],9824:[.12963,.69444,0,0,.77778],9825:[.12963,.69444,0,0,.77778],9826:[.12963,.69444,0,0,.77778],9827:[.12963,.69444,0,0,.77778],9837:[0,.75,0,0,.38889],9838:[.19444,.69444,0,0,.38889],9839:[.19444,.69444,0,0,.38889],10216:[.25,.75,0,0,.38889],10217:[.25,.75,0,0,.38889],10222:[.244,.744,0,0,.412],10223:[.244,.745,0,0,.412],10229:[.011,.511,0,0,1.609],10230:[.011,.511,0,0,1.638],10231:[.011,.511,0,0,1.859],10232:[.024,.525,0,0,1.609],10233:[.024,.525,0,0,1.638],10234:[.024,.525,0,0,1.858],10236:[.011,.511,0,0,1.638],10815:[0,.68333,0,0,.75],10927:[.13597,.63597,0,0,.77778],10928:[.13597,.63597,0,0,.77778],57376:[.19444,.69444,0,0,0]},"Math-BoldItalic":{32:[0,0,0,0,.25],48:[0,.44444,0,0,.575],49:[0,.44444,0,0,.575],50:[0,.44444,0,0,.575],51:[.19444,.44444,0,0,.575],52:[.19444,.44444,0,0,.575],53:[.19444,.44444,0,0,.575],54:[0,.64444,0,0,.575],55:[.19444,.44444,0,0,.575],56:[0,.64444,0,0,.575],57:[.19444,.44444,0,0,.575],65:[0,.68611,0,0,.86944],66:[0,.68611,.04835,0,.8664],67:[0,.68611,.06979,0,.81694],68:[0,.68611,.03194,0,.93812],69:[0,.68611,.05451,0,.81007],70:[0,.68611,.15972,0,.68889],71:[0,.68611,0,0,.88673],72:[0,.68611,.08229,0,.98229],73:[0,.68611,.07778,0,.51111],74:[0,.68611,.10069,0,.63125],75:[0,.68611,.06979,0,.97118],76:[0,.68611,0,0,.75555],77:[0,.68611,.11424,0,1.14201],78:[0,.68611,.11424,0,.95034],79:[0,.68611,.03194,0,.83666],80:[0,.68611,.15972,0,.72309],81:[.19444,.68611,0,0,.86861],82:[0,.68611,.00421,0,.87235],83:[0,.68611,.05382,0,.69271],84:[0,.68611,.15972,0,.63663],85:[0,.68611,.11424,0,.80027],86:[0,.68611,.25555,0,.67778],87:[0,.68611,.15972,0,1.09305],88:[0,.68611,.07778,0,.94722],89:[0,.68611,.25555,0,.67458],90:[0,.68611,.06979,0,.77257],97:[0,.44444,0,0,.63287],98:[0,.69444,0,0,.52083],99:[0,.44444,0,0,.51342],100:[0,.69444,0,0,.60972],101:[0,.44444,0,0,.55361],102:[.19444,.69444,.11042,0,.56806],103:[.19444,.44444,.03704,0,.5449],104:[0,.69444,0,0,.66759],105:[0,.69326,0,0,.4048],106:[.19444,.69326,.0622,0,.47083],107:[0,.69444,.01852,0,.6037],108:[0,.69444,.0088,0,.34815],109:[0,.44444,0,0,1.0324],110:[0,.44444,0,0,.71296],111:[0,.44444,0,0,.58472],112:[.19444,.44444,0,0,.60092],113:[.19444,.44444,.03704,0,.54213],114:[0,.44444,.03194,0,.5287],115:[0,.44444,0,0,.53125],116:[0,.63492,0,0,.41528],117:[0,.44444,0,0,.68102],118:[0,.44444,.03704,0,.56666],119:[0,.44444,.02778,0,.83148],120:[0,.44444,0,0,.65903],121:[.19444,.44444,.03704,0,.59028],122:[0,.44444,.04213,0,.55509],160:[0,0,0,0,.25],915:[0,.68611,.15972,0,.65694],916:[0,.68611,0,0,.95833],920:[0,.68611,.03194,0,.86722],923:[0,.68611,0,0,.80555],926:[0,.68611,.07458,0,.84125],928:[0,.68611,.08229,0,.98229],931:[0,.68611,.05451,0,.88507],933:[0,.68611,.15972,0,.67083],934:[0,.68611,0,0,.76666],936:[0,.68611,.11653,0,.71402],937:[0,.68611,.04835,0,.8789],945:[0,.44444,0,0,.76064],946:[.19444,.69444,.03403,0,.65972],947:[.19444,.44444,.06389,0,.59003],948:[0,.69444,.03819,0,.52222],949:[0,.44444,0,0,.52882],950:[.19444,.69444,.06215,0,.50833],951:[.19444,.44444,.03704,0,.6],952:[0,.69444,.03194,0,.5618],953:[0,.44444,0,0,.41204],954:[0,.44444,0,0,.66759],955:[0,.69444,0,0,.67083],956:[.19444,.44444,0,0,.70787],957:[0,.44444,.06898,0,.57685],958:[.19444,.69444,.03021,0,.50833],959:[0,.44444,0,0,.58472],960:[0,.44444,.03704,0,.68241],961:[.19444,.44444,0,0,.6118],962:[.09722,.44444,.07917,0,.42361],963:[0,.44444,.03704,0,.68588],964:[0,.44444,.13472,0,.52083],965:[0,.44444,.03704,0,.63055],966:[.19444,.44444,0,0,.74722],967:[.19444,.44444,0,0,.71805],968:[.19444,.69444,.03704,0,.75833],969:[0,.44444,.03704,0,.71782],977:[0,.69444,0,0,.69155],981:[.19444,.69444,0,0,.7125],982:[0,.44444,.03194,0,.975],1009:[.19444,.44444,0,0,.6118],1013:[0,.44444,0,0,.48333],57649:[0,.44444,0,0,.39352],57911:[.19444,.44444,0,0,.43889]},"Math-Italic":{32:[0,0,0,0,.25],48:[0,.43056,0,0,.5],49:[0,.43056,0,0,.5],50:[0,.43056,0,0,.5],51:[.19444,.43056,0,0,.5],52:[.19444,.43056,0,0,.5],53:[.19444,.43056,0,0,.5],54:[0,.64444,0,0,.5],55:[.19444,.43056,0,0,.5],56:[0,.64444,0,0,.5],57:[.19444,.43056,0,0,.5],65:[0,.68333,0,.13889,.75],66:[0,.68333,.05017,.08334,.75851],67:[0,.68333,.07153,.08334,.71472],68:[0,.68333,.02778,.05556,.82792],69:[0,.68333,.05764,.08334,.7382],70:[0,.68333,.13889,.08334,.64306],71:[0,.68333,0,.08334,.78625],72:[0,.68333,.08125,.05556,.83125],73:[0,.68333,.07847,.11111,.43958],74:[0,.68333,.09618,.16667,.55451],75:[0,.68333,.07153,.05556,.84931],76:[0,.68333,0,.02778,.68056],77:[0,.68333,.10903,.08334,.97014],78:[0,.68333,.10903,.08334,.80347],79:[0,.68333,.02778,.08334,.76278],80:[0,.68333,.13889,.08334,.64201],81:[.19444,.68333,0,.08334,.79056],82:[0,.68333,.00773,.08334,.75929],83:[0,.68333,.05764,.08334,.6132],84:[0,.68333,.13889,.08334,.58438],85:[0,.68333,.10903,.02778,.68278],86:[0,.68333,.22222,0,.58333],87:[0,.68333,.13889,0,.94445],88:[0,.68333,.07847,.08334,.82847],89:[0,.68333,.22222,0,.58056],90:[0,.68333,.07153,.08334,.68264],97:[0,.43056,0,0,.52859],98:[0,.69444,0,0,.42917],99:[0,.43056,0,.05556,.43276],100:[0,.69444,0,.16667,.52049],101:[0,.43056,0,.05556,.46563],102:[.19444,.69444,.10764,.16667,.48959],103:[.19444,.43056,.03588,.02778,.47697],104:[0,.69444,0,0,.57616],105:[0,.65952,0,0,.34451],106:[.19444,.65952,.05724,0,.41181],107:[0,.69444,.03148,0,.5206],108:[0,.69444,.01968,.08334,.29838],109:[0,.43056,0,0,.87801],110:[0,.43056,0,0,.60023],111:[0,.43056,0,.05556,.48472],112:[.19444,.43056,0,.08334,.50313],113:[.19444,.43056,.03588,.08334,.44641],114:[0,.43056,.02778,.05556,.45116],115:[0,.43056,0,.05556,.46875],116:[0,.61508,0,.08334,.36111],117:[0,.43056,0,.02778,.57246],118:[0,.43056,.03588,.02778,.48472],119:[0,.43056,.02691,.08334,.71592],120:[0,.43056,0,.02778,.57153],121:[.19444,.43056,.03588,.05556,.49028],122:[0,.43056,.04398,.05556,.46505],160:[0,0,0,0,.25],915:[0,.68333,.13889,.08334,.61528],916:[0,.68333,0,.16667,.83334],920:[0,.68333,.02778,.08334,.76278],923:[0,.68333,0,.16667,.69445],926:[0,.68333,.07569,.08334,.74236],928:[0,.68333,.08125,.05556,.83125],931:[0,.68333,.05764,.08334,.77986],933:[0,.68333,.13889,.05556,.58333],934:[0,.68333,0,.08334,.66667],936:[0,.68333,.11,.05556,.61222],937:[0,.68333,.05017,.08334,.7724],945:[0,.43056,.0037,.02778,.6397],946:[.19444,.69444,.05278,.08334,.56563],947:[.19444,.43056,.05556,0,.51773],948:[0,.69444,.03785,.05556,.44444],949:[0,.43056,0,.08334,.46632],950:[.19444,.69444,.07378,.08334,.4375],951:[.19444,.43056,.03588,.05556,.49653],952:[0,.69444,.02778,.08334,.46944],953:[0,.43056,0,.05556,.35394],954:[0,.43056,0,0,.57616],955:[0,.69444,0,0,.58334],956:[.19444,.43056,0,.02778,.60255],957:[0,.43056,.06366,.02778,.49398],958:[.19444,.69444,.04601,.11111,.4375],959:[0,.43056,0,.05556,.48472],960:[0,.43056,.03588,0,.57003],961:[.19444,.43056,0,.08334,.51702],962:[.09722,.43056,.07986,.08334,.36285],963:[0,.43056,.03588,0,.57141],964:[0,.43056,.1132,.02778,.43715],965:[0,.43056,.03588,.02778,.54028],966:[.19444,.43056,0,.08334,.65417],967:[.19444,.43056,0,.05556,.62569],968:[.19444,.69444,.03588,.11111,.65139],969:[0,.43056,.03588,0,.62245],977:[0,.69444,0,.08334,.59144],981:[.19444,.69444,0,.08334,.59583],982:[0,.43056,.02778,0,.82813],1009:[.19444,.43056,0,.08334,.51702],1013:[0,.43056,0,.05556,.4059],57649:[0,.43056,0,.02778,.32246],57911:[.19444,.43056,0,.08334,.38403]},"SansSerif-Bold":{32:[0,0,0,0,.25],33:[0,.69444,0,0,.36667],34:[0,.69444,0,0,.55834],35:[.19444,.69444,0,0,.91667],36:[.05556,.75,0,0,.55],37:[.05556,.75,0,0,1.02912],38:[0,.69444,0,0,.83056],39:[0,.69444,0,0,.30556],40:[.25,.75,0,0,.42778],41:[.25,.75,0,0,.42778],42:[0,.75,0,0,.55],43:[.11667,.61667,0,0,.85556],44:[.10556,.13056,0,0,.30556],45:[0,.45833,0,0,.36667],46:[0,.13056,0,0,.30556],47:[.25,.75,0,0,.55],48:[0,.69444,0,0,.55],49:[0,.69444,0,0,.55],50:[0,.69444,0,0,.55],51:[0,.69444,0,0,.55],52:[0,.69444,0,0,.55],53:[0,.69444,0,0,.55],54:[0,.69444,0,0,.55],55:[0,.69444,0,0,.55],56:[0,.69444,0,0,.55],57:[0,.69444,0,0,.55],58:[0,.45833,0,0,.30556],59:[.10556,.45833,0,0,.30556],61:[-.09375,.40625,0,0,.85556],63:[0,.69444,0,0,.51945],64:[0,.69444,0,0,.73334],65:[0,.69444,0,0,.73334],66:[0,.69444,0,0,.73334],67:[0,.69444,0,0,.70278],68:[0,.69444,0,0,.79445],69:[0,.69444,0,0,.64167],70:[0,.69444,0,0,.61111],71:[0,.69444,0,0,.73334],72:[0,.69444,0,0,.79445],73:[0,.69444,0,0,.33056],74:[0,.69444,0,0,.51945],75:[0,.69444,0,0,.76389],76:[0,.69444,0,0,.58056],77:[0,.69444,0,0,.97778],78:[0,.69444,0,0,.79445],79:[0,.69444,0,0,.79445],80:[0,.69444,0,0,.70278],81:[.10556,.69444,0,0,.79445],82:[0,.69444,0,0,.70278],83:[0,.69444,0,0,.61111],84:[0,.69444,0,0,.73334],85:[0,.69444,0,0,.76389],86:[0,.69444,.01528,0,.73334],87:[0,.69444,.01528,0,1.03889],88:[0,.69444,0,0,.73334],89:[0,.69444,.0275,0,.73334],90:[0,.69444,0,0,.67223],91:[.25,.75,0,0,.34306],93:[.25,.75,0,0,.34306],94:[0,.69444,0,0,.55],95:[.35,.10833,.03056,0,.55],97:[0,.45833,0,0,.525],98:[0,.69444,0,0,.56111],99:[0,.45833,0,0,.48889],100:[0,.69444,0,0,.56111],101:[0,.45833,0,0,.51111],102:[0,.69444,.07639,0,.33611],103:[.19444,.45833,.01528,0,.55],104:[0,.69444,0,0,.56111],105:[0,.69444,0,0,.25556],106:[.19444,.69444,0,0,.28611],107:[0,.69444,0,0,.53056],108:[0,.69444,0,0,.25556],109:[0,.45833,0,0,.86667],110:[0,.45833,0,0,.56111],111:[0,.45833,0,0,.55],112:[.19444,.45833,0,0,.56111],113:[.19444,.45833,0,0,.56111],114:[0,.45833,.01528,0,.37222],115:[0,.45833,0,0,.42167],116:[0,.58929,0,0,.40417],117:[0,.45833,0,0,.56111],118:[0,.45833,.01528,0,.5],119:[0,.45833,.01528,0,.74445],120:[0,.45833,0,0,.5],121:[.19444,.45833,.01528,0,.5],122:[0,.45833,0,0,.47639],126:[.35,.34444,0,0,.55],160:[0,0,0,0,.25],168:[0,.69444,0,0,.55],176:[0,.69444,0,0,.73334],180:[0,.69444,0,0,.55],184:[.17014,0,0,0,.48889],305:[0,.45833,0,0,.25556],567:[.19444,.45833,0,0,.28611],710:[0,.69444,0,0,.55],711:[0,.63542,0,0,.55],713:[0,.63778,0,0,.55],728:[0,.69444,0,0,.55],729:[0,.69444,0,0,.30556],730:[0,.69444,0,0,.73334],732:[0,.69444,0,0,.55],733:[0,.69444,0,0,.55],915:[0,.69444,0,0,.58056],916:[0,.69444,0,0,.91667],920:[0,.69444,0,0,.85556],923:[0,.69444,0,0,.67223],926:[0,.69444,0,0,.73334],928:[0,.69444,0,0,.79445],931:[0,.69444,0,0,.79445],933:[0,.69444,0,0,.85556],934:[0,.69444,0,0,.79445],936:[0,.69444,0,0,.85556],937:[0,.69444,0,0,.79445],8211:[0,.45833,.03056,0,.55],8212:[0,.45833,.03056,0,1.10001],8216:[0,.69444,0,0,.30556],8217:[0,.69444,0,0,.30556],8220:[0,.69444,0,0,.55834],8221:[0,.69444,0,0,.55834]},"SansSerif-Italic":{32:[0,0,0,0,.25],33:[0,.69444,.05733,0,.31945],34:[0,.69444,.00316,0,.5],35:[.19444,.69444,.05087,0,.83334],36:[.05556,.75,.11156,0,.5],37:[.05556,.75,.03126,0,.83334],38:[0,.69444,.03058,0,.75834],39:[0,.69444,.07816,0,.27778],40:[.25,.75,.13164,0,.38889],41:[.25,.75,.02536,0,.38889],42:[0,.75,.11775,0,.5],43:[.08333,.58333,.02536,0,.77778],44:[.125,.08333,0,0,.27778],45:[0,.44444,.01946,0,.33333],46:[0,.08333,0,0,.27778],47:[.25,.75,.13164,0,.5],48:[0,.65556,.11156,0,.5],49:[0,.65556,.11156,0,.5],50:[0,.65556,.11156,0,.5],51:[0,.65556,.11156,0,.5],52:[0,.65556,.11156,0,.5],53:[0,.65556,.11156,0,.5],54:[0,.65556,.11156,0,.5],55:[0,.65556,.11156,0,.5],56:[0,.65556,.11156,0,.5],57:[0,.65556,.11156,0,.5],58:[0,.44444,.02502,0,.27778],59:[.125,.44444,.02502,0,.27778],61:[-.13,.37,.05087,0,.77778],63:[0,.69444,.11809,0,.47222],64:[0,.69444,.07555,0,.66667],65:[0,.69444,0,0,.66667],66:[0,.69444,.08293,0,.66667],67:[0,.69444,.11983,0,.63889],68:[0,.69444,.07555,0,.72223],69:[0,.69444,.11983,0,.59722],70:[0,.69444,.13372,0,.56945],71:[0,.69444,.11983,0,.66667],72:[0,.69444,.08094,0,.70834],73:[0,.69444,.13372,0,.27778],74:[0,.69444,.08094,0,.47222],75:[0,.69444,.11983,0,.69445],76:[0,.69444,0,0,.54167],77:[0,.69444,.08094,0,.875],78:[0,.69444,.08094,0,.70834],79:[0,.69444,.07555,0,.73611],80:[0,.69444,.08293,0,.63889],81:[.125,.69444,.07555,0,.73611],82:[0,.69444,.08293,0,.64584],83:[0,.69444,.09205,0,.55556],84:[0,.69444,.13372,0,.68056],85:[0,.69444,.08094,0,.6875],86:[0,.69444,.1615,0,.66667],87:[0,.69444,.1615,0,.94445],88:[0,.69444,.13372,0,.66667],89:[0,.69444,.17261,0,.66667],90:[0,.69444,.11983,0,.61111],91:[.25,.75,.15942,0,.28889],93:[.25,.75,.08719,0,.28889],94:[0,.69444,.0799,0,.5],95:[.35,.09444,.08616,0,.5],97:[0,.44444,.00981,0,.48056],98:[0,.69444,.03057,0,.51667],99:[0,.44444,.08336,0,.44445],100:[0,.69444,.09483,0,.51667],101:[0,.44444,.06778,0,.44445],102:[0,.69444,.21705,0,.30556],103:[.19444,.44444,.10836,0,.5],104:[0,.69444,.01778,0,.51667],105:[0,.67937,.09718,0,.23889],106:[.19444,.67937,.09162,0,.26667],107:[0,.69444,.08336,0,.48889],108:[0,.69444,.09483,0,.23889],109:[0,.44444,.01778,0,.79445],110:[0,.44444,.01778,0,.51667],111:[0,.44444,.06613,0,.5],112:[.19444,.44444,.0389,0,.51667],113:[.19444,.44444,.04169,0,.51667],114:[0,.44444,.10836,0,.34167],115:[0,.44444,.0778,0,.38333],116:[0,.57143,.07225,0,.36111],117:[0,.44444,.04169,0,.51667],118:[0,.44444,.10836,0,.46111],119:[0,.44444,.10836,0,.68334],120:[0,.44444,.09169,0,.46111],121:[.19444,.44444,.10836,0,.46111],122:[0,.44444,.08752,0,.43472],126:[.35,.32659,.08826,0,.5],160:[0,0,0,0,.25],168:[0,.67937,.06385,0,.5],176:[0,.69444,0,0,.73752],184:[.17014,0,0,0,.44445],305:[0,.44444,.04169,0,.23889],567:[.19444,.44444,.04169,0,.26667],710:[0,.69444,.0799,0,.5],711:[0,.63194,.08432,0,.5],713:[0,.60889,.08776,0,.5],714:[0,.69444,.09205,0,.5],715:[0,.69444,0,0,.5],728:[0,.69444,.09483,0,.5],729:[0,.67937,.07774,0,.27778],730:[0,.69444,0,0,.73752],732:[0,.67659,.08826,0,.5],733:[0,.69444,.09205,0,.5],915:[0,.69444,.13372,0,.54167],916:[0,.69444,0,0,.83334],920:[0,.69444,.07555,0,.77778],923:[0,.69444,0,0,.61111],926:[0,.69444,.12816,0,.66667],928:[0,.69444,.08094,0,.70834],931:[0,.69444,.11983,0,.72222],933:[0,.69444,.09031,0,.77778],934:[0,.69444,.04603,0,.72222],936:[0,.69444,.09031,0,.77778],937:[0,.69444,.08293,0,.72222],8211:[0,.44444,.08616,0,.5],8212:[0,.44444,.08616,0,1],8216:[0,.69444,.07816,0,.27778],8217:[0,.69444,.07816,0,.27778],8220:[0,.69444,.14205,0,.5],8221:[0,.69444,.00316,0,.5]},"SansSerif-Regular":{32:[0,0,0,0,.25],33:[0,.69444,0,0,.31945],34:[0,.69444,0,0,.5],35:[.19444,.69444,0,0,.83334],36:[.05556,.75,0,0,.5],37:[.05556,.75,0,0,.83334],38:[0,.69444,0,0,.75834],39:[0,.69444,0,0,.27778],40:[.25,.75,0,0,.38889],41:[.25,.75,0,0,.38889],42:[0,.75,0,0,.5],43:[.08333,.58333,0,0,.77778],44:[.125,.08333,0,0,.27778],45:[0,.44444,0,0,.33333],46:[0,.08333,0,0,.27778],47:[.25,.75,0,0,.5],48:[0,.65556,0,0,.5],49:[0,.65556,0,0,.5],50:[0,.65556,0,0,.5],51:[0,.65556,0,0,.5],52:[0,.65556,0,0,.5],53:[0,.65556,0,0,.5],54:[0,.65556,0,0,.5],55:[0,.65556,0,0,.5],56:[0,.65556,0,0,.5],57:[0,.65556,0,0,.5],58:[0,.44444,0,0,.27778],59:[.125,.44444,0,0,.27778],61:[-.13,.37,0,0,.77778],63:[0,.69444,0,0,.47222],64:[0,.69444,0,0,.66667],65:[0,.69444,0,0,.66667],66:[0,.69444,0,0,.66667],67:[0,.69444,0,0,.63889],68:[0,.69444,0,0,.72223],69:[0,.69444,0,0,.59722],70:[0,.69444,0,0,.56945],71:[0,.69444,0,0,.66667],72:[0,.69444,0,0,.70834],73:[0,.69444,0,0,.27778],74:[0,.69444,0,0,.47222],75:[0,.69444,0,0,.69445],76:[0,.69444,0,0,.54167],77:[0,.69444,0,0,.875],78:[0,.69444,0,0,.70834],79:[0,.69444,0,0,.73611],80:[0,.69444,0,0,.63889],81:[.125,.69444,0,0,.73611],82:[0,.69444,0,0,.64584],83:[0,.69444,0,0,.55556],84:[0,.69444,0,0,.68056],85:[0,.69444,0,0,.6875],86:[0,.69444,.01389,0,.66667],87:[0,.69444,.01389,0,.94445],88:[0,.69444,0,0,.66667],89:[0,.69444,.025,0,.66667],90:[0,.69444,0,0,.61111],91:[.25,.75,0,0,.28889],93:[.25,.75,0,0,.28889],94:[0,.69444,0,0,.5],95:[.35,.09444,.02778,0,.5],97:[0,.44444,0,0,.48056],98:[0,.69444,0,0,.51667],99:[0,.44444,0,0,.44445],100:[0,.69444,0,0,.51667],101:[0,.44444,0,0,.44445],102:[0,.69444,.06944,0,.30556],103:[.19444,.44444,.01389,0,.5],104:[0,.69444,0,0,.51667],105:[0,.67937,0,0,.23889],106:[.19444,.67937,0,0,.26667],107:[0,.69444,0,0,.48889],108:[0,.69444,0,0,.23889],109:[0,.44444,0,0,.79445],110:[0,.44444,0,0,.51667],111:[0,.44444,0,0,.5],112:[.19444,.44444,0,0,.51667],113:[.19444,.44444,0,0,.51667],114:[0,.44444,.01389,0,.34167],115:[0,.44444,0,0,.38333],116:[0,.57143,0,0,.36111],117:[0,.44444,0,0,.51667],118:[0,.44444,.01389,0,.46111],119:[0,.44444,.01389,0,.68334],120:[0,.44444,0,0,.46111],121:[.19444,.44444,.01389,0,.46111],122:[0,.44444,0,0,.43472],126:[.35,.32659,0,0,.5],160:[0,0,0,0,.25],168:[0,.67937,0,0,.5],176:[0,.69444,0,0,.66667],184:[.17014,0,0,0,.44445],305:[0,.44444,0,0,.23889],567:[.19444,.44444,0,0,.26667],710:[0,.69444,0,0,.5],711:[0,.63194,0,0,.5],713:[0,.60889,0,0,.5],714:[0,.69444,0,0,.5],715:[0,.69444,0,0,.5],728:[0,.69444,0,0,.5],729:[0,.67937,0,0,.27778],730:[0,.69444,0,0,.66667],732:[0,.67659,0,0,.5],733:[0,.69444,0,0,.5],915:[0,.69444,0,0,.54167],916:[0,.69444,0,0,.83334],920:[0,.69444,0,0,.77778],923:[0,.69444,0,0,.61111],926:[0,.69444,0,0,.66667],928:[0,.69444,0,0,.70834],931:[0,.69444,0,0,.72222],933:[0,.69444,0,0,.77778],934:[0,.69444,0,0,.72222],936:[0,.69444,0,0,.77778],937:[0,.69444,0,0,.72222],8211:[0,.44444,.02778,0,.5],8212:[0,.44444,.02778,0,1],8216:[0,.69444,0,0,.27778],8217:[0,.69444,0,0,.27778],8220:[0,.69444,0,0,.5],8221:[0,.69444,0,0,.5]},"Script-Regular":{32:[0,0,0,0,.25],65:[0,.7,.22925,0,.80253],66:[0,.7,.04087,0,.90757],67:[0,.7,.1689,0,.66619],68:[0,.7,.09371,0,.77443],69:[0,.7,.18583,0,.56162],70:[0,.7,.13634,0,.89544],71:[0,.7,.17322,0,.60961],72:[0,.7,.29694,0,.96919],73:[0,.7,.19189,0,.80907],74:[.27778,.7,.19189,0,1.05159],75:[0,.7,.31259,0,.91364],76:[0,.7,.19189,0,.87373],77:[0,.7,.15981,0,1.08031],78:[0,.7,.3525,0,.9015],79:[0,.7,.08078,0,.73787],80:[0,.7,.08078,0,1.01262],81:[0,.7,.03305,0,.88282],82:[0,.7,.06259,0,.85],83:[0,.7,.19189,0,.86767],84:[0,.7,.29087,0,.74697],85:[0,.7,.25815,0,.79996],86:[0,.7,.27523,0,.62204],87:[0,.7,.27523,0,.80532],88:[0,.7,.26006,0,.94445],89:[0,.7,.2939,0,.70961],90:[0,.7,.24037,0,.8212],160:[0,0,0,0,.25]},"Size1-Regular":{32:[0,0,0,0,.25],40:[.35001,.85,0,0,.45834],41:[.35001,.85,0,0,.45834],47:[.35001,.85,0,0,.57778],91:[.35001,.85,0,0,.41667],92:[.35001,.85,0,0,.57778],93:[.35001,.85,0,0,.41667],123:[.35001,.85,0,0,.58334],125:[.35001,.85,0,0,.58334],160:[0,0,0,0,.25],710:[0,.72222,0,0,.55556],732:[0,.72222,0,0,.55556],770:[0,.72222,0,0,.55556],771:[0,.72222,0,0,.55556],8214:[-99e-5,.601,0,0,.77778],8593:[1e-5,.6,0,0,.66667],8595:[1e-5,.6,0,0,.66667],8657:[1e-5,.6,0,0,.77778],8659:[1e-5,.6,0,0,.77778],8719:[.25001,.75,0,0,.94445],8720:[.25001,.75,0,0,.94445],8721:[.25001,.75,0,0,1.05556],8730:[.35001,.85,0,0,1],8739:[-.00599,.606,0,0,.33333],8741:[-.00599,.606,0,0,.55556],8747:[.30612,.805,.19445,0,.47222],8748:[.306,.805,.19445,0,.47222],8749:[.306,.805,.19445,0,.47222],8750:[.30612,.805,.19445,0,.47222],8896:[.25001,.75,0,0,.83334],8897:[.25001,.75,0,0,.83334],8898:[.25001,.75,0,0,.83334],8899:[.25001,.75,0,0,.83334],8968:[.35001,.85,0,0,.47222],8969:[.35001,.85,0,0,.47222],8970:[.35001,.85,0,0,.47222],8971:[.35001,.85,0,0,.47222],9168:[-99e-5,.601,0,0,.66667],10216:[.35001,.85,0,0,.47222],10217:[.35001,.85,0,0,.47222],10752:[.25001,.75,0,0,1.11111],10753:[.25001,.75,0,0,1.11111],10754:[.25001,.75,0,0,1.11111],10756:[.25001,.75,0,0,.83334],10758:[.25001,.75,0,0,.83334]},"Size2-Regular":{32:[0,0,0,0,.25],40:[.65002,1.15,0,0,.59722],41:[.65002,1.15,0,0,.59722],47:[.65002,1.15,0,0,.81111],91:[.65002,1.15,0,0,.47222],92:[.65002,1.15,0,0,.81111],93:[.65002,1.15,0,0,.47222],123:[.65002,1.15,0,0,.66667],125:[.65002,1.15,0,0,.66667],160:[0,0,0,0,.25],710:[0,.75,0,0,1],732:[0,.75,0,0,1],770:[0,.75,0,0,1],771:[0,.75,0,0,1],8719:[.55001,1.05,0,0,1.27778],8720:[.55001,1.05,0,0,1.27778],8721:[.55001,1.05,0,0,1.44445],8730:[.65002,1.15,0,0,1],8747:[.86225,1.36,.44445,0,.55556],8748:[.862,1.36,.44445,0,.55556],8749:[.862,1.36,.44445,0,.55556],8750:[.86225,1.36,.44445,0,.55556],8896:[.55001,1.05,0,0,1.11111],8897:[.55001,1.05,0,0,1.11111],8898:[.55001,1.05,0,0,1.11111],8899:[.55001,1.05,0,0,1.11111],8968:[.65002,1.15,0,0,.52778],8969:[.65002,1.15,0,0,.52778],8970:[.65002,1.15,0,0,.52778],8971:[.65002,1.15,0,0,.52778],10216:[.65002,1.15,0,0,.61111],10217:[.65002,1.15,0,0,.61111],10752:[.55001,1.05,0,0,1.51112],10753:[.55001,1.05,0,0,1.51112],10754:[.55001,1.05,0,0,1.51112],10756:[.55001,1.05,0,0,1.11111],10758:[.55001,1.05,0,0,1.11111]},"Size3-Regular":{32:[0,0,0,0,.25],40:[.95003,1.45,0,0,.73611],41:[.95003,1.45,0,0,.73611],47:[.95003,1.45,0,0,1.04445],91:[.95003,1.45,0,0,.52778],92:[.95003,1.45,0,0,1.04445],93:[.95003,1.45,0,0,.52778],123:[.95003,1.45,0,0,.75],125:[.95003,1.45,0,0,.75],160:[0,0,0,0,.25],710:[0,.75,0,0,1.44445],732:[0,.75,0,0,1.44445],770:[0,.75,0,0,1.44445],771:[0,.75,0,0,1.44445],8730:[.95003,1.45,0,0,1],8968:[.95003,1.45,0,0,.58334],8969:[.95003,1.45,0,0,.58334],8970:[.95003,1.45,0,0,.58334],8971:[.95003,1.45,0,0,.58334],10216:[.95003,1.45,0,0,.75],10217:[.95003,1.45,0,0,.75]},"Size4-Regular":{32:[0,0,0,0,.25],40:[1.25003,1.75,0,0,.79167],41:[1.25003,1.75,0,0,.79167],47:[1.25003,1.75,0,0,1.27778],91:[1.25003,1.75,0,0,.58334],92:[1.25003,1.75,0,0,1.27778],93:[1.25003,1.75,0,0,.58334],123:[1.25003,1.75,0,0,.80556],125:[1.25003,1.75,0,0,.80556],160:[0,0,0,0,.25],710:[0,.825,0,0,1.8889],732:[0,.825,0,0,1.8889],770:[0,.825,0,0,1.8889],771:[0,.825,0,0,1.8889],8730:[1.25003,1.75,0,0,1],8968:[1.25003,1.75,0,0,.63889],8969:[1.25003,1.75,0,0,.63889],8970:[1.25003,1.75,0,0,.63889],8971:[1.25003,1.75,0,0,.63889],9115:[.64502,1.155,0,0,.875],9116:[1e-5,.6,0,0,.875],9117:[.64502,1.155,0,0,.875],9118:[.64502,1.155,0,0,.875],9119:[1e-5,.6,0,0,.875],9120:[.64502,1.155,0,0,.875],9121:[.64502,1.155,0,0,.66667],9122:[-99e-5,.601,0,0,.66667],9123:[.64502,1.155,0,0,.66667],9124:[.64502,1.155,0,0,.66667],9125:[-99e-5,.601,0,0,.66667],9126:[.64502,1.155,0,0,.66667],9127:[1e-5,.9,0,0,.88889],9128:[.65002,1.15,0,0,.88889],9129:[.90001,0,0,0,.88889],9130:[0,.3,0,0,.88889],9131:[1e-5,.9,0,0,.88889],9132:[.65002,1.15,0,0,.88889],9133:[.90001,0,0,0,.88889],9143:[.88502,.915,0,0,1.05556],10216:[1.25003,1.75,0,0,.80556],10217:[1.25003,1.75,0,0,.80556],57344:[-.00499,.605,0,0,1.05556],57345:[-.00499,.605,0,0,1.05556],57680:[0,.12,0,0,.45],57681:[0,.12,0,0,.45],57682:[0,.12,0,0,.45],57683:[0,.12,0,0,.45]},"Typewriter-Regular":{32:[0,0,0,0,.525],33:[0,.61111,0,0,.525],34:[0,.61111,0,0,.525],35:[0,.61111,0,0,.525],36:[.08333,.69444,0,0,.525],37:[.08333,.69444,0,0,.525],38:[0,.61111,0,0,.525],39:[0,.61111,0,0,.525],40:[.08333,.69444,0,0,.525],41:[.08333,.69444,0,0,.525],42:[0,.52083,0,0,.525],43:[-.08056,.53055,0,0,.525],44:[.13889,.125,0,0,.525],45:[-.08056,.53055,0,0,.525],46:[0,.125,0,0,.525],47:[.08333,.69444,0,0,.525],48:[0,.61111,0,0,.525],49:[0,.61111,0,0,.525],50:[0,.61111,0,0,.525],51:[0,.61111,0,0,.525],52:[0,.61111,0,0,.525],53:[0,.61111,0,0,.525],54:[0,.61111,0,0,.525],55:[0,.61111,0,0,.525],56:[0,.61111,0,0,.525],57:[0,.61111,0,0,.525],58:[0,.43056,0,0,.525],59:[.13889,.43056,0,0,.525],60:[-.05556,.55556,0,0,.525],61:[-.19549,.41562,0,0,.525],62:[-.05556,.55556,0,0,.525],63:[0,.61111,0,0,.525],64:[0,.61111,0,0,.525],65:[0,.61111,0,0,.525],66:[0,.61111,0,0,.525],67:[0,.61111,0,0,.525],68:[0,.61111,0,0,.525],69:[0,.61111,0,0,.525],70:[0,.61111,0,0,.525],71:[0,.61111,0,0,.525],72:[0,.61111,0,0,.525],73:[0,.61111,0,0,.525],74:[0,.61111,0,0,.525],75:[0,.61111,0,0,.525],76:[0,.61111,0,0,.525],77:[0,.61111,0,0,.525],78:[0,.61111,0,0,.525],79:[0,.61111,0,0,.525],80:[0,.61111,0,0,.525],81:[.13889,.61111,0,0,.525],82:[0,.61111,0,0,.525],83:[0,.61111,0,0,.525],84:[0,.61111,0,0,.525],85:[0,.61111,0,0,.525],86:[0,.61111,0,0,.525],87:[0,.61111,0,0,.525],88:[0,.61111,0,0,.525],89:[0,.61111,0,0,.525],90:[0,.61111,0,0,.525],91:[.08333,.69444,0,0,.525],92:[.08333,.69444,0,0,.525],93:[.08333,.69444,0,0,.525],94:[0,.61111,0,0,.525],95:[.09514,0,0,0,.525],96:[0,.61111,0,0,.525],97:[0,.43056,0,0,.525],98:[0,.61111,0,0,.525],99:[0,.43056,0,0,.525],100:[0,.61111,0,0,.525],101:[0,.43056,0,0,.525],102:[0,.61111,0,0,.525],103:[.22222,.43056,0,0,.525],104:[0,.61111,0,0,.525],105:[0,.61111,0,0,.525],106:[.22222,.61111,0,0,.525],107:[0,.61111,0,0,.525],108:[0,.61111,0,0,.525],109:[0,.43056,0,0,.525],110:[0,.43056,0,0,.525],111:[0,.43056,0,0,.525],112:[.22222,.43056,0,0,.525],113:[.22222,.43056,0,0,.525],114:[0,.43056,0,0,.525],115:[0,.43056,0,0,.525],116:[0,.55358,0,0,.525],117:[0,.43056,0,0,.525],118:[0,.43056,0,0,.525],119:[0,.43056,0,0,.525],120:[0,.43056,0,0,.525],121:[.22222,.43056,0,0,.525],122:[0,.43056,0,0,.525],123:[.08333,.69444,0,0,.525],124:[.08333,.69444,0,0,.525],125:[.08333,.69444,0,0,.525],126:[0,.61111,0,0,.525],127:[0,.61111,0,0,.525],160:[0,0,0,0,.525],176:[0,.61111,0,0,.525],184:[.19445,0,0,0,.525],305:[0,.43056,0,0,.525],567:[.22222,.43056,0,0,.525],711:[0,.56597,0,0,.525],713:[0,.56555,0,0,.525],714:[0,.61111,0,0,.525],715:[0,.61111,0,0,.525],728:[0,.61111,0,0,.525],730:[0,.61111,0,0,.525],770:[0,.61111,0,0,.525],771:[0,.61111,0,0,.525],776:[0,.61111,0,0,.525],915:[0,.61111,0,0,.525],916:[0,.61111,0,0,.525],920:[0,.61111,0,0,.525],923:[0,.61111,0,0,.525],926:[0,.61111,0,0,.525],928:[0,.61111,0,0,.525],931:[0,.61111,0,0,.525],933:[0,.61111,0,0,.525],934:[0,.61111,0,0,.525],936:[0,.61111,0,0,.525],937:[0,.61111,0,0,.525],8216:[0,.61111,0,0,.525],8217:[0,.61111,0,0,.525],8242:[0,.61111,0,0,.525],9251:[.11111,.21944,0,0,.525]}},ke={slant:[.25,.25,.25],space:[0,0,0],stretch:[0,0,0],shrink:[0,0,0],xHeight:[.431,.431,.431],quad:[1,1.171,1.472],extraSpace:[0,0,0],num1:[.677,.732,.925],num2:[.394,.384,.387],num3:[.444,.471,.504],denom1:[.686,.752,1.025],denom2:[.345,.344,.532],sup1:[.413,.503,.504],sup2:[.363,.431,.404],sup3:[.289,.286,.294],sub1:[.15,.143,.2],sub2:[.247,.286,.4],supDrop:[.386,.353,.494],subDrop:[.05,.071,.1],delim1:[2.39,1.7,1.98],delim2:[1.01,1.157,1.42],axisHeight:[.25,.25,.25],defaultRuleThickness:[.04,.049,.049],bigOpSpacing1:[.111,.111,.111],bigOpSpacing2:[.166,.166,.166],bigOpSpacing3:[.2,.2,.2],bigOpSpacing4:[.6,.611,.611],bigOpSpacing5:[.1,.143,.143],sqrtRuleThickness:[.04,.04,.04],ptPerEm:[10,10,10],doubleRuleSep:[.2,.2,.2],arrayRuleWidth:[.04,.04,.04],fboxsep:[.3,.3,.3],fboxrule:[.04,.04,.04]},Zt={\u00C5:"A",\u00D0:"D",\u00DE:"o",\u00E5:"a",\u00F0:"d",\u00FE:"o",\u0410:"A",\u0411:"B",\u0412:"B",\u0413:"F",\u0414:"A",\u0415:"E",\u0416:"K",\u0417:"3",\u0418:"N",\u0419:"N",\u041A:"K",\u041B:"N",\u041C:"M",\u041D:"H",\u041E:"O",\u041F:"N",\u0420:"P",\u0421:"C",\u0422:"T",\u0423:"y",\u0424:"O",\u0425:"X",\u0426:"U",\u0427:"h",\u0428:"W",\u0429:"W",\u042A:"B",\u042B:"X",\u042C:"B",\u042D:"3",\u042E:"X",\u042F:"R",\u0430:"a",\u0431:"b",\u0432:"a",\u0433:"r",\u0434:"y",\u0435:"e",\u0436:"m",\u0437:"e",\u0438:"n",\u0439:"n",\u043A:"n",\u043B:"n",\u043C:"m",\u043D:"n",\u043E:"o",\u043F:"n",\u0440:"p",\u0441:"c",\u0442:"o",\u0443:"y",\u0444:"b",\u0445:"x",\u0446:"n",\u0447:"n",\u0448:"w",\u0449:"w",\u044A:"a",\u044B:"m",\u044C:"a",\u044D:"e",\u044E:"m",\u044F:"r"};function Ja(r,e){z0[r]=e}function Tt(r,e,t){if(!z0[e])throw new Error("Font metrics not found for font: "+e+".");var a=r.charCodeAt(0),n=z0[e][a];if(!n&&r[0]in Zt&&(a=Zt[r[0]].charCodeAt(0),n=z0[e][a]),!n&&t==="text"&&Ar(a)&&(n=z0[e][77]),n)return{depth:n[0],height:n[1],italic:n[2],skew:n[3],width:n[4]}}var tt={};function Qa(r){var e;if(r>=5?e=0:r>=3?e=1:e=2,!tt[e]){var t=tt[e]={cssEmPerMu:ke.quad[e]/18};for(var a in ke)ke.hasOwnProperty(a)&&(t[a]=ke[a][e])}return tt[e]}var e1=[[1,1,1],[2,1,1],[3,1,1],[4,2,1],[5,2,1],[6,3,1],[7,4,2],[8,6,3],[9,7,6],[10,8,7],[11,10,9]],jt=[.5,.6,.7,.8,.9,1,1.2,1.44,1.728,2.074,2.488],Kt=function(e,t){return t.size<2?e:e1[e-1][t.size-1]},Re=class r{constructor(e){this.style=void 0,this.color=void 0,this.size=void 0,this.textSize=void 0,this.phantom=void 0,this.font=void 0,this.fontFamily=void 0,this.fontWeight=void 0,this.fontShape=void 0,this.sizeMultiplier=void 0,this.maxSize=void 0,this.minRuleThickness=void 0,this._fontMetrics=void 0,this.style=e.style,this.color=e.color,this.size=e.size||r.BASESIZE,this.textSize=e.textSize||this.size,this.phantom=!!e.phantom,this.font=e.font||"",this.fontFamily=e.fontFamily||"",this.fontWeight=e.fontWeight||"",this.fontShape=e.fontShape||"",this.sizeMultiplier=jt[this.size-1],this.maxSize=e.maxSize,this.minRuleThickness=e.minRuleThickness,this._fontMetrics=void 0}extend(e){var t={style:this.style,size:this.size,textSize:this.textSize,color:this.color,phantom:this.phantom,font:this.font,fontFamily:this.fontFamily,fontWeight:this.fontWeight,fontShape:this.fontShape,maxSize:this.maxSize,minRuleThickness:this.minRuleThickness};for(var a in e)e.hasOwnProperty(a)&&(t[a]=e[a]);return new r(t)}havingStyle(e){return this.style===e?this:this.extend({style:e,size:Kt(this.textSize,e)})}havingCrampedStyle(){return this.havingStyle(this.style.cramp())}havingSize(e){return this.size===e&&this.textSize===e?this:this.extend({style:this.style.text(),size:e,textSize:e,sizeMultiplier:jt[e-1]})}havingBaseStyle(e){e=e||this.style.text();var t=Kt(r.BASESIZE,e);return this.size===t&&this.textSize===r.BASESIZE&&this.style===e?this:this.extend({style:e,size:t})}havingBaseSizing(){var e;switch(this.style.id){case 4:case 5:e=3;break;case 6:case 7:e=1;break;default:e=6}return this.extend({style:this.style.text(),size:e})}withColor(e){return this.extend({color:e})}withPhantom(){return this.extend({phantom:!0})}withFont(e){return this.extend({font:e})}withTextFontFamily(e){return this.extend({fontFamily:e,font:""})}withTextFontWeight(e){return this.extend({fontWeight:e,font:""})}withTextFontShape(e){return this.extend({fontShape:e,font:""})}sizingClasses(e){return e.size!==this.size?["sizing","reset-size"+e.size,"size"+this.size]:[]}baseSizingClasses(){return this.size!==r.BASESIZE?["sizing","reset-size"+this.size,"size"+r.BASESIZE]:[]}fontMetrics(){return this._fontMetrics||(this._fontMetrics=Qa(this.size)),this._fontMetrics}getColor(){return this.phantom?"transparent":this.color}};Re.BASESIZE=6;var ft={pt:1,mm:7227/2540,cm:7227/254,in:72.27,bp:803/800,pc:12,dd:1238/1157,cc:14856/1157,nd:685/642,nc:1370/107,sp:1/65536,px:803/800},t1={ex:!0,em:!0,mu:!0},Tr=function(e){return typeof e!="string"&&(e=e.unit),e in ft||e in t1||e==="ex"},Q=function(e,t){var a;if(e.unit in ft)a=ft[e.unit]/t.fontMetrics().ptPerEm/t.sizeMultiplier;else if(e.unit==="mu")a=t.fontMetrics().cssEmPerMu;else{var n;if(t.style.isTight()?n=t.havingStyle(t.style.text()):n=t,e.unit==="ex")a=n.fontMetrics().xHeight;else if(e.unit==="em")a=n.fontMetrics().quad;else throw new z("Invalid unit: '"+e.unit+"'");n!==t&&(a*=n.sizeMultiplier/t.sizeMultiplier)}return Math.min(e.number*a,t.maxSize)},T=function(e){return+e.toFixed(4)+"em"},G0=function(e){return e.filter(t=>t).join(" ")},qr=function(e,t,a){if(this.classes=e||[],this.attributes={},this.height=0,this.depth=0,this.maxFontSize=0,this.style=a||{},t){t.style.isTight()&&this.classes.push("mtight");var n=t.getColor();n&&(this.style.color=n)}},Br=function(e){var t=document.createElement(e);t.className=G0(this.classes);for(var a in this.style)this.style.hasOwnProperty(a)&&(t.style[a]=this.style[a]);for(var n in this.attributes)this.attributes.hasOwnProperty(n)&&t.setAttribute(n,this.attributes[n]);for(var s=0;s/=\x00-\x1f]/,Dr=function(e){var t="<"+e;this.classes.length&&(t+=' class="'+O.escape(G0(this.classes))+'"');var a="";for(var n in this.style)this.style.hasOwnProperty(n)&&(a+=O.hyphenate(n)+":"+this.style[n]+";");a&&(t+=' style="'+O.escape(a)+'"');for(var s in this.attributes)if(this.attributes.hasOwnProperty(s)){if(r1.test(s))throw new z("Invalid attribute name '"+s+"'");t+=" "+s+'="'+O.escape(this.attributes[s])+'"'}t+=">";for(var l=0;l",t},Z0=class{constructor(e,t,a,n){this.children=void 0,this.attributes=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.width=void 0,this.maxFontSize=void 0,this.style=void 0,qr.call(this,e,a,n),this.children=t||[]}setAttribute(e,t){this.attributes[e]=t}hasClass(e){return O.contains(this.classes,e)}toNode(){return Br.call(this,"span")}toMarkup(){return Dr.call(this,"span")}},fe=class{constructor(e,t,a,n){this.children=void 0,this.attributes=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,qr.call(this,t,n),this.children=a||[],this.setAttribute("href",e)}setAttribute(e,t){this.attributes[e]=t}hasClass(e){return O.contains(this.classes,e)}toNode(){return Br.call(this,"a")}toMarkup(){return Dr.call(this,"a")}},vt=class{constructor(e,t,a){this.src=void 0,this.alt=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,this.alt=t,this.src=e,this.classes=["mord"],this.style=a}hasClass(e){return O.contains(this.classes,e)}toNode(){var e=document.createElement("img");e.src=this.src,e.alt=this.alt,e.className="mord";for(var t in this.style)this.style.hasOwnProperty(t)&&(e.style[t]=this.style[t]);return e}toMarkup(){var e=' ",e}},a1={\u00EE:"\u0131\u0302",\u00EF:"\u0131\u0308",\u00ED:"\u0131\u0301",\u00EC:"\u0131\u0300"},c0=class{constructor(e,t,a,n,s,l,h,c){this.text=void 0,this.height=void 0,this.depth=void 0,this.italic=void 0,this.skew=void 0,this.width=void 0,this.maxFontSize=void 0,this.classes=void 0,this.style=void 0,this.text=e,this.height=t||0,this.depth=a||0,this.italic=n||0,this.skew=s||0,this.width=l||0,this.classes=h||[],this.style=c||{},this.maxFontSize=0;var f=Ha(this.text.charCodeAt(0));f&&this.classes.push(f+"_fallback"),/[îïíì]/.test(this.text)&&(this.text=a1[this.text])}hasClass(e){return O.contains(this.classes,e)}toNode(){var e=document.createTextNode(this.text),t=null;this.italic>0&&(t=document.createElement("span"),t.style.marginRight=T(this.italic)),this.classes.length>0&&(t=t||document.createElement("span"),t.className=G0(this.classes));for(var a in this.style)this.style.hasOwnProperty(a)&&(t=t||document.createElement("span"),t.style[a]=this.style[a]);return t?(t.appendChild(e),t):e}toMarkup(){var e=!1,t="0&&(a+="margin-right:"+this.italic+"em;");for(var n in this.style)this.style.hasOwnProperty(n)&&(a+=O.hyphenate(n)+":"+this.style[n]+";");a&&(e=!0,t+=' style="'+O.escape(a)+'"');var s=O.escape(this.text);return e?(t+=">",t+=s,t+=" ",t):s}},S0=class{constructor(e,t){this.children=void 0,this.attributes=void 0,this.children=e||[],this.attributes=t||{}}toNode(){var e="http://www.w3.org/2000/svg",t=document.createElementNS(e,"svg");for(var a in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,a)&&t.setAttribute(a,this.attributes[a]);for(var n=0;n";for(var a=0;a",e}},A0=class{constructor(e,t){this.pathName=void 0,this.alternate=void 0,this.pathName=e,this.alternate=t}toNode(){var e="http://www.w3.org/2000/svg",t=document.createElementNS(e,"path");return this.alternate?t.setAttribute("d",this.alternate):t.setAttribute("d",Yt[this.pathName]),t}toMarkup(){return this.alternate?' ':' '}},ve=class{constructor(e){this.attributes=void 0,this.attributes=e||{}}toNode(){var e="http://www.w3.org/2000/svg",t=document.createElementNS(e,"line");for(var a in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,a)&&t.setAttribute(a,this.attributes[a]);return t}toMarkup(){var e=" ",e}};function Jt(r){if(r instanceof c0)return r;throw new Error("Expected symbolNode but got "+String(r)+".")}function n1(r){if(r instanceof Z0)return r;throw new Error("Expected span but got "+String(r)+".")}var i1={bin:1,close:1,inner:1,open:1,punct:1,rel:1},s1={"accent-token":1,mathord:1,"op-token":1,spacing:1,textord:1},Y={math:{},text:{}};function i(r,e,t,a,n,s){Y[r][n]={font:e,group:t,replace:a},s&&a&&(Y[r][a]=Y[r][n])}var o="math",k="text",u="main",d="ams",Z="accent-token",C="bin",l0="close",ie="inner",I="mathord",t0="op-token",p0="open",Ve="punct",p="rel",R0="spacing",g="textord";i(o,u,p,"\u2261","\\equiv",!0);i(o,u,p,"\u227A","\\prec",!0);i(o,u,p,"\u227B","\\succ",!0);i(o,u,p,"\u223C","\\sim",!0);i(o,u,p,"\u22A5","\\perp");i(o,u,p,"\u2AAF","\\preceq",!0);i(o,u,p,"\u2AB0","\\succeq",!0);i(o,u,p,"\u2243","\\simeq",!0);i(o,u,p,"\u2223","\\mid",!0);i(o,u,p,"\u226A","\\ll",!0);i(o,u,p,"\u226B","\\gg",!0);i(o,u,p,"\u224D","\\asymp",!0);i(o,u,p,"\u2225","\\parallel");i(o,u,p,"\u22C8","\\bowtie",!0);i(o,u,p,"\u2323","\\smile",!0);i(o,u,p,"\u2291","\\sqsubseteq",!0);i(o,u,p,"\u2292","\\sqsupseteq",!0);i(o,u,p,"\u2250","\\doteq",!0);i(o,u,p,"\u2322","\\frown",!0);i(o,u,p,"\u220B","\\ni",!0);i(o,u,p,"\u221D","\\propto",!0);i(o,u,p,"\u22A2","\\vdash",!0);i(o,u,p,"\u22A3","\\dashv",!0);i(o,u,p,"\u220B","\\owns");i(o,u,Ve,".","\\ldotp");i(o,u,Ve,"\u22C5","\\cdotp");i(o,u,g,"#","\\#");i(k,u,g,"#","\\#");i(o,u,g,"&","\\&");i(k,u,g,"&","\\&");i(o,u,g,"\u2135","\\aleph",!0);i(o,u,g,"\u2200","\\forall",!0);i(o,u,g,"\u210F","\\hbar",!0);i(o,u,g,"\u2203","\\exists",!0);i(o,u,g,"\u2207","\\nabla",!0);i(o,u,g,"\u266D","\\flat",!0);i(o,u,g,"\u2113","\\ell",!0);i(o,u,g,"\u266E","\\natural",!0);i(o,u,g,"\u2663","\\clubsuit",!0);i(o,u,g,"\u2118","\\wp",!0);i(o,u,g,"\u266F","\\sharp",!0);i(o,u,g,"\u2662","\\diamondsuit",!0);i(o,u,g,"\u211C","\\Re",!0);i(o,u,g,"\u2661","\\heartsuit",!0);i(o,u,g,"\u2111","\\Im",!0);i(o,u,g,"\u2660","\\spadesuit",!0);i(o,u,g,"\xA7","\\S",!0);i(k,u,g,"\xA7","\\S");i(o,u,g,"\xB6","\\P",!0);i(k,u,g,"\xB6","\\P");i(o,u,g,"\u2020","\\dag");i(k,u,g,"\u2020","\\dag");i(k,u,g,"\u2020","\\textdagger");i(o,u,g,"\u2021","\\ddag");i(k,u,g,"\u2021","\\ddag");i(k,u,g,"\u2021","\\textdaggerdbl");i(o,u,l0,"\u23B1","\\rmoustache",!0);i(o,u,p0,"\u23B0","\\lmoustache",!0);i(o,u,l0,"\u27EF","\\rgroup",!0);i(o,u,p0,"\u27EE","\\lgroup",!0);i(o,u,C,"\u2213","\\mp",!0);i(o,u,C,"\u2296","\\ominus",!0);i(o,u,C,"\u228E","\\uplus",!0);i(o,u,C,"\u2293","\\sqcap",!0);i(o,u,C,"\u2217","\\ast");i(o,u,C,"\u2294","\\sqcup",!0);i(o,u,C,"\u25EF","\\bigcirc",!0);i(o,u,C,"\u2219","\\bullet",!0);i(o,u,C,"\u2021","\\ddagger");i(o,u,C,"\u2240","\\wr",!0);i(o,u,C,"\u2A3F","\\amalg");i(o,u,C,"&","\\And");i(o,u,p,"\u27F5","\\longleftarrow",!0);i(o,u,p,"\u21D0","\\Leftarrow",!0);i(o,u,p,"\u27F8","\\Longleftarrow",!0);i(o,u,p,"\u27F6","\\longrightarrow",!0);i(o,u,p,"\u21D2","\\Rightarrow",!0);i(o,u,p,"\u27F9","\\Longrightarrow",!0);i(o,u,p,"\u2194","\\leftrightarrow",!0);i(o,u,p,"\u27F7","\\longleftrightarrow",!0);i(o,u,p,"\u21D4","\\Leftrightarrow",!0);i(o,u,p,"\u27FA","\\Longleftrightarrow",!0);i(o,u,p,"\u21A6","\\mapsto",!0);i(o,u,p,"\u27FC","\\longmapsto",!0);i(o,u,p,"\u2197","\\nearrow",!0);i(o,u,p,"\u21A9","\\hookleftarrow",!0);i(o,u,p,"\u21AA","\\hookrightarrow",!0);i(o,u,p,"\u2198","\\searrow",!0);i(o,u,p,"\u21BC","\\leftharpoonup",!0);i(o,u,p,"\u21C0","\\rightharpoonup",!0);i(o,u,p,"\u2199","\\swarrow",!0);i(o,u,p,"\u21BD","\\leftharpoondown",!0);i(o,u,p,"\u21C1","\\rightharpoondown",!0);i(o,u,p,"\u2196","\\nwarrow",!0);i(o,u,p,"\u21CC","\\rightleftharpoons",!0);i(o,d,p,"\u226E","\\nless",!0);i(o,d,p,"\uE010","\\@nleqslant");i(o,d,p,"\uE011","\\@nleqq");i(o,d,p,"\u2A87","\\lneq",!0);i(o,d,p,"\u2268","\\lneqq",!0);i(o,d,p,"\uE00C","\\@lvertneqq");i(o,d,p,"\u22E6","\\lnsim",!0);i(o,d,p,"\u2A89","\\lnapprox",!0);i(o,d,p,"\u2280","\\nprec",!0);i(o,d,p,"\u22E0","\\npreceq",!0);i(o,d,p,"\u22E8","\\precnsim",!0);i(o,d,p,"\u2AB9","\\precnapprox",!0);i(o,d,p,"\u2241","\\nsim",!0);i(o,d,p,"\uE006","\\@nshortmid");i(o,d,p,"\u2224","\\nmid",!0);i(o,d,p,"\u22AC","\\nvdash",!0);i(o,d,p,"\u22AD","\\nvDash",!0);i(o,d,p,"\u22EA","\\ntriangleleft");i(o,d,p,"\u22EC","\\ntrianglelefteq",!0);i(o,d,p,"\u228A","\\subsetneq",!0);i(o,d,p,"\uE01A","\\@varsubsetneq");i(o,d,p,"\u2ACB","\\subsetneqq",!0);i(o,d,p,"\uE017","\\@varsubsetneqq");i(o,d,p,"\u226F","\\ngtr",!0);i(o,d,p,"\uE00F","\\@ngeqslant");i(o,d,p,"\uE00E","\\@ngeqq");i(o,d,p,"\u2A88","\\gneq",!0);i(o,d,p,"\u2269","\\gneqq",!0);i(o,d,p,"\uE00D","\\@gvertneqq");i(o,d,p,"\u22E7","\\gnsim",!0);i(o,d,p,"\u2A8A","\\gnapprox",!0);i(o,d,p,"\u2281","\\nsucc",!0);i(o,d,p,"\u22E1","\\nsucceq",!0);i(o,d,p,"\u22E9","\\succnsim",!0);i(o,d,p,"\u2ABA","\\succnapprox",!0);i(o,d,p,"\u2246","\\ncong",!0);i(o,d,p,"\uE007","\\@nshortparallel");i(o,d,p,"\u2226","\\nparallel",!0);i(o,d,p,"\u22AF","\\nVDash",!0);i(o,d,p,"\u22EB","\\ntriangleright");i(o,d,p,"\u22ED","\\ntrianglerighteq",!0);i(o,d,p,"\uE018","\\@nsupseteqq");i(o,d,p,"\u228B","\\supsetneq",!0);i(o,d,p,"\uE01B","\\@varsupsetneq");i(o,d,p,"\u2ACC","\\supsetneqq",!0);i(o,d,p,"\uE019","\\@varsupsetneqq");i(o,d,p,"\u22AE","\\nVdash",!0);i(o,d,p,"\u2AB5","\\precneqq",!0);i(o,d,p,"\u2AB6","\\succneqq",!0);i(o,d,p,"\uE016","\\@nsubseteqq");i(o,d,C,"\u22B4","\\unlhd");i(o,d,C,"\u22B5","\\unrhd");i(o,d,p,"\u219A","\\nleftarrow",!0);i(o,d,p,"\u219B","\\nrightarrow",!0);i(o,d,p,"\u21CD","\\nLeftarrow",!0);i(o,d,p,"\u21CF","\\nRightarrow",!0);i(o,d,p,"\u21AE","\\nleftrightarrow",!0);i(o,d,p,"\u21CE","\\nLeftrightarrow",!0);i(o,d,p,"\u25B3","\\vartriangle");i(o,d,g,"\u210F","\\hslash");i(o,d,g,"\u25BD","\\triangledown");i(o,d,g,"\u25CA","\\lozenge");i(o,d,g,"\u24C8","\\circledS");i(o,d,g,"\xAE","\\circledR");i(k,d,g,"\xAE","\\circledR");i(o,d,g,"\u2221","\\measuredangle",!0);i(o,d,g,"\u2204","\\nexists");i(o,d,g,"\u2127","\\mho");i(o,d,g,"\u2132","\\Finv",!0);i(o,d,g,"\u2141","\\Game",!0);i(o,d,g,"\u2035","\\backprime");i(o,d,g,"\u25B2","\\blacktriangle");i(o,d,g,"\u25BC","\\blacktriangledown");i(o,d,g,"\u25A0","\\blacksquare");i(o,d,g,"\u29EB","\\blacklozenge");i(o,d,g,"\u2605","\\bigstar");i(o,d,g,"\u2222","\\sphericalangle",!0);i(o,d,g,"\u2201","\\complement",!0);i(o,d,g,"\xF0","\\eth",!0);i(k,u,g,"\xF0","\xF0");i(o,d,g,"\u2571","\\diagup");i(o,d,g,"\u2572","\\diagdown");i(o,d,g,"\u25A1","\\square");i(o,d,g,"\u25A1","\\Box");i(o,d,g,"\u25CA","\\Diamond");i(o,d,g,"\xA5","\\yen",!0);i(k,d,g,"\xA5","\\yen",!0);i(o,d,g,"\u2713","\\checkmark",!0);i(k,d,g,"\u2713","\\checkmark");i(o,d,g,"\u2136","\\beth",!0);i(o,d,g,"\u2138","\\daleth",!0);i(o,d,g,"\u2137","\\gimel",!0);i(o,d,g,"\u03DD","\\digamma",!0);i(o,d,g,"\u03F0","\\varkappa");i(o,d,p0,"\u250C","\\@ulcorner",!0);i(o,d,l0,"\u2510","\\@urcorner",!0);i(o,d,p0,"\u2514","\\@llcorner",!0);i(o,d,l0,"\u2518","\\@lrcorner",!0);i(o,d,p,"\u2266","\\leqq",!0);i(o,d,p,"\u2A7D","\\leqslant",!0);i(o,d,p,"\u2A95","\\eqslantless",!0);i(o,d,p,"\u2272","\\lesssim",!0);i(o,d,p,"\u2A85","\\lessapprox",!0);i(o,d,p,"\u224A","\\approxeq",!0);i(o,d,C,"\u22D6","\\lessdot");i(o,d,p,"\u22D8","\\lll",!0);i(o,d,p,"\u2276","\\lessgtr",!0);i(o,d,p,"\u22DA","\\lesseqgtr",!0);i(o,d,p,"\u2A8B","\\lesseqqgtr",!0);i(o,d,p,"\u2251","\\doteqdot");i(o,d,p,"\u2253","\\risingdotseq",!0);i(o,d,p,"\u2252","\\fallingdotseq",!0);i(o,d,p,"\u223D","\\backsim",!0);i(o,d,p,"\u22CD","\\backsimeq",!0);i(o,d,p,"\u2AC5","\\subseteqq",!0);i(o,d,p,"\u22D0","\\Subset",!0);i(o,d,p,"\u228F","\\sqsubset",!0);i(o,d,p,"\u227C","\\preccurlyeq",!0);i(o,d,p,"\u22DE","\\curlyeqprec",!0);i(o,d,p,"\u227E","\\precsim",!0);i(o,d,p,"\u2AB7","\\precapprox",!0);i(o,d,p,"\u22B2","\\vartriangleleft");i(o,d,p,"\u22B4","\\trianglelefteq");i(o,d,p,"\u22A8","\\vDash",!0);i(o,d,p,"\u22AA","\\Vvdash",!0);i(o,d,p,"\u2323","\\smallsmile");i(o,d,p,"\u2322","\\smallfrown");i(o,d,p,"\u224F","\\bumpeq",!0);i(o,d,p,"\u224E","\\Bumpeq",!0);i(o,d,p,"\u2267","\\geqq",!0);i(o,d,p,"\u2A7E","\\geqslant",!0);i(o,d,p,"\u2A96","\\eqslantgtr",!0);i(o,d,p,"\u2273","\\gtrsim",!0);i(o,d,p,"\u2A86","\\gtrapprox",!0);i(o,d,C,"\u22D7","\\gtrdot");i(o,d,p,"\u22D9","\\ggg",!0);i(o,d,p,"\u2277","\\gtrless",!0);i(o,d,p,"\u22DB","\\gtreqless",!0);i(o,d,p,"\u2A8C","\\gtreqqless",!0);i(o,d,p,"\u2256","\\eqcirc",!0);i(o,d,p,"\u2257","\\circeq",!0);i(o,d,p,"\u225C","\\triangleq",!0);i(o,d,p,"\u223C","\\thicksim");i(o,d,p,"\u2248","\\thickapprox");i(o,d,p,"\u2AC6","\\supseteqq",!0);i(o,d,p,"\u22D1","\\Supset",!0);i(o,d,p,"\u2290","\\sqsupset",!0);i(o,d,p,"\u227D","\\succcurlyeq",!0);i(o,d,p,"\u22DF","\\curlyeqsucc",!0);i(o,d,p,"\u227F","\\succsim",!0);i(o,d,p,"\u2AB8","\\succapprox",!0);i(o,d,p,"\u22B3","\\vartriangleright");i(o,d,p,"\u22B5","\\trianglerighteq");i(o,d,p,"\u22A9","\\Vdash",!0);i(o,d,p,"\u2223","\\shortmid");i(o,d,p,"\u2225","\\shortparallel");i(o,d,p,"\u226C","\\between",!0);i(o,d,p,"\u22D4","\\pitchfork",!0);i(o,d,p,"\u221D","\\varpropto");i(o,d,p,"\u25C0","\\blacktriangleleft");i(o,d,p,"\u2234","\\therefore",!0);i(o,d,p,"\u220D","\\backepsilon");i(o,d,p,"\u25B6","\\blacktriangleright");i(o,d,p,"\u2235","\\because",!0);i(o,d,p,"\u22D8","\\llless");i(o,d,p,"\u22D9","\\gggtr");i(o,d,C,"\u22B2","\\lhd");i(o,d,C,"\u22B3","\\rhd");i(o,d,p,"\u2242","\\eqsim",!0);i(o,u,p,"\u22C8","\\Join");i(o,d,p,"\u2251","\\Doteq",!0);i(o,d,C,"\u2214","\\dotplus",!0);i(o,d,C,"\u2216","\\smallsetminus");i(o,d,C,"\u22D2","\\Cap",!0);i(o,d,C,"\u22D3","\\Cup",!0);i(o,d,C,"\u2A5E","\\doublebarwedge",!0);i(o,d,C,"\u229F","\\boxminus",!0);i(o,d,C,"\u229E","\\boxplus",!0);i(o,d,C,"\u22C7","\\divideontimes",!0);i(o,d,C,"\u22C9","\\ltimes",!0);i(o,d,C,"\u22CA","\\rtimes",!0);i(o,d,C,"\u22CB","\\leftthreetimes",!0);i(o,d,C,"\u22CC","\\rightthreetimes",!0);i(o,d,C,"\u22CF","\\curlywedge",!0);i(o,d,C,"\u22CE","\\curlyvee",!0);i(o,d,C,"\u229D","\\circleddash",!0);i(o,d,C,"\u229B","\\circledast",!0);i(o,d,C,"\u22C5","\\centerdot");i(o,d,C,"\u22BA","\\intercal",!0);i(o,d,C,"\u22D2","\\doublecap");i(o,d,C,"\u22D3","\\doublecup");i(o,d,C,"\u22A0","\\boxtimes",!0);i(o,d,p,"\u21E2","\\dashrightarrow",!0);i(o,d,p,"\u21E0","\\dashleftarrow",!0);i(o,d,p,"\u21C7","\\leftleftarrows",!0);i(o,d,p,"\u21C6","\\leftrightarrows",!0);i(o,d,p,"\u21DA","\\Lleftarrow",!0);i(o,d,p,"\u219E","\\twoheadleftarrow",!0);i(o,d,p,"\u21A2","\\leftarrowtail",!0);i(o,d,p,"\u21AB","\\looparrowleft",!0);i(o,d,p,"\u21CB","\\leftrightharpoons",!0);i(o,d,p,"\u21B6","\\curvearrowleft",!0);i(o,d,p,"\u21BA","\\circlearrowleft",!0);i(o,d,p,"\u21B0","\\Lsh",!0);i(o,d,p,"\u21C8","\\upuparrows",!0);i(o,d,p,"\u21BF","\\upharpoonleft",!0);i(o,d,p,"\u21C3","\\downharpoonleft",!0);i(o,u,p,"\u22B6","\\origof",!0);i(o,u,p,"\u22B7","\\imageof",!0);i(o,d,p,"\u22B8","\\multimap",!0);i(o,d,p,"\u21AD","\\leftrightsquigarrow",!0);i(o,d,p,"\u21C9","\\rightrightarrows",!0);i(o,d,p,"\u21C4","\\rightleftarrows",!0);i(o,d,p,"\u21A0","\\twoheadrightarrow",!0);i(o,d,p,"\u21A3","\\rightarrowtail",!0);i(o,d,p,"\u21AC","\\looparrowright",!0);i(o,d,p,"\u21B7","\\curvearrowright",!0);i(o,d,p,"\u21BB","\\circlearrowright",!0);i(o,d,p,"\u21B1","\\Rsh",!0);i(o,d,p,"\u21CA","\\downdownarrows",!0);i(o,d,p,"\u21BE","\\upharpoonright",!0);i(o,d,p,"\u21C2","\\downharpoonright",!0);i(o,d,p,"\u21DD","\\rightsquigarrow",!0);i(o,d,p,"\u21DD","\\leadsto");i(o,d,p,"\u21DB","\\Rrightarrow",!0);i(o,d,p,"\u21BE","\\restriction");i(o,u,g,"\u2018","`");i(o,u,g,"$","\\$");i(k,u,g,"$","\\$");i(k,u,g,"$","\\textdollar");i(o,u,g,"%","\\%");i(k,u,g,"%","\\%");i(o,u,g,"_","\\_");i(k,u,g,"_","\\_");i(k,u,g,"_","\\textunderscore");i(o,u,g,"\u2220","\\angle",!0);i(o,u,g,"\u221E","\\infty",!0);i(o,u,g,"\u2032","\\prime");i(o,u,g,"\u25B3","\\triangle");i(o,u,g,"\u0393","\\Gamma",!0);i(o,u,g,"\u0394","\\Delta",!0);i(o,u,g,"\u0398","\\Theta",!0);i(o,u,g,"\u039B","\\Lambda",!0);i(o,u,g,"\u039E","\\Xi",!0);i(o,u,g,"\u03A0","\\Pi",!0);i(o,u,g,"\u03A3","\\Sigma",!0);i(o,u,g,"\u03A5","\\Upsilon",!0);i(o,u,g,"\u03A6","\\Phi",!0);i(o,u,g,"\u03A8","\\Psi",!0);i(o,u,g,"\u03A9","\\Omega",!0);i(o,u,g,"A","\u0391");i(o,u,g,"B","\u0392");i(o,u,g,"E","\u0395");i(o,u,g,"Z","\u0396");i(o,u,g,"H","\u0397");i(o,u,g,"I","\u0399");i(o,u,g,"K","\u039A");i(o,u,g,"M","\u039C");i(o,u,g,"N","\u039D");i(o,u,g,"O","\u039F");i(o,u,g,"P","\u03A1");i(o,u,g,"T","\u03A4");i(o,u,g,"X","\u03A7");i(o,u,g,"\xAC","\\neg",!0);i(o,u,g,"\xAC","\\lnot");i(o,u,g,"\u22A4","\\top");i(o,u,g,"\u22A5","\\bot");i(o,u,g,"\u2205","\\emptyset");i(o,d,g,"\u2205","\\varnothing");i(o,u,I,"\u03B1","\\alpha",!0);i(o,u,I,"\u03B2","\\beta",!0);i(o,u,I,"\u03B3","\\gamma",!0);i(o,u,I,"\u03B4","\\delta",!0);i(o,u,I,"\u03F5","\\epsilon",!0);i(o,u,I,"\u03B6","\\zeta",!0);i(o,u,I,"\u03B7","\\eta",!0);i(o,u,I,"\u03B8","\\theta",!0);i(o,u,I,"\u03B9","\\iota",!0);i(o,u,I,"\u03BA","\\kappa",!0);i(o,u,I,"\u03BB","\\lambda",!0);i(o,u,I,"\u03BC","\\mu",!0);i(o,u,I,"\u03BD","\\nu",!0);i(o,u,I,"\u03BE","\\xi",!0);i(o,u,I,"\u03BF","\\omicron",!0);i(o,u,I,"\u03C0","\\pi",!0);i(o,u,I,"\u03C1","\\rho",!0);i(o,u,I,"\u03C3","\\sigma",!0);i(o,u,I,"\u03C4","\\tau",!0);i(o,u,I,"\u03C5","\\upsilon",!0);i(o,u,I,"\u03D5","\\phi",!0);i(o,u,I,"\u03C7","\\chi",!0);i(o,u,I,"\u03C8","\\psi",!0);i(o,u,I,"\u03C9","\\omega",!0);i(o,u,I,"\u03B5","\\varepsilon",!0);i(o,u,I,"\u03D1","\\vartheta",!0);i(o,u,I,"\u03D6","\\varpi",!0);i(o,u,I,"\u03F1","\\varrho",!0);i(o,u,I,"\u03C2","\\varsigma",!0);i(o,u,I,"\u03C6","\\varphi",!0);i(o,u,C,"\u2217","*",!0);i(o,u,C,"+","+");i(o,u,C,"\u2212","-",!0);i(o,u,C,"\u22C5","\\cdot",!0);i(o,u,C,"\u2218","\\circ",!0);i(o,u,C,"\xF7","\\div",!0);i(o,u,C,"\xB1","\\pm",!0);i(o,u,C,"\xD7","\\times",!0);i(o,u,C,"\u2229","\\cap",!0);i(o,u,C,"\u222A","\\cup",!0);i(o,u,C,"\u2216","\\setminus",!0);i(o,u,C,"\u2227","\\land");i(o,u,C,"\u2228","\\lor");i(o,u,C,"\u2227","\\wedge",!0);i(o,u,C,"\u2228","\\vee",!0);i(o,u,g,"\u221A","\\surd");i(o,u,p0,"\u27E8","\\langle",!0);i(o,u,p0,"\u2223","\\lvert");i(o,u,p0,"\u2225","\\lVert");i(o,u,l0,"?","?");i(o,u,l0,"!","!");i(o,u,l0,"\u27E9","\\rangle",!0);i(o,u,l0,"\u2223","\\rvert");i(o,u,l0,"\u2225","\\rVert");i(o,u,p,"=","=");i(o,u,p,":",":");i(o,u,p,"\u2248","\\approx",!0);i(o,u,p,"\u2245","\\cong",!0);i(o,u,p,"\u2265","\\ge");i(o,u,p,"\u2265","\\geq",!0);i(o,u,p,"\u2190","\\gets");i(o,u,p,">","\\gt",!0);i(o,u,p,"\u2208","\\in",!0);i(o,u,p,"\uE020","\\@not");i(o,u,p,"\u2282","\\subset",!0);i(o,u,p,"\u2283","\\supset",!0);i(o,u,p,"\u2286","\\subseteq",!0);i(o,u,p,"\u2287","\\supseteq",!0);i(o,d,p,"\u2288","\\nsubseteq",!0);i(o,d,p,"\u2289","\\nsupseteq",!0);i(o,u,p,"\u22A8","\\models");i(o,u,p,"\u2190","\\leftarrow",!0);i(o,u,p,"\u2264","\\le");i(o,u,p,"\u2264","\\leq",!0);i(o,u,p,"<","\\lt",!0);i(o,u,p,"\u2192","\\rightarrow",!0);i(o,u,p,"\u2192","\\to");i(o,d,p,"\u2271","\\ngeq",!0);i(o,d,p,"\u2270","\\nleq",!0);i(o,u,R0,"\xA0","\\ ");i(o,u,R0,"\xA0","\\space");i(o,u,R0,"\xA0","\\nobreakspace");i(k,u,R0,"\xA0","\\ ");i(k,u,R0,"\xA0"," ");i(k,u,R0,"\xA0","\\space");i(k,u,R0,"\xA0","\\nobreakspace");i(o,u,R0,null,"\\nobreak");i(o,u,R0,null,"\\allowbreak");i(o,u,Ve,",",",");i(o,u,Ve,";",";");i(o,d,C,"\u22BC","\\barwedge",!0);i(o,d,C,"\u22BB","\\veebar",!0);i(o,u,C,"\u2299","\\odot",!0);i(o,u,C,"\u2295","\\oplus",!0);i(o,u,C,"\u2297","\\otimes",!0);i(o,u,g,"\u2202","\\partial",!0);i(o,u,C,"\u2298","\\oslash",!0);i(o,d,C,"\u229A","\\circledcirc",!0);i(o,d,C,"\u22A1","\\boxdot",!0);i(o,u,C,"\u25B3","\\bigtriangleup");i(o,u,C,"\u25BD","\\bigtriangledown");i(o,u,C,"\u2020","\\dagger");i(o,u,C,"\u22C4","\\diamond");i(o,u,C,"\u22C6","\\star");i(o,u,C,"\u25C3","\\triangleleft");i(o,u,C,"\u25B9","\\triangleright");i(o,u,p0,"{","\\{");i(k,u,g,"{","\\{");i(k,u,g,"{","\\textbraceleft");i(o,u,l0,"}","\\}");i(k,u,g,"}","\\}");i(k,u,g,"}","\\textbraceright");i(o,u,p0,"{","\\lbrace");i(o,u,l0,"}","\\rbrace");i(o,u,p0,"[","\\lbrack",!0);i(k,u,g,"[","\\lbrack",!0);i(o,u,l0,"]","\\rbrack",!0);i(k,u,g,"]","\\rbrack",!0);i(o,u,p0,"(","\\lparen",!0);i(o,u,l0,")","\\rparen",!0);i(k,u,g,"<","\\textless",!0);i(k,u,g,">","\\textgreater",!0);i(o,u,p0,"\u230A","\\lfloor",!0);i(o,u,l0,"\u230B","\\rfloor",!0);i(o,u,p0,"\u2308","\\lceil",!0);i(o,u,l0,"\u2309","\\rceil",!0);i(o,u,g,"\\","\\backslash");i(o,u,g,"\u2223","|");i(o,u,g,"\u2223","\\vert");i(k,u,g,"|","\\textbar",!0);i(o,u,g,"\u2225","\\|");i(o,u,g,"\u2225","\\Vert");i(k,u,g,"\u2225","\\textbardbl");i(k,u,g,"~","\\textasciitilde");i(k,u,g,"\\","\\textbackslash");i(k,u,g,"^","\\textasciicircum");i(o,u,p,"\u2191","\\uparrow",!0);i(o,u,p,"\u21D1","\\Uparrow",!0);i(o,u,p,"\u2193","\\downarrow",!0);i(o,u,p,"\u21D3","\\Downarrow",!0);i(o,u,p,"\u2195","\\updownarrow",!0);i(o,u,p,"\u21D5","\\Updownarrow",!0);i(o,u,t0,"\u2210","\\coprod");i(o,u,t0,"\u22C1","\\bigvee");i(o,u,t0,"\u22C0","\\bigwedge");i(o,u,t0,"\u2A04","\\biguplus");i(o,u,t0,"\u22C2","\\bigcap");i(o,u,t0,"\u22C3","\\bigcup");i(o,u,t0,"\u222B","\\int");i(o,u,t0,"\u222B","\\intop");i(o,u,t0,"\u222C","\\iint");i(o,u,t0,"\u222D","\\iiint");i(o,u,t0,"\u220F","\\prod");i(o,u,t0,"\u2211","\\sum");i(o,u,t0,"\u2A02","\\bigotimes");i(o,u,t0,"\u2A01","\\bigoplus");i(o,u,t0,"\u2A00","\\bigodot");i(o,u,t0,"\u222E","\\oint");i(o,u,t0,"\u222F","\\oiint");i(o,u,t0,"\u2230","\\oiiint");i(o,u,t0,"\u2A06","\\bigsqcup");i(o,u,t0,"\u222B","\\smallint");i(k,u,ie,"\u2026","\\textellipsis");i(o,u,ie,"\u2026","\\mathellipsis");i(k,u,ie,"\u2026","\\ldots",!0);i(o,u,ie,"\u2026","\\ldots",!0);i(o,u,ie,"\u22EF","\\@cdots",!0);i(o,u,ie,"\u22F1","\\ddots",!0);i(o,u,g,"\u22EE","\\varvdots");i(k,u,g,"\u22EE","\\varvdots");i(o,u,Z,"\u02CA","\\acute");i(o,u,Z,"\u02CB","\\grave");i(o,u,Z,"\xA8","\\ddot");i(o,u,Z,"~","\\tilde");i(o,u,Z,"\u02C9","\\bar");i(o,u,Z,"\u02D8","\\breve");i(o,u,Z,"\u02C7","\\check");i(o,u,Z,"^","\\hat");i(o,u,Z,"\u20D7","\\vec");i(o,u,Z,"\u02D9","\\dot");i(o,u,Z,"\u02DA","\\mathring");i(o,u,I,"\uE131","\\@imath");i(o,u,I,"\uE237","\\@jmath");i(o,u,g,"\u0131","\u0131");i(o,u,g,"\u0237","\u0237");i(k,u,g,"\u0131","\\i",!0);i(k,u,g,"\u0237","\\j",!0);i(k,u,g,"\xDF","\\ss",!0);i(k,u,g,"\xE6","\\ae",!0);i(k,u,g,"\u0153","\\oe",!0);i(k,u,g,"\xF8","\\o",!0);i(k,u,g,"\xC6","\\AE",!0);i(k,u,g,"\u0152","\\OE",!0);i(k,u,g,"\xD8","\\O",!0);i(k,u,Z,"\u02CA","\\'");i(k,u,Z,"\u02CB","\\`");i(k,u,Z,"\u02C6","\\^");i(k,u,Z,"\u02DC","\\~");i(k,u,Z,"\u02C9","\\=");i(k,u,Z,"\u02D8","\\u");i(k,u,Z,"\u02D9","\\.");i(k,u,Z,"\xB8","\\c");i(k,u,Z,"\u02DA","\\r");i(k,u,Z,"\u02C7","\\v");i(k,u,Z,"\xA8",'\\"');i(k,u,Z,"\u02DD","\\H");i(k,u,Z,"\u25EF","\\textcircled");var Cr={"--":!0,"---":!0,"``":!0,"''":!0};i(k,u,g,"\u2013","--",!0);i(k,u,g,"\u2013","\\textendash");i(k,u,g,"\u2014","---",!0);i(k,u,g,"\u2014","\\textemdash");i(k,u,g,"\u2018","`",!0);i(k,u,g,"\u2018","\\textquoteleft");i(k,u,g,"\u2019","'",!0);i(k,u,g,"\u2019","\\textquoteright");i(k,u,g,"\u201C","``",!0);i(k,u,g,"\u201C","\\textquotedblleft");i(k,u,g,"\u201D","''",!0);i(k,u,g,"\u201D","\\textquotedblright");i(o,u,g,"\xB0","\\degree",!0);i(k,u,g,"\xB0","\\degree");i(k,u,g,"\xB0","\\textdegree",!0);i(o,u,g,"\xA3","\\pounds");i(o,u,g,"\xA3","\\mathsterling",!0);i(k,u,g,"\xA3","\\pounds");i(k,u,g,"\xA3","\\textsterling",!0);i(o,d,g,"\u2720","\\maltese");i(k,d,g,"\u2720","\\maltese");var Qt='0123456789/@."';for(Me=0;Me0)return w0(s,f,n,t,l.concat(v));if(c){var b,x;if(c==="boldsymbol"){var w=u1(s,n,t,l,a);b=w.fontName,x=[w.fontClass]}else h?(b=Or[c].fontName,x=[c]):(b=Be(c,t.fontWeight,t.fontShape),x=[c,t.fontWeight,t.fontShape]);if(Ue(s,b,n).metrics)return w0(s,b,n,t,l.concat(x));if(Cr.hasOwnProperty(s)&&b.slice(0,10)==="Typewriter"){for(var A=[],q=0;q{if(G0(r.classes)!==G0(e.classes)||r.skew!==e.skew||r.maxFontSize!==e.maxFontSize)return!1;if(r.classes.length===1){var t=r.classes[0];if(t==="mbin"||t==="mord")return!1}for(var a in r.style)if(r.style.hasOwnProperty(a)&&r.style[a]!==e.style[a])return!1;for(var n in e.style)if(e.style.hasOwnProperty(n)&&r.style[n]!==e.style[n])return!1;return!0},m1=r=>{for(var e=0;et&&(t=l.height),l.depth>a&&(a=l.depth),l.maxFontSize>n&&(n=l.maxFontSize)}e.height=t,e.depth=a,e.maxFontSize=n},h0=function(e,t,a,n){var s=new Z0(e,t,a,n);return qt(s),s},_r=(r,e,t,a)=>new Z0(r,e,t,a),d1=function(e,t,a){var n=h0([e],[],t);return n.height=Math.max(a||t.fontMetrics().defaultRuleThickness,t.minRuleThickness),n.style.borderBottomWidth=T(n.height),n.maxFontSize=1,n},p1=function(e,t,a,n){var s=new fe(e,t,a,n);return qt(s),s},Nr=function(e){var t=new Y0(e);return qt(t),t},f1=function(e,t){return e instanceof Y0?h0([],[e],t):e},v1=function(e){if(e.positionType==="individualShift"){for(var t=e.children,a=[t[0]],n=-t[0].shift-t[0].elem.depth,s=n,l=1;l{var t=h0(["mspace"],[],e),a=Q(r,e);return t.style.marginRight=T(a),t},Be=function(e,t,a){var n="";switch(e){case"amsrm":n="AMS";break;case"textrm":n="Main";break;case"textsf":n="SansSerif";break;case"texttt":n="Typewriter";break;default:n=e}var s;return t==="textbf"&&a==="textit"?s="BoldItalic":t==="textbf"?s="Bold":t==="textit"?s="Italic":s="Regular",n+"-"+s},Or={mathbf:{variant:"bold",fontName:"Main-Bold"},mathrm:{variant:"normal",fontName:"Main-Regular"},textit:{variant:"italic",fontName:"Main-Italic"},mathit:{variant:"italic",fontName:"Main-Italic"},mathnormal:{variant:"italic",fontName:"Math-Italic"},mathsfit:{variant:"sans-serif-italic",fontName:"SansSerif-Italic"},mathbb:{variant:"double-struck",fontName:"AMS-Regular"},mathcal:{variant:"script",fontName:"Caligraphic-Regular"},mathfrak:{variant:"fraktur",fontName:"Fraktur-Regular"},mathscr:{variant:"script",fontName:"Script-Regular"},mathsf:{variant:"sans-serif",fontName:"SansSerif-Regular"},mathtt:{variant:"monospace",fontName:"Typewriter-Regular"}},Ir={vec:["vec",.471,.714],oiintSize1:["oiintSize1",.957,.499],oiintSize2:["oiintSize2",1.472,.659],oiiintSize1:["oiiintSize1",1.304,.499],oiiintSize2:["oiiintSize2",1.98,.659]},y1=function(e,t){var[a,n,s]=Ir[e],l=new A0(a),h=new S0([l],{width:T(n),height:T(s),style:"width:"+T(n),viewBox:"0 0 "+1e3*n+" "+1e3*s,preserveAspectRatio:"xMinYMin"}),c=_r(["overlay"],[h],t);return c.height=s,c.style.height=T(s),c.style.width=T(n),c},y={fontMap:Or,makeSymbol:w0,mathsym:l1,makeSpan:h0,makeSvgSpan:_r,makeLineSpan:d1,makeAnchor:p1,makeFragment:Nr,wrapFragment:f1,makeVList:g1,makeOrd:h1,makeGlue:b1,staticSvg:y1,svgData:Ir,tryCombineChars:m1},J={number:3,unit:"mu"},W0={number:4,unit:"mu"},_0={number:5,unit:"mu"},x1={mord:{mop:J,mbin:W0,mrel:_0,minner:J},mop:{mord:J,mop:J,mrel:_0,minner:J},mbin:{mord:W0,mop:W0,mopen:W0,minner:W0},mrel:{mord:_0,mop:_0,mopen:_0,minner:_0},mopen:{},mclose:{mop:J,mbin:W0,mrel:_0,minner:J},mpunct:{mord:J,mop:J,mrel:_0,mopen:J,mclose:J,mpunct:J,minner:J},minner:{mord:J,mop:J,mbin:W0,mrel:_0,mopen:J,mpunct:J,minner:J}},w1={mord:{mop:J},mop:{mord:J,mop:J},mbin:{},mrel:{},mopen:{},mclose:{mop:J},mpunct:{},minner:{mop:J}},Er={},Le={},Fe={};function B(r){for(var{type:e,names:t,props:a,handler:n,htmlBuilder:s,mathmlBuilder:l}=r,h={type:e,numArgs:a.numArgs,argTypes:a.argTypes,allowedInArgument:!!a.allowedInArgument,allowedInText:!!a.allowedInText,allowedInMath:a.allowedInMath===void 0?!0:a.allowedInMath,numOptionalArgs:a.numOptionalArgs||0,infix:!!a.infix,primitive:!!a.primitive,handler:n},c=0;c{var _=q.classes[0],D=A.classes[0];_==="mbin"&&O.contains(k1,D)?q.classes[0]="mord":D==="mbin"&&O.contains(S1,_)&&(A.classes[0]="mord")},{node:b},x,w),rr(s,(A,q)=>{var _=bt(q),D=bt(A),N=_&&D?A.hasClass("mtight")?w1[_][D]:x1[_][D]:null;if(N)return y.makeGlue(N,f)},{node:b},x,w),s},rr=function r(e,t,a,n,s){n&&e.push(n);for(var l=0;lx=>{e.splice(b+1,0,x),l++})(l)}n&&e.pop()},Rr=function(e){return e instanceof Y0||e instanceof fe||e instanceof Z0&&e.hasClass("enclosing")?e:null},A1=function r(e,t){var a=Rr(e);if(a){var n=a.children;if(n.length){if(t==="right")return r(n[n.length-1],"right");if(t==="left")return r(n[0],"left")}}return e},bt=function(e,t){return e?(t&&(e=A1(e,t)),z1[e.classes[0]]||null):null},ge=function(e,t){var a=["nulldelimiter"].concat(e.baseSizingClasses());return I0(t.concat(a))},G=function(e,t,a){if(!e)return I0();if(Le[e.type]){var n=Le[e.type](e,t);if(a&&t.size!==a.size){n=I0(t.sizingClasses(a),[n],t);var s=t.sizeMultiplier/a.sizeMultiplier;n.height*=s,n.depth*=s}return n}else throw new z("Got group of unknown type: '"+e.type+"'")};function De(r,e){var t=I0(["base"],r,e),a=I0(["strut"]);return a.style.height=T(t.height+t.depth),t.depth&&(a.style.verticalAlign=T(-t.depth)),t.children.unshift(a),t}function yt(r,e){var t=null;r.length===1&&r[0].type==="tag"&&(t=r[0].tag,r=r[0].body);var a=a0(r,e,"root"),n;a.length===2&&a[1].hasClass("tag")&&(n=a.pop());for(var s=[],l=[],h=0;h0&&(s.push(De(l,e)),l=[]),s.push(a[h]));l.length>0&&s.push(De(l,e));var f;t?(f=De(a0(t,e,!0)),f.classes=["tag"],s.push(f)):n&&s.push(n);var v=I0(["katex-html"],s);if(v.setAttribute("aria-hidden","true"),f){var b=f.children[0];b.style.height=T(v.height+v.depth),v.depth&&(b.style.verticalAlign=T(-v.depth))}return v}function $r(r){return new Y0(r)}var s0=class{constructor(e,t,a){this.type=void 0,this.attributes=void 0,this.children=void 0,this.classes=void 0,this.type=e,this.attributes={},this.children=t||[],this.classes=a||[]}setAttribute(e,t){this.attributes[e]=t}getAttribute(e){return this.attributes[e]}toNode(){var e=document.createElementNS("http://www.w3.org/1998/Math/MathML",this.type);for(var t in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,t)&&e.setAttribute(t,this.attributes[t]);this.classes.length>0&&(e.className=G0(this.classes));for(var a=0;a0&&(e+=' class ="'+O.escape(G0(this.classes))+'"'),e+=">";for(var a=0;a",e}toText(){return this.children.map(e=>e.toText()).join("")}},g0=class{constructor(e){this.text=void 0,this.text=e}toNode(){return document.createTextNode(this.text)}toMarkup(){return O.escape(this.toText())}toText(){return this.text}},xt=class{constructor(e){this.width=void 0,this.character=void 0,this.width=e,e>=.05555&&e<=.05556?this.character="\u200A":e>=.1666&&e<=.1667?this.character="\u2009":e>=.2222&&e<=.2223?this.character="\u2005":e>=.2777&&e<=.2778?this.character="\u2005\u200A":e>=-.05556&&e<=-.05555?this.character="\u200A\u2063":e>=-.1667&&e<=-.1666?this.character="\u2009\u2063":e>=-.2223&&e<=-.2222?this.character="\u205F\u2063":e>=-.2778&&e<=-.2777?this.character="\u2005\u2063":this.character=null}toNode(){if(this.character)return document.createTextNode(this.character);var e=document.createElementNS("http://www.w3.org/1998/Math/MathML","mspace");return e.setAttribute("width",T(this.width)),e}toMarkup(){return this.character?""+this.character+" ":' '}toText(){return this.character?this.character:" "}},M={MathNode:s0,TextNode:g0,SpaceNode:xt,newDocumentFragment:$r},y0=function(e,t,a){return Y[t][e]&&Y[t][e].replace&&e.charCodeAt(0)!==55349&&!(Cr.hasOwnProperty(e)&&a&&(a.fontFamily&&a.fontFamily.slice(4,6)==="tt"||a.font&&a.font.slice(4,6)==="tt"))&&(e=Y[t][e].replace),new M.TextNode(e)},Bt=function(e){return e.length===1?e[0]:new M.MathNode("mrow",e)},Dt=function(e,t){if(t.fontFamily==="texttt")return"monospace";if(t.fontFamily==="textsf")return t.fontShape==="textit"&&t.fontWeight==="textbf"?"sans-serif-bold-italic":t.fontShape==="textit"?"sans-serif-italic":t.fontWeight==="textbf"?"bold-sans-serif":"sans-serif";if(t.fontShape==="textit"&&t.fontWeight==="textbf")return"bold-italic";if(t.fontShape==="textit")return"italic";if(t.fontWeight==="textbf")return"bold";var a=t.font;if(!a||a==="mathnormal")return null;var n=e.mode;if(a==="mathit")return"italic";if(a==="boldsymbol")return e.type==="textord"?"bold":"bold-italic";if(a==="mathbf")return"bold";if(a==="mathbb")return"double-struck";if(a==="mathsfit")return"sans-serif-italic";if(a==="mathfrak")return"fraktur";if(a==="mathscr"||a==="mathcal")return"script";if(a==="mathsf")return"sans-serif";if(a==="mathtt")return"monospace";var s=e.text;if(O.contains(["\\imath","\\jmath"],s))return null;Y[n][s]&&Y[n][s].replace&&(s=Y[n][s].replace);var l=y.fontMap[a].fontName;return Tt(s,l,n)?y.fontMap[a].variant:null};function nt(r){if(!r)return!1;if(r.type==="mi"&&r.children.length===1){var e=r.children[0];return e instanceof g0&&e.text==="."}else if(r.type==="mo"&&r.children.length===1&&r.getAttribute("separator")==="true"&&r.getAttribute("lspace")==="0em"&&r.getAttribute("rspace")==="0em"){var t=r.children[0];return t instanceof g0&&t.text===","}else return!1}var m0=function(e,t,a){if(e.length===1){var n=X(e[0],t);return a&&n instanceof s0&&n.type==="mo"&&(n.setAttribute("lspace","0em"),n.setAttribute("rspace","0em")),[n]}for(var s=[],l,h=0;h=1&&(l.type==="mn"||nt(l))){var f=c.children[0];f instanceof s0&&f.type==="mn"&&(f.children=[...l.children,...f.children],s.pop())}else if(l.type==="mi"&&l.children.length===1){var v=l.children[0];if(v instanceof g0&&v.text==="\u0338"&&(c.type==="mo"||c.type==="mi"||c.type==="mn")){var b=c.children[0];b instanceof g0&&b.text.length>0&&(b.text=b.text.slice(0,1)+"\u0338"+b.text.slice(1),s.pop())}}}s.push(c),l=c}return s},V0=function(e,t,a){return Bt(m0(e,t,a))},X=function(e,t){if(!e)return new M.MathNode("mrow");if(Fe[e.type]){var a=Fe[e.type](e,t);return a}else throw new z("Got group of unknown type: '"+e.type+"'")};function ar(r,e,t,a,n){var s=m0(r,t),l;s.length===1&&s[0]instanceof s0&&O.contains(["mrow","mtable"],s[0].type)?l=s[0]:l=new M.MathNode("mrow",s);var h=new M.MathNode("annotation",[new M.TextNode(e)]);h.setAttribute("encoding","application/x-tex");var c=new M.MathNode("semantics",[l,h]),f=new M.MathNode("math",[c]);f.setAttribute("xmlns","http://www.w3.org/1998/Math/MathML"),a&&f.setAttribute("display","block");var v=n?"katex":"katex-mathml";return y.makeSpan([v],[f])}var Lr=function(e){return new Re({style:e.displayMode?E.DISPLAY:E.TEXT,maxSize:e.maxSize,minRuleThickness:e.minRuleThickness})},Fr=function(e,t){if(t.displayMode){var a=["katex-display"];t.leqno&&a.push("leqno"),t.fleqn&&a.push("fleqn"),e=y.makeSpan(a,[e])}return e},T1=function(e,t,a){var n=Lr(a),s;if(a.output==="mathml")return ar(e,t,n,a.displayMode,!0);if(a.output==="html"){var l=yt(e,n);s=y.makeSpan(["katex"],[l])}else{var h=ar(e,t,n,a.displayMode,!1),c=yt(e,n);s=y.makeSpan(["katex"],[h,c])}return Fr(s,a)},q1=function(e,t,a){var n=Lr(a),s=yt(e,n),l=y.makeSpan(["katex"],[s]);return Fr(l,a)},B1={widehat:"^",widecheck:"\u02C7",widetilde:"~",utilde:"~",overleftarrow:"\u2190",underleftarrow:"\u2190",xleftarrow:"\u2190",overrightarrow:"\u2192",underrightarrow:"\u2192",xrightarrow:"\u2192",underbrace:"\u23DF",overbrace:"\u23DE",overgroup:"\u23E0",undergroup:"\u23E1",overleftrightarrow:"\u2194",underleftrightarrow:"\u2194",xleftrightarrow:"\u2194",Overrightarrow:"\u21D2",xRightarrow:"\u21D2",overleftharpoon:"\u21BC",xleftharpoonup:"\u21BC",overrightharpoon:"\u21C0",xrightharpoonup:"\u21C0",xLeftarrow:"\u21D0",xLeftrightarrow:"\u21D4",xhookleftarrow:"\u21A9",xhookrightarrow:"\u21AA",xmapsto:"\u21A6",xrightharpoondown:"\u21C1",xleftharpoondown:"\u21BD",xrightleftharpoons:"\u21CC",xleftrightharpoons:"\u21CB",xtwoheadleftarrow:"\u219E",xtwoheadrightarrow:"\u21A0",xlongequal:"=",xtofrom:"\u21C4",xrightleftarrows:"\u21C4",xrightequilibrium:"\u21CC",xleftequilibrium:"\u21CB","\\cdrightarrow":"\u2192","\\cdleftarrow":"\u2190","\\cdlongequal":"="},D1=function(e){var t=new M.MathNode("mo",[new M.TextNode(B1[e.replace(/^\\/,"")])]);return t.setAttribute("stretchy","true"),t},C1={overrightarrow:[["rightarrow"],.888,522,"xMaxYMin"],overleftarrow:[["leftarrow"],.888,522,"xMinYMin"],underrightarrow:[["rightarrow"],.888,522,"xMaxYMin"],underleftarrow:[["leftarrow"],.888,522,"xMinYMin"],xrightarrow:[["rightarrow"],1.469,522,"xMaxYMin"],"\\cdrightarrow":[["rightarrow"],3,522,"xMaxYMin"],xleftarrow:[["leftarrow"],1.469,522,"xMinYMin"],"\\cdleftarrow":[["leftarrow"],3,522,"xMinYMin"],Overrightarrow:[["doublerightarrow"],.888,560,"xMaxYMin"],xRightarrow:[["doublerightarrow"],1.526,560,"xMaxYMin"],xLeftarrow:[["doubleleftarrow"],1.526,560,"xMinYMin"],overleftharpoon:[["leftharpoon"],.888,522,"xMinYMin"],xleftharpoonup:[["leftharpoon"],.888,522,"xMinYMin"],xleftharpoondown:[["leftharpoondown"],.888,522,"xMinYMin"],overrightharpoon:[["rightharpoon"],.888,522,"xMaxYMin"],xrightharpoonup:[["rightharpoon"],.888,522,"xMaxYMin"],xrightharpoondown:[["rightharpoondown"],.888,522,"xMaxYMin"],xlongequal:[["longequal"],.888,334,"xMinYMin"],"\\cdlongequal":[["longequal"],3,334,"xMinYMin"],xtwoheadleftarrow:[["twoheadleftarrow"],.888,334,"xMinYMin"],xtwoheadrightarrow:[["twoheadrightarrow"],.888,334,"xMaxYMin"],overleftrightarrow:[["leftarrow","rightarrow"],.888,522],overbrace:[["leftbrace","midbrace","rightbrace"],1.6,548],underbrace:[["leftbraceunder","midbraceunder","rightbraceunder"],1.6,548],underleftrightarrow:[["leftarrow","rightarrow"],.888,522],xleftrightarrow:[["leftarrow","rightarrow"],1.75,522],xLeftrightarrow:[["doubleleftarrow","doublerightarrow"],1.75,560],xrightleftharpoons:[["leftharpoondownplus","rightharpoonplus"],1.75,716],xleftrightharpoons:[["leftharpoonplus","rightharpoondownplus"],1.75,716],xhookleftarrow:[["leftarrow","righthook"],1.08,522],xhookrightarrow:[["lefthook","rightarrow"],1.08,522],overlinesegment:[["leftlinesegment","rightlinesegment"],.888,522],underlinesegment:[["leftlinesegment","rightlinesegment"],.888,522],overgroup:[["leftgroup","rightgroup"],.888,342],undergroup:[["leftgroupunder","rightgroupunder"],.888,342],xmapsto:[["leftmapsto","rightarrow"],1.5,522],xtofrom:[["leftToFrom","rightToFrom"],1.75,528],xrightleftarrows:[["baraboveleftarrow","rightarrowabovebar"],1.75,901],xrightequilibrium:[["baraboveshortleftharpoon","rightharpoonaboveshortbar"],1.75,716],xleftequilibrium:[["shortbaraboveleftharpoon","shortrightharpoonabovebar"],1.75,716]},_1=function(e){return e.type==="ordgroup"?e.body.length:1},N1=function(e,t){function a(){var h=4e5,c=e.label.slice(1);if(O.contains(["widehat","widecheck","widetilde","utilde"],c)){var f=e,v=_1(f.base),b,x,w;if(v>5)c==="widehat"||c==="widecheck"?(b=420,h=2364,w=.42,x=c+"4"):(b=312,h=2340,w=.34,x="tilde4");else{var A=[1,1,2,2,3,3][v];c==="widehat"||c==="widecheck"?(h=[0,1062,2364,2364,2364][A],b=[0,239,300,360,420][A],w=[0,.24,.3,.3,.36,.42][A],x=c+A):(h=[0,600,1033,2339,2340][A],b=[0,260,286,306,312][A],w=[0,.26,.286,.3,.306,.34][A],x="tilde"+A)}var q=new A0(x),_=new S0([q],{width:"100%",height:T(w),viewBox:"0 0 "+h+" "+b,preserveAspectRatio:"none"});return{span:y.makeSvgSpan([],[_],t),minWidth:0,height:w}}else{var D=[],N=C1[c],[$,H,F]=N,P=F/1e3,V=$.length,j,U;if(V===1){var D0=N[3];j=["hide-tail"],U=[D0]}else if(V===2)j=["halfarrow-left","halfarrow-right"],U=["xMinYMin","xMaxYMin"];else if(V===3)j=["brace-left","brace-center","brace-right"],U=["xMinYMin","xMidYMin","xMaxYMin"];else throw new Error(`Correct katexImagesData or update code here to support
+ `+V+" children.");for(var i0=0;i00&&(n.style.minWidth=T(s)),n},O1=function(e,t,a,n,s){var l,h=e.height+e.depth+a+n;if(/fbox|color|angl/.test(t)){if(l=y.makeSpan(["stretchy",t],[],s),t==="fbox"){var c=s.color&&s.getColor();c&&(l.style.borderColor=c)}}else{var f=[];/^[bx]cancel$/.test(t)&&f.push(new ve({x1:"0",y1:"0",x2:"100%",y2:"100%","stroke-width":"0.046em"})),/^x?cancel$/.test(t)&&f.push(new ve({x1:"0",y1:"100%",x2:"100%",y2:"0","stroke-width":"0.046em"}));var v=new S0(f,{width:"100%",height:T(h)});l=y.makeSvgSpan([],[v],s)}return l.height=h,l.style.height=T(h),l},E0={encloseSpan:O1,mathMLnode:D1,svgSpan:N1};function L(r,e){if(!r||r.type!==e)throw new Error("Expected node of type "+e+", but got "+(r?"node of type "+r.type:String(r)));return r}function Ct(r){var e=Xe(r);if(!e)throw new Error("Expected node of symbol group type, but got "+(r?"node of type "+r.type:String(r)));return e}function Xe(r){return r&&(r.type==="atom"||s1.hasOwnProperty(r.type))?r:null}var _t=(r,e)=>{var t,a,n;r&&r.type==="supsub"?(a=L(r.base,"accent"),t=a.base,r.base=t,n=n1(G(r,e)),r.base=a):(a=L(r,"accent"),t=a.base);var s=G(t,e.havingCrampedStyle()),l=a.isShifty&&O.isCharacterBox(t),h=0;if(l){var c=O.getBaseElem(t),f=G(c,e.havingCrampedStyle());h=Jt(f).skew}var v=a.label==="\\c",b=v?s.height+s.depth:Math.min(s.height,e.fontMetrics().xHeight),x;if(a.isStretchy)x=E0.svgSpan(a,e),x=y.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:s},{type:"elem",elem:x,wrapperClasses:["svg-align"],wrapperStyle:h>0?{width:"calc(100% - "+T(2*h)+")",marginLeft:T(2*h)}:void 0}]},e);else{var w,A;a.label==="\\vec"?(w=y.staticSvg("vec",e),A=y.svgData.vec[1]):(w=y.makeOrd({mode:a.mode,text:a.label},e,"textord"),w=Jt(w),w.italic=0,A=w.width,v&&(b+=w.depth)),x=y.makeSpan(["accent-body"],[w]);var q=a.label==="\\textcircled";q&&(x.classes.push("accent-full"),b=s.height);var _=h;q||(_-=A/2),x.style.left=T(_),a.label==="\\textcircled"&&(x.style.top=".2em"),x=y.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:s},{type:"kern",size:-b},{type:"elem",elem:x}]},e)}var D=y.makeSpan(["mord","accent"],[x],e);return n?(n.children[0]=D,n.height=Math.max(D.height,n.height),n.classes[0]="mord",n):D},Hr=(r,e)=>{var t=r.isStretchy?E0.mathMLnode(r.label):new M.MathNode("mo",[y0(r.label,r.mode)]),a=new M.MathNode("mover",[X(r.base,e),t]);return a.setAttribute("accent","true"),a},I1=new RegExp(["\\acute","\\grave","\\ddot","\\tilde","\\bar","\\breve","\\check","\\hat","\\vec","\\dot","\\mathring"].map(r=>"\\"+r).join("|"));B({type:"accent",names:["\\acute","\\grave","\\ddot","\\tilde","\\bar","\\breve","\\check","\\hat","\\vec","\\dot","\\mathring","\\widecheck","\\widehat","\\widetilde","\\overrightarrow","\\overleftarrow","\\Overrightarrow","\\overleftrightarrow","\\overgroup","\\overlinesegment","\\overleftharpoon","\\overrightharpoon"],props:{numArgs:1},handler:(r,e)=>{var t=He(e[0]),a=!I1.test(r.funcName),n=!a||r.funcName==="\\widehat"||r.funcName==="\\widetilde"||r.funcName==="\\widecheck";return{type:"accent",mode:r.parser.mode,label:r.funcName,isStretchy:a,isShifty:n,base:t}},htmlBuilder:_t,mathmlBuilder:Hr});B({type:"accent",names:["\\'","\\`","\\^","\\~","\\=","\\u","\\.",'\\"',"\\c","\\r","\\H","\\v","\\textcircled"],props:{numArgs:1,allowedInText:!0,allowedInMath:!0,argTypes:["primitive"]},handler:(r,e)=>{var t=e[0],a=r.parser.mode;return a==="math"&&(r.parser.settings.reportNonstrict("mathVsTextAccents","LaTeX's accent "+r.funcName+" works only in text mode"),a="text"),{type:"accent",mode:a,label:r.funcName,isStretchy:!1,isShifty:!0,base:t}},htmlBuilder:_t,mathmlBuilder:Hr});B({type:"accentUnder",names:["\\underleftarrow","\\underrightarrow","\\underleftrightarrow","\\undergroup","\\underlinesegment","\\utilde"],props:{numArgs:1},handler:(r,e)=>{var{parser:t,funcName:a}=r,n=e[0];return{type:"accentUnder",mode:t.mode,label:a,base:n}},htmlBuilder:(r,e)=>{var t=G(r.base,e),a=E0.svgSpan(r,e),n=r.label==="\\utilde"?.12:0,s=y.makeVList({positionType:"top",positionData:t.height,children:[{type:"elem",elem:a,wrapperClasses:["svg-align"]},{type:"kern",size:n},{type:"elem",elem:t}]},e);return y.makeSpan(["mord","accentunder"],[s],e)},mathmlBuilder:(r,e)=>{var t=E0.mathMLnode(r.label),a=new M.MathNode("munder",[X(r.base,e),t]);return a.setAttribute("accentunder","true"),a}});var Ce=r=>{var e=new M.MathNode("mpadded",r?[r]:[]);return e.setAttribute("width","+0.6em"),e.setAttribute("lspace","0.3em"),e};B({type:"xArrow",names:["\\xleftarrow","\\xrightarrow","\\xLeftarrow","\\xRightarrow","\\xleftrightarrow","\\xLeftrightarrow","\\xhookleftarrow","\\xhookrightarrow","\\xmapsto","\\xrightharpoondown","\\xrightharpoonup","\\xleftharpoondown","\\xleftharpoonup","\\xrightleftharpoons","\\xleftrightharpoons","\\xlongequal","\\xtwoheadrightarrow","\\xtwoheadleftarrow","\\xtofrom","\\xrightleftarrows","\\xrightequilibrium","\\xleftequilibrium","\\\\cdrightarrow","\\\\cdleftarrow","\\\\cdlongequal"],props:{numArgs:1,numOptionalArgs:1},handler(r,e,t){var{parser:a,funcName:n}=r;return{type:"xArrow",mode:a.mode,label:n,body:e[0],below:t[0]}},htmlBuilder(r,e){var t=e.style,a=e.havingStyle(t.sup()),n=y.wrapFragment(G(r.body,a,e),e),s=r.label.slice(0,2)==="\\x"?"x":"cd";n.classes.push(s+"-arrow-pad");var l;r.below&&(a=e.havingStyle(t.sub()),l=y.wrapFragment(G(r.below,a,e),e),l.classes.push(s+"-arrow-pad"));var h=E0.svgSpan(r,e),c=-e.fontMetrics().axisHeight+.5*h.height,f=-e.fontMetrics().axisHeight-.5*h.height-.111;(n.depth>.25||r.label==="\\xleftequilibrium")&&(f-=n.depth);var v;if(l){var b=-e.fontMetrics().axisHeight+l.height+.5*h.height+.111;v=y.makeVList({positionType:"individualShift",children:[{type:"elem",elem:n,shift:f},{type:"elem",elem:h,shift:c},{type:"elem",elem:l,shift:b}]},e)}else v=y.makeVList({positionType:"individualShift",children:[{type:"elem",elem:n,shift:f},{type:"elem",elem:h,shift:c}]},e);return v.children[0].children[0].children[1].classes.push("svg-align"),y.makeSpan(["mrel","x-arrow"],[v],e)},mathmlBuilder(r,e){var t=E0.mathMLnode(r.label);t.setAttribute("minsize",r.label.charAt(0)==="x"?"1.75em":"3.0em");var a;if(r.body){var n=Ce(X(r.body,e));if(r.below){var s=Ce(X(r.below,e));a=new M.MathNode("munderover",[t,s,n])}else a=new M.MathNode("mover",[t,n])}else if(r.below){var l=Ce(X(r.below,e));a=new M.MathNode("munder",[t,l])}else a=Ce(),a=new M.MathNode("mover",[t,a]);return a}});var E1=y.makeSpan;function Pr(r,e){var t=a0(r.body,e,!0);return E1([r.mclass],t,e)}function Gr(r,e){var t,a=m0(r.body,e);return r.mclass==="minner"?t=new M.MathNode("mpadded",a):r.mclass==="mord"?r.isCharacterBox?(t=a[0],t.type="mi"):t=new M.MathNode("mi",a):(r.isCharacterBox?(t=a[0],t.type="mo"):t=new M.MathNode("mo",a),r.mclass==="mbin"?(t.attributes.lspace="0.22em",t.attributes.rspace="0.22em"):r.mclass==="mpunct"?(t.attributes.lspace="0em",t.attributes.rspace="0.17em"):r.mclass==="mopen"||r.mclass==="mclose"?(t.attributes.lspace="0em",t.attributes.rspace="0em"):r.mclass==="minner"&&(t.attributes.lspace="0.0556em",t.attributes.width="+0.1111em")),t}B({type:"mclass",names:["\\mathord","\\mathbin","\\mathrel","\\mathopen","\\mathclose","\\mathpunct","\\mathinner"],props:{numArgs:1,primitive:!0},handler(r,e){var{parser:t,funcName:a}=r,n=e[0];return{type:"mclass",mode:t.mode,mclass:"m"+a.slice(5),body:e0(n),isCharacterBox:O.isCharacterBox(n)}},htmlBuilder:Pr,mathmlBuilder:Gr});var We=r=>{var e=r.type==="ordgroup"&&r.body.length?r.body[0]:r;return e.type==="atom"&&(e.family==="bin"||e.family==="rel")?"m"+e.family:"mord"};B({type:"mclass",names:["\\@binrel"],props:{numArgs:2},handler(r,e){var{parser:t}=r;return{type:"mclass",mode:t.mode,mclass:We(e[0]),body:e0(e[1]),isCharacterBox:O.isCharacterBox(e[1])}}});B({type:"mclass",names:["\\stackrel","\\overset","\\underset"],props:{numArgs:2},handler(r,e){var{parser:t,funcName:a}=r,n=e[1],s=e[0],l;a!=="\\stackrel"?l=We(n):l="mrel";var h={type:"op",mode:n.mode,limits:!0,alwaysHandleSupSub:!0,parentIsSupSub:!1,symbol:!1,suppressBaseShift:a!=="\\stackrel",body:e0(n)},c={type:"supsub",mode:s.mode,base:h,sup:a==="\\underset"?null:s,sub:a==="\\underset"?s:null};return{type:"mclass",mode:t.mode,mclass:l,body:[c],isCharacterBox:O.isCharacterBox(c)}},htmlBuilder:Pr,mathmlBuilder:Gr});B({type:"pmb",names:["\\pmb"],props:{numArgs:1,allowedInText:!0},handler(r,e){var{parser:t}=r;return{type:"pmb",mode:t.mode,mclass:We(e[0]),body:e0(e[0])}},htmlBuilder(r,e){var t=a0(r.body,e,!0),a=y.makeSpan([r.mclass],t,e);return a.style.textShadow="0.02em 0.01em 0.04px",a},mathmlBuilder(r,e){var t=m0(r.body,e),a=new M.MathNode("mstyle",t);return a.setAttribute("style","text-shadow: 0.02em 0.01em 0.04px"),a}});var R1={">":"\\\\cdrightarrow","<":"\\\\cdleftarrow","=":"\\\\cdlongequal",A:"\\uparrow",V:"\\downarrow","|":"\\Vert",".":"no arrow"},nr=()=>({type:"styling",body:[],mode:"math",style:"display"}),ir=r=>r.type==="textord"&&r.text==="@",$1=(r,e)=>(r.type==="mathord"||r.type==="atom")&&r.text===e;function L1(r,e,t){var a=R1[r];switch(a){case"\\\\cdrightarrow":case"\\\\cdleftarrow":return t.callFunction(a,[e[0]],[e[1]]);case"\\uparrow":case"\\downarrow":{var n=t.callFunction("\\\\cdleft",[e[0]],[]),s={type:"atom",text:a,mode:"math",family:"rel"},l=t.callFunction("\\Big",[s],[]),h=t.callFunction("\\\\cdright",[e[1]],[]),c={type:"ordgroup",mode:"math",body:[n,l,h]};return t.callFunction("\\\\cdparent",[c],[])}case"\\\\cdlongequal":return t.callFunction("\\\\cdlongequal",[],[]);case"\\Vert":{var f={type:"textord",text:"\\Vert",mode:"math"};return t.callFunction("\\Big",[f],[])}default:return{type:"textord",text:" ",mode:"math"}}}function F1(r){var e=[];for(r.gullet.beginGroup(),r.gullet.macros.set("\\cr","\\\\\\relax"),r.gullet.beginGroup();;){e.push(r.parseExpression(!1,"\\\\")),r.gullet.endGroup(),r.gullet.beginGroup();var t=r.fetch().text;if(t==="&"||t==="\\\\")r.consume();else if(t==="\\end"){e[e.length-1].length===0&&e.pop();break}else throw new z("Expected \\\\ or \\cr or \\end",r.nextToken)}for(var a=[],n=[a],s=0;s-1))if("<>AV".indexOf(f)>-1)for(var b=0;b<2;b++){for(var x=!0,w=c+1;wAV=|." after @',l[c]);var A=L1(f,v,r),q={type:"styling",body:[A],mode:"math",style:"display"};a.push(q),h=nr()}s%2===0?a.push(h):a.shift(),a=[],n.push(a)}r.gullet.endGroup(),r.gullet.endGroup();var _=new Array(n[0].length).fill({type:"align",align:"c",pregap:.25,postgap:.25});return{type:"array",mode:"math",body:n,arraystretch:1,addJot:!0,rowGaps:[null],cols:_,colSeparationType:"CD",hLinesBeforeRow:new Array(n.length+1).fill([])}}B({type:"cdlabel",names:["\\\\cdleft","\\\\cdright"],props:{numArgs:1},handler(r,e){var{parser:t,funcName:a}=r;return{type:"cdlabel",mode:t.mode,side:a.slice(4),label:e[0]}},htmlBuilder(r,e){var t=e.havingStyle(e.style.sup()),a=y.wrapFragment(G(r.label,t,e),e);return a.classes.push("cd-label-"+r.side),a.style.bottom=T(.8-a.depth),a.height=0,a.depth=0,a},mathmlBuilder(r,e){var t=new M.MathNode("mrow",[X(r.label,e)]);return t=new M.MathNode("mpadded",[t]),t.setAttribute("width","0"),r.side==="left"&&t.setAttribute("lspace","-1width"),t.setAttribute("voffset","0.7em"),t=new M.MathNode("mstyle",[t]),t.setAttribute("displaystyle","false"),t.setAttribute("scriptlevel","1"),t}});B({type:"cdlabelparent",names:["\\\\cdparent"],props:{numArgs:1},handler(r,e){var{parser:t}=r;return{type:"cdlabelparent",mode:t.mode,fragment:e[0]}},htmlBuilder(r,e){var t=y.wrapFragment(G(r.fragment,e),e);return t.classes.push("cd-vert-arrow"),t},mathmlBuilder(r,e){return new M.MathNode("mrow",[X(r.fragment,e)])}});B({type:"textord",names:["\\@char"],props:{numArgs:1,allowedInText:!0},handler(r,e){for(var{parser:t}=r,a=L(e[0],"ordgroup"),n=a.body,s="",l=0;l=1114111)throw new z("\\@char with invalid code point "+s);return c<=65535?f=String.fromCharCode(c):(c-=65536,f=String.fromCharCode((c>>10)+55296,(c&1023)+56320)),{type:"textord",mode:t.mode,text:f}}});var Vr=(r,e)=>{var t=a0(r.body,e.withColor(r.color),!1);return y.makeFragment(t)},Ur=(r,e)=>{var t=m0(r.body,e.withColor(r.color)),a=new M.MathNode("mstyle",t);return a.setAttribute("mathcolor",r.color),a};B({type:"color",names:["\\textcolor"],props:{numArgs:2,allowedInText:!0,argTypes:["color","original"]},handler(r,e){var{parser:t}=r,a=L(e[0],"color-token").color,n=e[1];return{type:"color",mode:t.mode,color:a,body:e0(n)}},htmlBuilder:Vr,mathmlBuilder:Ur});B({type:"color",names:["\\color"],props:{numArgs:1,allowedInText:!0,argTypes:["color"]},handler(r,e){var{parser:t,breakOnTokenText:a}=r,n=L(e[0],"color-token").color;t.gullet.macros.set("\\current@color",n);var s=t.parseExpression(!0,a);return{type:"color",mode:t.mode,color:n,body:s}},htmlBuilder:Vr,mathmlBuilder:Ur});B({type:"cr",names:["\\\\"],props:{numArgs:0,numOptionalArgs:0,allowedInText:!0},handler(r,e,t){var{parser:a}=r,n=a.gullet.future().text==="["?a.parseSizeGroup(!0):null,s=!a.settings.displayMode||!a.settings.useStrictBehavior("newLineInDisplayMode","In LaTeX, \\\\ or \\newline does nothing in display mode");return{type:"cr",mode:a.mode,newLine:s,size:n&&L(n,"size").value}},htmlBuilder(r,e){var t=y.makeSpan(["mspace"],[],e);return r.newLine&&(t.classes.push("newline"),r.size&&(t.style.marginTop=T(Q(r.size,e)))),t},mathmlBuilder(r,e){var t=new M.MathNode("mspace");return r.newLine&&(t.setAttribute("linebreak","newline"),r.size&&t.setAttribute("height",T(Q(r.size,e)))),t}});var wt={"\\global":"\\global","\\long":"\\\\globallong","\\\\globallong":"\\\\globallong","\\def":"\\gdef","\\gdef":"\\gdef","\\edef":"\\xdef","\\xdef":"\\xdef","\\let":"\\\\globallet","\\futurelet":"\\\\globalfuture"},Xr=r=>{var e=r.text;if(/^(?:[\\{}$^_]|EOF)$/.test(e))throw new z("Expected a control sequence",r);return e},H1=r=>{var e=r.gullet.popToken();return e.text==="="&&(e=r.gullet.popToken(),e.text===" "&&(e=r.gullet.popToken())),e},Wr=(r,e,t,a)=>{var n=r.gullet.macros.get(t.text);n==null&&(t.noexpand=!0,n={tokens:[t],numArgs:0,unexpandable:!r.gullet.isExpandable(t.text)}),r.gullet.macros.set(e,n,a)};B({type:"internal",names:["\\global","\\long","\\\\globallong"],props:{numArgs:0,allowedInText:!0},handler(r){var{parser:e,funcName:t}=r;e.consumeSpaces();var a=e.fetch();if(wt[a.text])return(t==="\\global"||t==="\\\\globallong")&&(a.text=wt[a.text]),L(e.parseFunction(),"internal");throw new z("Invalid token after macro prefix",a)}});B({type:"internal",names:["\\def","\\gdef","\\edef","\\xdef"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler(r){var{parser:e,funcName:t}=r,a=e.gullet.popToken(),n=a.text;if(/^(?:[\\{}$^_]|EOF)$/.test(n))throw new z("Expected a control sequence",a);for(var s=0,l,h=[[]];e.gullet.future().text!=="{";)if(a=e.gullet.popToken(),a.text==="#"){if(e.gullet.future().text==="{"){l=e.gullet.future(),h[s].push("{");break}if(a=e.gullet.popToken(),!/^[1-9]$/.test(a.text))throw new z('Invalid argument number "'+a.text+'"');if(parseInt(a.text)!==s+1)throw new z('Argument number "'+a.text+'" out of order');s++,h.push([])}else{if(a.text==="EOF")throw new z("Expected a macro definition");h[s].push(a.text)}var{tokens:c}=e.gullet.consumeArg();return l&&c.unshift(l),(t==="\\edef"||t==="\\xdef")&&(c=e.gullet.expandTokens(c),c.reverse()),e.gullet.macros.set(n,{tokens:c,numArgs:s,delimiters:h},t===wt[t]),{type:"internal",mode:e.mode}}});B({type:"internal",names:["\\let","\\\\globallet"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler(r){var{parser:e,funcName:t}=r,a=Xr(e.gullet.popToken());e.gullet.consumeSpaces();var n=H1(e);return Wr(e,a,n,t==="\\\\globallet"),{type:"internal",mode:e.mode}}});B({type:"internal",names:["\\futurelet","\\\\globalfuture"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler(r){var{parser:e,funcName:t}=r,a=Xr(e.gullet.popToken()),n=e.gullet.popToken(),s=e.gullet.popToken();return Wr(e,a,s,t==="\\\\globalfuture"),e.gullet.pushToken(s),e.gullet.pushToken(n),{type:"internal",mode:e.mode}}});var ce=function(e,t,a){var n=Y.math[e]&&Y.math[e].replace,s=Tt(n||e,t,a);if(!s)throw new Error("Unsupported symbol "+e+" and font size "+t+".");return s},Nt=function(e,t,a,n){var s=a.havingBaseStyle(t),l=y.makeSpan(n.concat(s.sizingClasses(a)),[e],a),h=s.sizeMultiplier/a.sizeMultiplier;return l.height*=h,l.depth*=h,l.maxFontSize=s.sizeMultiplier,l},Yr=function(e,t,a){var n=t.havingBaseStyle(a),s=(1-t.sizeMultiplier/n.sizeMultiplier)*t.fontMetrics().axisHeight;e.classes.push("delimcenter"),e.style.top=T(s),e.height-=s,e.depth+=s},P1=function(e,t,a,n,s,l){var h=y.makeSymbol(e,"Main-Regular",s,n),c=Nt(h,t,n,l);return a&&Yr(c,n,t),c},G1=function(e,t,a,n){return y.makeSymbol(e,"Size"+t+"-Regular",a,n)},Zr=function(e,t,a,n,s,l){var h=G1(e,t,s,n),c=Nt(y.makeSpan(["delimsizing","size"+t],[h],n),E.TEXT,n,l);return a&&Yr(c,n,E.TEXT),c},it=function(e,t,a){var n;t==="Size1-Regular"?n="delim-size1":n="delim-size4";var s=y.makeSpan(["delimsizinginner",n],[y.makeSpan([],[y.makeSymbol(e,t,a)])]);return{type:"elem",elem:s}},st=function(e,t,a){var n=z0["Size4-Regular"][e.charCodeAt(0)]?z0["Size4-Regular"][e.charCodeAt(0)][4]:z0["Size1-Regular"][e.charCodeAt(0)][4],s=new A0("inner",ja(e,Math.round(1e3*t))),l=new S0([s],{width:T(n),height:T(t),style:"width:"+T(n),viewBox:"0 0 "+1e3*n+" "+Math.round(1e3*t),preserveAspectRatio:"xMinYMin"}),h=y.makeSvgSpan([],[l],a);return h.height=t,h.style.height=T(t),h.style.width=T(n),{type:"elem",elem:h}},St=.008,_e={type:"kern",size:-1*St},V1=["|","\\lvert","\\rvert","\\vert"],U1=["\\|","\\lVert","\\rVert","\\Vert"],jr=function(e,t,a,n,s,l){var h,c,f,v,b="",x=0;h=f=v=e,c=null;var w="Size1-Regular";e==="\\uparrow"?f=v="\u23D0":e==="\\Uparrow"?f=v="\u2016":e==="\\downarrow"?h=f="\u23D0":e==="\\Downarrow"?h=f="\u2016":e==="\\updownarrow"?(h="\\uparrow",f="\u23D0",v="\\downarrow"):e==="\\Updownarrow"?(h="\\Uparrow",f="\u2016",v="\\Downarrow"):O.contains(V1,e)?(f="\u2223",b="vert",x=333):O.contains(U1,e)?(f="\u2225",b="doublevert",x=556):e==="["||e==="\\lbrack"?(h="\u23A1",f="\u23A2",v="\u23A3",w="Size4-Regular",b="lbrack",x=667):e==="]"||e==="\\rbrack"?(h="\u23A4",f="\u23A5",v="\u23A6",w="Size4-Regular",b="rbrack",x=667):e==="\\lfloor"||e==="\u230A"?(f=h="\u23A2",v="\u23A3",w="Size4-Regular",b="lfloor",x=667):e==="\\lceil"||e==="\u2308"?(h="\u23A1",f=v="\u23A2",w="Size4-Regular",b="lceil",x=667):e==="\\rfloor"||e==="\u230B"?(f=h="\u23A5",v="\u23A6",w="Size4-Regular",b="rfloor",x=667):e==="\\rceil"||e==="\u2309"?(h="\u23A4",f=v="\u23A5",w="Size4-Regular",b="rceil",x=667):e==="("||e==="\\lparen"?(h="\u239B",f="\u239C",v="\u239D",w="Size4-Regular",b="lparen",x=875):e===")"||e==="\\rparen"?(h="\u239E",f="\u239F",v="\u23A0",w="Size4-Regular",b="rparen",x=875):e==="\\{"||e==="\\lbrace"?(h="\u23A7",c="\u23A8",v="\u23A9",f="\u23AA",w="Size4-Regular"):e==="\\}"||e==="\\rbrace"?(h="\u23AB",c="\u23AC",v="\u23AD",f="\u23AA",w="Size4-Regular"):e==="\\lgroup"||e==="\u27EE"?(h="\u23A7",v="\u23A9",f="\u23AA",w="Size4-Regular"):e==="\\rgroup"||e==="\u27EF"?(h="\u23AB",v="\u23AD",f="\u23AA",w="Size4-Regular"):e==="\\lmoustache"||e==="\u23B0"?(h="\u23A7",v="\u23AD",f="\u23AA",w="Size4-Regular"):(e==="\\rmoustache"||e==="\u23B1")&&(h="\u23AB",v="\u23A9",f="\u23AA",w="Size4-Regular");var A=ce(h,w,s),q=A.height+A.depth,_=ce(f,w,s),D=_.height+_.depth,N=ce(v,w,s),$=N.height+N.depth,H=0,F=1;if(c!==null){var P=ce(c,w,s);H=P.height+P.depth,F=2}var V=q+$+H,j=Math.max(0,Math.ceil((t-V)/(F*D))),U=V+j*F*D,D0=n.fontMetrics().axisHeight;a&&(D0*=n.sizeMultiplier);var i0=U/2-D0,r0=[];if(b.length>0){var X0=U-q-$,u0=Math.round(U*1e3),x0=Ka(b,Math.round(X0*1e3)),$0=new A0(b,x0),K0=(x/1e3).toFixed(3)+"em",J0=(u0/1e3).toFixed(3)+"em",je=new S0([$0],{width:K0,height:J0,viewBox:"0 0 "+x+" "+u0}),L0=y.makeSvgSpan([],[je],n);L0.height=u0/1e3,L0.style.width=K0,L0.style.height=J0,r0.push({type:"elem",elem:L0})}else{if(r0.push(it(v,w,s)),r0.push(_e),c===null){var F0=U-q-$+2*St;r0.push(st(f,F0,n))}else{var f0=(U-q-$-H)/2+2*St;r0.push(st(f,f0,n)),r0.push(_e),r0.push(it(c,w,s)),r0.push(_e),r0.push(st(f,f0,n))}r0.push(_e),r0.push(it(h,w,s))}var le=n.havingBaseStyle(E.TEXT),Ke=y.makeVList({positionType:"bottom",positionData:i0,children:r0},le);return Nt(y.makeSpan(["delimsizing","mult"],[Ke],le),E.TEXT,n,l)},ot=80,lt=.08,ut=function(e,t,a,n,s){var l=Za(e,n,a),h=new A0(e,l),c=new S0([h],{width:"400em",height:T(t),viewBox:"0 0 400000 "+a,preserveAspectRatio:"xMinYMin slice"});return y.makeSvgSpan(["hide-tail"],[c],s)},X1=function(e,t){var a=t.havingBaseSizing(),n=ea("\\surd",e*a.sizeMultiplier,Qr,a),s=a.sizeMultiplier,l=Math.max(0,t.minRuleThickness-t.fontMetrics().sqrtRuleThickness),h,c=0,f=0,v=0,b;return n.type==="small"?(v=1e3+1e3*l+ot,e<1?s=1:e<1.4&&(s=.7),c=(1+l+lt)/s,f=(1+l)/s,h=ut("sqrtMain",c,v,l,t),h.style.minWidth="0.853em",b=.833/s):n.type==="large"?(v=(1e3+ot)*me[n.size],f=(me[n.size]+l)/s,c=(me[n.size]+l+lt)/s,h=ut("sqrtSize"+n.size,c,v,l,t),h.style.minWidth="1.02em",b=1/s):(c=e+l+lt,f=e+l,v=Math.floor(1e3*e+l)+ot,h=ut("sqrtTall",c,v,l,t),h.style.minWidth="0.742em",b=1.056),h.height=f,h.style.height=T(c),{span:h,advanceWidth:b,ruleWidth:(t.fontMetrics().sqrtRuleThickness+l)*s}},Kr=["(","\\lparen",")","\\rparen","[","\\lbrack","]","\\rbrack","\\{","\\lbrace","\\}","\\rbrace","\\lfloor","\\rfloor","\u230A","\u230B","\\lceil","\\rceil","\u2308","\u2309","\\surd"],W1=["\\uparrow","\\downarrow","\\updownarrow","\\Uparrow","\\Downarrow","\\Updownarrow","|","\\|","\\vert","\\Vert","\\lvert","\\rvert","\\lVert","\\rVert","\\lgroup","\\rgroup","\u27EE","\u27EF","\\lmoustache","\\rmoustache","\u23B0","\u23B1"],Jr=["<",">","\\langle","\\rangle","/","\\backslash","\\lt","\\gt"],me=[0,1.2,1.8,2.4,3],Y1=function(e,t,a,n,s){if(e==="<"||e==="\\lt"||e==="\u27E8"?e="\\langle":(e===">"||e==="\\gt"||e==="\u27E9")&&(e="\\rangle"),O.contains(Kr,e)||O.contains(Jr,e))return Zr(e,t,!1,a,n,s);if(O.contains(W1,e))return jr(e,me[t],!1,a,n,s);throw new z("Illegal delimiter: '"+e+"'")},Z1=[{type:"small",style:E.SCRIPTSCRIPT},{type:"small",style:E.SCRIPT},{type:"small",style:E.TEXT},{type:"large",size:1},{type:"large",size:2},{type:"large",size:3},{type:"large",size:4}],j1=[{type:"small",style:E.SCRIPTSCRIPT},{type:"small",style:E.SCRIPT},{type:"small",style:E.TEXT},{type:"stack"}],Qr=[{type:"small",style:E.SCRIPTSCRIPT},{type:"small",style:E.SCRIPT},{type:"small",style:E.TEXT},{type:"large",size:1},{type:"large",size:2},{type:"large",size:3},{type:"large",size:4},{type:"stack"}],K1=function(e){if(e.type==="small")return"Main-Regular";if(e.type==="large")return"Size"+e.size+"-Regular";if(e.type==="stack")return"Size4-Regular";throw new Error("Add support for delim type '"+e.type+"' here.")},ea=function(e,t,a,n){for(var s=Math.min(2,3-n.style.size),l=s;lt)return a[l]}return a[a.length-1]},ta=function(e,t,a,n,s,l){e==="<"||e==="\\lt"||e==="\u27E8"?e="\\langle":(e===">"||e==="\\gt"||e==="\u27E9")&&(e="\\rangle");var h;O.contains(Jr,e)?h=Z1:O.contains(Kr,e)?h=Qr:h=j1;var c=ea(e,t,h,n);return c.type==="small"?P1(e,c.style,a,n,s,l):c.type==="large"?Zr(e,c.size,a,n,s,l):jr(e,t,a,n,s,l)},J1=function(e,t,a,n,s,l){var h=n.fontMetrics().axisHeight*n.sizeMultiplier,c=901,f=5/n.fontMetrics().ptPerEm,v=Math.max(t-h,a+h),b=Math.max(v/500*c,2*v-f);return ta(e,b,!0,n,s,l)},O0={sqrtImage:X1,sizedDelim:Y1,sizeToMaxHeight:me,customSizedDelim:ta,leftRightDelim:J1},sr={"\\bigl":{mclass:"mopen",size:1},"\\Bigl":{mclass:"mopen",size:2},"\\biggl":{mclass:"mopen",size:3},"\\Biggl":{mclass:"mopen",size:4},"\\bigr":{mclass:"mclose",size:1},"\\Bigr":{mclass:"mclose",size:2},"\\biggr":{mclass:"mclose",size:3},"\\Biggr":{mclass:"mclose",size:4},"\\bigm":{mclass:"mrel",size:1},"\\Bigm":{mclass:"mrel",size:2},"\\biggm":{mclass:"mrel",size:3},"\\Biggm":{mclass:"mrel",size:4},"\\big":{mclass:"mord",size:1},"\\Big":{mclass:"mord",size:2},"\\bigg":{mclass:"mord",size:3},"\\Bigg":{mclass:"mord",size:4}},Q1=["(","\\lparen",")","\\rparen","[","\\lbrack","]","\\rbrack","\\{","\\lbrace","\\}","\\rbrace","\\lfloor","\\rfloor","\u230A","\u230B","\\lceil","\\rceil","\u2308","\u2309","<",">","\\langle","\u27E8","\\rangle","\u27E9","\\lt","\\gt","\\lvert","\\rvert","\\lVert","\\rVert","\\lgroup","\\rgroup","\u27EE","\u27EF","\\lmoustache","\\rmoustache","\u23B0","\u23B1","/","\\backslash","|","\\vert","\\|","\\Vert","\\uparrow","\\Uparrow","\\downarrow","\\Downarrow","\\updownarrow","\\Updownarrow","."];function Ye(r,e){var t=Xe(r);if(t&&O.contains(Q1,t.text))return t;throw t?new z("Invalid delimiter '"+t.text+"' after '"+e.funcName+"'",r):new z("Invalid delimiter type '"+r.type+"'",r)}B({type:"delimsizing",names:["\\bigl","\\Bigl","\\biggl","\\Biggl","\\bigr","\\Bigr","\\biggr","\\Biggr","\\bigm","\\Bigm","\\biggm","\\Biggm","\\big","\\Big","\\bigg","\\Bigg"],props:{numArgs:1,argTypes:["primitive"]},handler:(r,e)=>{var t=Ye(e[0],r);return{type:"delimsizing",mode:r.parser.mode,size:sr[r.funcName].size,mclass:sr[r.funcName].mclass,delim:t.text}},htmlBuilder:(r,e)=>r.delim==="."?y.makeSpan([r.mclass]):O0.sizedDelim(r.delim,r.size,e,r.mode,[r.mclass]),mathmlBuilder:r=>{var e=[];r.delim!=="."&&e.push(y0(r.delim,r.mode));var t=new M.MathNode("mo",e);r.mclass==="mopen"||r.mclass==="mclose"?t.setAttribute("fence","true"):t.setAttribute("fence","false"),t.setAttribute("stretchy","true");var a=T(O0.sizeToMaxHeight[r.size]);return t.setAttribute("minsize",a),t.setAttribute("maxsize",a),t}});function or(r){if(!r.body)throw new Error("Bug: The leftright ParseNode wasn't fully parsed.")}B({type:"leftright-right",names:["\\right"],props:{numArgs:1,primitive:!0},handler:(r,e)=>{var t=r.parser.gullet.macros.get("\\current@color");if(t&&typeof t!="string")throw new z("\\current@color set to non-string in \\right");return{type:"leftright-right",mode:r.parser.mode,delim:Ye(e[0],r).text,color:t}}});B({type:"leftright",names:["\\left"],props:{numArgs:1,primitive:!0},handler:(r,e)=>{var t=Ye(e[0],r),a=r.parser;++a.leftrightDepth;var n=a.parseExpression(!1);--a.leftrightDepth,a.expect("\\right",!1);var s=L(a.parseFunction(),"leftright-right");return{type:"leftright",mode:a.mode,body:n,left:t.text,right:s.delim,rightColor:s.color}},htmlBuilder:(r,e)=>{or(r);for(var t=a0(r.body,e,!0,["mopen","mclose"]),a=0,n=0,s=!1,l=0;l{or(r);var t=m0(r.body,e);if(r.left!=="."){var a=new M.MathNode("mo",[y0(r.left,r.mode)]);a.setAttribute("fence","true"),t.unshift(a)}if(r.right!=="."){var n=new M.MathNode("mo",[y0(r.right,r.mode)]);n.setAttribute("fence","true"),r.rightColor&&n.setAttribute("mathcolor",r.rightColor),t.push(n)}return Bt(t)}});B({type:"middle",names:["\\middle"],props:{numArgs:1,primitive:!0},handler:(r,e)=>{var t=Ye(e[0],r);if(!r.parser.leftrightDepth)throw new z("\\middle without preceding \\left",t);return{type:"middle",mode:r.parser.mode,delim:t.text}},htmlBuilder:(r,e)=>{var t;if(r.delim===".")t=ge(e,[]);else{t=O0.sizedDelim(r.delim,1,e,r.mode,[]);var a={delim:r.delim,options:e};t.isMiddle=a}return t},mathmlBuilder:(r,e)=>{var t=r.delim==="\\vert"||r.delim==="|"?y0("|","text"):y0(r.delim,r.mode),a=new M.MathNode("mo",[t]);return a.setAttribute("fence","true"),a.setAttribute("lspace","0.05em"),a.setAttribute("rspace","0.05em"),a}});var Ot=(r,e)=>{var t=y.wrapFragment(G(r.body,e),e),a=r.label.slice(1),n=e.sizeMultiplier,s,l=0,h=O.isCharacterBox(r.body);if(a==="sout")s=y.makeSpan(["stretchy","sout"]),s.height=e.fontMetrics().defaultRuleThickness/n,l=-.5*e.fontMetrics().xHeight;else if(a==="phase"){var c=Q({number:.6,unit:"pt"},e),f=Q({number:.35,unit:"ex"},e),v=e.havingBaseSizing();n=n/v.sizeMultiplier;var b=t.height+t.depth+c+f;t.style.paddingLeft=T(b/2+c);var x=Math.floor(1e3*b*n),w=Wa(x),A=new S0([new A0("phase",w)],{width:"400em",height:T(x/1e3),viewBox:"0 0 400000 "+x,preserveAspectRatio:"xMinYMin slice"});s=y.makeSvgSpan(["hide-tail"],[A],e),s.style.height=T(b),l=t.depth+c+f}else{/cancel/.test(a)?h||t.classes.push("cancel-pad"):a==="angl"?t.classes.push("anglpad"):t.classes.push("boxpad");var q=0,_=0,D=0;/box/.test(a)?(D=Math.max(e.fontMetrics().fboxrule,e.minRuleThickness),q=e.fontMetrics().fboxsep+(a==="colorbox"?0:D),_=q):a==="angl"?(D=Math.max(e.fontMetrics().defaultRuleThickness,e.minRuleThickness),q=4*D,_=Math.max(0,.25-t.depth)):(q=h?.2:0,_=q),s=E0.encloseSpan(t,a,q,_,e),/fbox|boxed|fcolorbox/.test(a)?(s.style.borderStyle="solid",s.style.borderWidth=T(D)):a==="angl"&&D!==.049&&(s.style.borderTopWidth=T(D),s.style.borderRightWidth=T(D)),l=t.depth+_,r.backgroundColor&&(s.style.backgroundColor=r.backgroundColor,r.borderColor&&(s.style.borderColor=r.borderColor))}var N;if(r.backgroundColor)N=y.makeVList({positionType:"individualShift",children:[{type:"elem",elem:s,shift:l},{type:"elem",elem:t,shift:0}]},e);else{var $=/cancel|phase/.test(a)?["svg-align"]:[];N=y.makeVList({positionType:"individualShift",children:[{type:"elem",elem:t,shift:0},{type:"elem",elem:s,shift:l,wrapperClasses:$}]},e)}return/cancel/.test(a)&&(N.height=t.height,N.depth=t.depth),/cancel/.test(a)&&!h?y.makeSpan(["mord","cancel-lap"],[N],e):y.makeSpan(["mord"],[N],e)},It=(r,e)=>{var t=0,a=new M.MathNode(r.label.indexOf("colorbox")>-1?"mpadded":"menclose",[X(r.body,e)]);switch(r.label){case"\\cancel":a.setAttribute("notation","updiagonalstrike");break;case"\\bcancel":a.setAttribute("notation","downdiagonalstrike");break;case"\\phase":a.setAttribute("notation","phasorangle");break;case"\\sout":a.setAttribute("notation","horizontalstrike");break;case"\\fbox":a.setAttribute("notation","box");break;case"\\angl":a.setAttribute("notation","actuarial");break;case"\\fcolorbox":case"\\colorbox":if(t=e.fontMetrics().fboxsep*e.fontMetrics().ptPerEm,a.setAttribute("width","+"+2*t+"pt"),a.setAttribute("height","+"+2*t+"pt"),a.setAttribute("lspace",t+"pt"),a.setAttribute("voffset",t+"pt"),r.label==="\\fcolorbox"){var n=Math.max(e.fontMetrics().fboxrule,e.minRuleThickness);a.setAttribute("style","border: "+n+"em solid "+String(r.borderColor))}break;case"\\xcancel":a.setAttribute("notation","updiagonalstrike downdiagonalstrike");break}return r.backgroundColor&&a.setAttribute("mathbackground",r.backgroundColor),a};B({type:"enclose",names:["\\colorbox"],props:{numArgs:2,allowedInText:!0,argTypes:["color","text"]},handler(r,e,t){var{parser:a,funcName:n}=r,s=L(e[0],"color-token").color,l=e[1];return{type:"enclose",mode:a.mode,label:n,backgroundColor:s,body:l}},htmlBuilder:Ot,mathmlBuilder:It});B({type:"enclose",names:["\\fcolorbox"],props:{numArgs:3,allowedInText:!0,argTypes:["color","color","text"]},handler(r,e,t){var{parser:a,funcName:n}=r,s=L(e[0],"color-token").color,l=L(e[1],"color-token").color,h=e[2];return{type:"enclose",mode:a.mode,label:n,backgroundColor:l,borderColor:s,body:h}},htmlBuilder:Ot,mathmlBuilder:It});B({type:"enclose",names:["\\fbox"],props:{numArgs:1,argTypes:["hbox"],allowedInText:!0},handler(r,e){var{parser:t}=r;return{type:"enclose",mode:t.mode,label:"\\fbox",body:e[0]}}});B({type:"enclose",names:["\\cancel","\\bcancel","\\xcancel","\\sout","\\phase"],props:{numArgs:1},handler(r,e){var{parser:t,funcName:a}=r,n=e[0];return{type:"enclose",mode:t.mode,label:a,body:n}},htmlBuilder:Ot,mathmlBuilder:It});B({type:"enclose",names:["\\angl"],props:{numArgs:1,argTypes:["hbox"],allowedInText:!1},handler(r,e){var{parser:t}=r;return{type:"enclose",mode:t.mode,label:"\\angl",body:e[0]}}});var ra={};function T0(r){for(var{type:e,names:t,props:a,handler:n,htmlBuilder:s,mathmlBuilder:l}=r,h={type:e,numArgs:a.numArgs||0,allowedInText:!1,numOptionalArgs:0,handler:n},c=0;c{var e=r.parser.settings;if(!e.displayMode)throw new z("{"+r.envName+"} can be used only in display mode.")};function Et(r){if(r.indexOf("ed")===-1)return r.indexOf("*")===-1}function U0(r,e,t){var{hskipBeforeAndAfter:a,addJot:n,cols:s,arraystretch:l,colSeparationType:h,autoTag:c,singleRow:f,emptySingleRow:v,maxNumCols:b,leqno:x}=e;if(r.gullet.beginGroup(),f||r.gullet.macros.set("\\cr","\\\\\\relax"),!l){var w=r.gullet.expandMacroAsText("\\arraystretch");if(w==null)l=1;else if(l=parseFloat(w),!l||l<0)throw new z("Invalid \\arraystretch: "+w)}r.gullet.beginGroup();var A=[],q=[A],_=[],D=[],N=c!=null?[]:void 0;function $(){c&&r.gullet.macros.set("\\@eqnsw","1",!0)}function H(){N&&(r.gullet.macros.get("\\df@tag")?(N.push(r.subparse([new b0("\\df@tag")])),r.gullet.macros.set("\\df@tag",void 0,!0)):N.push(!!c&&r.gullet.macros.get("\\@eqnsw")==="1"))}for($(),D.push(lr(r));;){var F=r.parseExpression(!1,f?"\\end":"\\\\");r.gullet.endGroup(),r.gullet.beginGroup(),F={type:"ordgroup",mode:r.mode,body:F},t&&(F={type:"styling",mode:r.mode,style:t,body:[F]}),A.push(F);var P=r.fetch().text;if(P==="&"){if(b&&A.length===b){if(f||h)throw new z("Too many tab characters: &",r.nextToken);r.settings.reportNonstrict("textEnv","Too few columns specified in the {array} column argument.")}r.consume()}else if(P==="\\end"){H(),A.length===1&&F.type==="styling"&&F.body[0].body.length===0&&(q.length>1||!v)&&q.pop(),D.length0&&($+=.25),f.push({pos:$,isDashed:we[Se]})}for(H(l[0]),a=0;a0&&(i0+=N,Vwe))for(a=0;a=h)){var ee=void 0;(n>0||e.hskipBeforeAndAfter)&&(ee=O.deflt(f0.pregap,x),ee!==0&&(x0=y.makeSpan(["arraycolsep"],[]),x0.style.width=T(ee),u0.push(x0)));var te=[];for(a=0;a0){for(var Sa=y.makeLineSpan("hline",t,v),ka=y.makeLineSpan("hdashline",t,v),Je=[{type:"elem",elem:c,shift:0}];f.length>0;){var Ut=f.pop(),Xt=Ut.pos-r0;Ut.isDashed?Je.push({type:"elem",elem:ka,shift:Xt}):Je.push({type:"elem",elem:Sa,shift:Xt})}c=y.makeVList({positionType:"individualShift",children:Je},t)}if(K0.length===0)return y.makeSpan(["mord"],[c],t);var Qe=y.makeVList({positionType:"individualShift",children:K0},t);return Qe=y.makeSpan(["tag"],[Qe],t),y.makeFragment([c,Qe])},en={c:"center ",l:"left ",r:"right "},B0=function(e,t){for(var a=[],n=new M.MathNode("mtd",[],["mtr-glue"]),s=new M.MathNode("mtd",[],["mml-eqn-num"]),l=0;l0){var A=e.cols,q="",_=!1,D=0,N=A.length;A[0].type==="separator"&&(x+="top ",D=1),A[A.length-1].type==="separator"&&(x+="bottom ",N-=1);for(var $=D;$0?"left ":"",x+=j[j.length-1].length>0?"right ":"";for(var U=1;U-1?"alignat":"align",s=e.envName==="split",l=U0(e.parser,{cols:a,addJot:!0,autoTag:s?void 0:Et(e.envName),emptySingleRow:!0,colSeparationType:n,maxNumCols:s?2:void 0,leqno:e.parser.settings.leqno},"display"),h,c=0,f={type:"ordgroup",mode:e.mode,body:[]};if(t[0]&&t[0].type==="ordgroup"){for(var v="",b=0;b0&&w&&(_=1),a[A]={type:"align",align:q,pregap:_,postgap:0}}return l.colSeparationType=w?"align":"alignat",l};T0({type:"array",names:["array","darray"],props:{numArgs:1},handler(r,e){var t=Xe(e[0]),a=t?[e[0]]:L(e[0],"ordgroup").body,n=a.map(function(l){var h=Ct(l),c=h.text;if("lcr".indexOf(c)!==-1)return{type:"align",align:c};if(c==="|")return{type:"separator",separator:"|"};if(c===":")return{type:"separator",separator:":"};throw new z("Unknown column alignment: "+c,l)}),s={cols:n,hskipBeforeAndAfter:!0,maxNumCols:n.length};return U0(r.parser,s,Rt(r.envName))},htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["matrix","pmatrix","bmatrix","Bmatrix","vmatrix","Vmatrix","matrix*","pmatrix*","bmatrix*","Bmatrix*","vmatrix*","Vmatrix*"],props:{numArgs:0},handler(r){var e={matrix:null,pmatrix:["(",")"],bmatrix:["[","]"],Bmatrix:["\\{","\\}"],vmatrix:["|","|"],Vmatrix:["\\Vert","\\Vert"]}[r.envName.replace("*","")],t="c",a={hskipBeforeAndAfter:!1,cols:[{type:"align",align:t}]};if(r.envName.charAt(r.envName.length-1)==="*"){var n=r.parser;if(n.consumeSpaces(),n.fetch().text==="["){if(n.consume(),n.consumeSpaces(),t=n.fetch().text,"lcr".indexOf(t)===-1)throw new z("Expected l or c or r",n.nextToken);n.consume(),n.consumeSpaces(),n.expect("]"),n.consume(),a.cols=[{type:"align",align:t}]}}var s=U0(r.parser,a,Rt(r.envName)),l=Math.max(0,...s.body.map(h=>h.length));return s.cols=new Array(l).fill({type:"align",align:t}),e?{type:"leftright",mode:r.mode,body:[s],left:e[0],right:e[1],rightColor:void 0}:s},htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["smallmatrix"],props:{numArgs:0},handler(r){var e={arraystretch:.5},t=U0(r.parser,e,"script");return t.colSeparationType="small",t},htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["subarray"],props:{numArgs:1},handler(r,e){var t=Xe(e[0]),a=t?[e[0]]:L(e[0],"ordgroup").body,n=a.map(function(l){var h=Ct(l),c=h.text;if("lc".indexOf(c)!==-1)return{type:"align",align:c};throw new z("Unknown column alignment: "+c,l)});if(n.length>1)throw new z("{subarray} can contain only one column");var s={cols:n,hskipBeforeAndAfter:!1,arraystretch:.5};if(s=U0(r.parser,s,"script"),s.body.length>0&&s.body[0].length>1)throw new z("{subarray} can contain only one column");return s},htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["cases","dcases","rcases","drcases"],props:{numArgs:0},handler(r){var e={arraystretch:1.2,cols:[{type:"align",align:"l",pregap:0,postgap:1},{type:"align",align:"l",pregap:0,postgap:0}]},t=U0(r.parser,e,Rt(r.envName));return{type:"leftright",mode:r.mode,body:[t],left:r.envName.indexOf("r")>-1?".":"\\{",right:r.envName.indexOf("r")>-1?"\\}":".",rightColor:void 0}},htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["align","align*","aligned","split"],props:{numArgs:0},handler:na,htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["gathered","gather","gather*"],props:{numArgs:0},handler(r){O.contains(["gather","gather*"],r.envName)&&Ze(r);var e={cols:[{type:"align",align:"c"}],addJot:!0,colSeparationType:"gather",autoTag:Et(r.envName),emptySingleRow:!0,leqno:r.parser.settings.leqno};return U0(r.parser,e,"display")},htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["alignat","alignat*","alignedat"],props:{numArgs:1},handler:na,htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["equation","equation*"],props:{numArgs:0},handler(r){Ze(r);var e={autoTag:Et(r.envName),emptySingleRow:!0,singleRow:!0,maxNumCols:1,leqno:r.parser.settings.leqno};return U0(r.parser,e,"display")},htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["CD"],props:{numArgs:0},handler(r){return Ze(r),F1(r.parser)},htmlBuilder:q0,mathmlBuilder:B0});m("\\nonumber","\\gdef\\@eqnsw{0}");m("\\notag","\\nonumber");B({type:"text",names:["\\hline","\\hdashline"],props:{numArgs:0,allowedInText:!0,allowedInMath:!0},handler(r,e){throw new z(r.funcName+" valid only within array environment")}});var ur=ra;B({type:"environment",names:["\\begin","\\end"],props:{numArgs:1,argTypes:["text"]},handler(r,e){var{parser:t,funcName:a}=r,n=e[0];if(n.type!=="ordgroup")throw new z("Invalid environment name",n);for(var s="",l=0;l{var t=r.font,a=e.withFont(t);return G(r.body,a)},sa=(r,e)=>{var t=r.font,a=e.withFont(t);return X(r.body,a)},hr={"\\Bbb":"\\mathbb","\\bold":"\\mathbf","\\frak":"\\mathfrak","\\bm":"\\boldsymbol"};B({type:"font",names:["\\mathrm","\\mathit","\\mathbf","\\mathnormal","\\mathsfit","\\mathbb","\\mathcal","\\mathfrak","\\mathscr","\\mathsf","\\mathtt","\\Bbb","\\bold","\\frak"],props:{numArgs:1,allowedInArgument:!0},handler:(r,e)=>{var{parser:t,funcName:a}=r,n=He(e[0]),s=a;return s in hr&&(s=hr[s]),{type:"font",mode:t.mode,font:s.slice(1),body:n}},htmlBuilder:ia,mathmlBuilder:sa});B({type:"mclass",names:["\\boldsymbol","\\bm"],props:{numArgs:1},handler:(r,e)=>{var{parser:t}=r,a=e[0],n=O.isCharacterBox(a);return{type:"mclass",mode:t.mode,mclass:We(a),body:[{type:"font",mode:t.mode,font:"boldsymbol",body:a}],isCharacterBox:n}}});B({type:"font",names:["\\rm","\\sf","\\tt","\\bf","\\it","\\cal"],props:{numArgs:0,allowedInText:!0},handler:(r,e)=>{var{parser:t,funcName:a,breakOnTokenText:n}=r,{mode:s}=t,l=t.parseExpression(!0,n),h="math"+a.slice(1);return{type:"font",mode:s,font:h,body:{type:"ordgroup",mode:t.mode,body:l}}},htmlBuilder:ia,mathmlBuilder:sa});var oa=(r,e)=>{var t=e;return r==="display"?t=t.id>=E.SCRIPT.id?t.text():E.DISPLAY:r==="text"&&t.size===E.DISPLAY.size?t=E.TEXT:r==="script"?t=E.SCRIPT:r==="scriptscript"&&(t=E.SCRIPTSCRIPT),t},$t=(r,e)=>{var t=oa(r.size,e.style),a=t.fracNum(),n=t.fracDen(),s;s=e.havingStyle(a);var l=G(r.numer,s,e);if(r.continued){var h=8.5/e.fontMetrics().ptPerEm,c=3.5/e.fontMetrics().ptPerEm;l.height=l.height0?A=3*x:A=7*x,q=e.fontMetrics().denom1):(b>0?(w=e.fontMetrics().num2,A=x):(w=e.fontMetrics().num3,A=3*x),q=e.fontMetrics().denom2);var _;if(v){var N=e.fontMetrics().axisHeight;w-l.depth-(N+.5*b){var t=new M.MathNode("mfrac",[X(r.numer,e),X(r.denom,e)]);if(!r.hasBarLine)t.setAttribute("linethickness","0px");else if(r.barSize){var a=Q(r.barSize,e);t.setAttribute("linethickness",T(a))}var n=oa(r.size,e.style);if(n.size!==e.style.size){t=new M.MathNode("mstyle",[t]);var s=n.size===E.DISPLAY.size?"true":"false";t.setAttribute("displaystyle",s),t.setAttribute("scriptlevel","0")}if(r.leftDelim!=null||r.rightDelim!=null){var l=[];if(r.leftDelim!=null){var h=new M.MathNode("mo",[new M.TextNode(r.leftDelim.replace("\\",""))]);h.setAttribute("fence","true"),l.push(h)}if(l.push(t),r.rightDelim!=null){var c=new M.MathNode("mo",[new M.TextNode(r.rightDelim.replace("\\",""))]);c.setAttribute("fence","true"),l.push(c)}return Bt(l)}return t};B({type:"genfrac",names:["\\dfrac","\\frac","\\tfrac","\\dbinom","\\binom","\\tbinom","\\\\atopfrac","\\\\bracefrac","\\\\brackfrac"],props:{numArgs:2,allowedInArgument:!0},handler:(r,e)=>{var{parser:t,funcName:a}=r,n=e[0],s=e[1],l,h=null,c=null,f="auto";switch(a){case"\\dfrac":case"\\frac":case"\\tfrac":l=!0;break;case"\\\\atopfrac":l=!1;break;case"\\dbinom":case"\\binom":case"\\tbinom":l=!1,h="(",c=")";break;case"\\\\bracefrac":l=!1,h="\\{",c="\\}";break;case"\\\\brackfrac":l=!1,h="[",c="]";break;default:throw new Error("Unrecognized genfrac command")}switch(a){case"\\dfrac":case"\\dbinom":f="display";break;case"\\tfrac":case"\\tbinom":f="text";break}return{type:"genfrac",mode:t.mode,continued:!1,numer:n,denom:s,hasBarLine:l,leftDelim:h,rightDelim:c,size:f,barSize:null}},htmlBuilder:$t,mathmlBuilder:Lt});B({type:"genfrac",names:["\\cfrac"],props:{numArgs:2},handler:(r,e)=>{var{parser:t,funcName:a}=r,n=e[0],s=e[1];return{type:"genfrac",mode:t.mode,continued:!0,numer:n,denom:s,hasBarLine:!0,leftDelim:null,rightDelim:null,size:"display",barSize:null}}});B({type:"infix",names:["\\over","\\choose","\\atop","\\brace","\\brack"],props:{numArgs:0,infix:!0},handler(r){var{parser:e,funcName:t,token:a}=r,n;switch(t){case"\\over":n="\\frac";break;case"\\choose":n="\\binom";break;case"\\atop":n="\\\\atopfrac";break;case"\\brace":n="\\\\bracefrac";break;case"\\brack":n="\\\\brackfrac";break;default:throw new Error("Unrecognized infix genfrac command")}return{type:"infix",mode:e.mode,replaceWith:n,token:a}}});var cr=["display","text","script","scriptscript"],mr=function(e){var t=null;return e.length>0&&(t=e,t=t==="."?null:t),t};B({type:"genfrac",names:["\\genfrac"],props:{numArgs:6,allowedInArgument:!0,argTypes:["math","math","size","text","math","math"]},handler(r,e){var{parser:t}=r,a=e[4],n=e[5],s=He(e[0]),l=s.type==="atom"&&s.family==="open"?mr(s.text):null,h=He(e[1]),c=h.type==="atom"&&h.family==="close"?mr(h.text):null,f=L(e[2],"size"),v,b=null;f.isBlank?v=!0:(b=f.value,v=b.number>0);var x="auto",w=e[3];if(w.type==="ordgroup"){if(w.body.length>0){var A=L(w.body[0],"textord");x=cr[Number(A.text)]}}else w=L(w,"textord"),x=cr[Number(w.text)];return{type:"genfrac",mode:t.mode,numer:a,denom:n,continued:!1,hasBarLine:v,barSize:b,leftDelim:l,rightDelim:c,size:x}},htmlBuilder:$t,mathmlBuilder:Lt});B({type:"infix",names:["\\above"],props:{numArgs:1,argTypes:["size"],infix:!0},handler(r,e){var{parser:t,funcName:a,token:n}=r;return{type:"infix",mode:t.mode,replaceWith:"\\\\abovefrac",size:L(e[0],"size").value,token:n}}});B({type:"genfrac",names:["\\\\abovefrac"],props:{numArgs:3,argTypes:["math","size","math"]},handler:(r,e)=>{var{parser:t,funcName:a}=r,n=e[0],s=_a(L(e[1],"infix").size),l=e[2],h=s.number>0;return{type:"genfrac",mode:t.mode,numer:n,denom:l,continued:!1,hasBarLine:h,barSize:s,leftDelim:null,rightDelim:null,size:"auto"}},htmlBuilder:$t,mathmlBuilder:Lt});var la=(r,e)=>{var t=e.style,a,n;r.type==="supsub"?(a=r.sup?G(r.sup,e.havingStyle(t.sup()),e):G(r.sub,e.havingStyle(t.sub()),e),n=L(r.base,"horizBrace")):n=L(r,"horizBrace");var s=G(n.base,e.havingBaseStyle(E.DISPLAY)),l=E0.svgSpan(n,e),h;if(n.isOver?(h=y.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:s},{type:"kern",size:.1},{type:"elem",elem:l}]},e),h.children[0].children[0].children[1].classes.push("svg-align")):(h=y.makeVList({positionType:"bottom",positionData:s.depth+.1+l.height,children:[{type:"elem",elem:l},{type:"kern",size:.1},{type:"elem",elem:s}]},e),h.children[0].children[0].children[0].classes.push("svg-align")),a){var c=y.makeSpan(["mord",n.isOver?"mover":"munder"],[h],e);n.isOver?h=y.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:c},{type:"kern",size:.2},{type:"elem",elem:a}]},e):h=y.makeVList({positionType:"bottom",positionData:c.depth+.2+a.height+a.depth,children:[{type:"elem",elem:a},{type:"kern",size:.2},{type:"elem",elem:c}]},e)}return y.makeSpan(["mord",n.isOver?"mover":"munder"],[h],e)},tn=(r,e)=>{var t=E0.mathMLnode(r.label);return new M.MathNode(r.isOver?"mover":"munder",[X(r.base,e),t])};B({type:"horizBrace",names:["\\overbrace","\\underbrace"],props:{numArgs:1},handler(r,e){var{parser:t,funcName:a}=r;return{type:"horizBrace",mode:t.mode,label:a,isOver:/^\\over/.test(a),base:e[0]}},htmlBuilder:la,mathmlBuilder:tn});B({type:"href",names:["\\href"],props:{numArgs:2,argTypes:["url","original"],allowedInText:!0},handler:(r,e)=>{var{parser:t}=r,a=e[1],n=L(e[0],"url").url;return t.settings.isTrusted({command:"\\href",url:n})?{type:"href",mode:t.mode,href:n,body:e0(a)}:t.formatUnsupportedCmd("\\href")},htmlBuilder:(r,e)=>{var t=a0(r.body,e,!1);return y.makeAnchor(r.href,[],t,e)},mathmlBuilder:(r,e)=>{var t=V0(r.body,e);return t instanceof s0||(t=new s0("mrow",[t])),t.setAttribute("href",r.href),t}});B({type:"href",names:["\\url"],props:{numArgs:1,argTypes:["url"],allowedInText:!0},handler:(r,e)=>{var{parser:t}=r,a=L(e[0],"url").url;if(!t.settings.isTrusted({command:"\\url",url:a}))return t.formatUnsupportedCmd("\\url");for(var n=[],s=0;s {var{parser:t,funcName:a,token:n}=r,s=L(e[0],"raw").string,l=e[1];t.settings.strict&&t.settings.reportNonstrict("htmlExtension","HTML extension is disabled on strict mode");var h,c={};switch(a){case"\\htmlClass":c.class=s,h={command:"\\htmlClass",class:s};break;case"\\htmlId":c.id=s,h={command:"\\htmlId",id:s};break;case"\\htmlStyle":c.style=s,h={command:"\\htmlStyle",style:s};break;case"\\htmlData":{for(var f=s.split(","),v=0;v{var t=a0(r.body,e,!1),a=["enclosing"];r.attributes.class&&a.push(...r.attributes.class.trim().split(/\s+/));var n=y.makeSpan(a,t,e);for(var s in r.attributes)s!=="class"&&r.attributes.hasOwnProperty(s)&&n.setAttribute(s,r.attributes[s]);return n},mathmlBuilder:(r,e)=>V0(r.body,e)});B({type:"htmlmathml",names:["\\html@mathml"],props:{numArgs:2,allowedInText:!0},handler:(r,e)=>{var{parser:t}=r;return{type:"htmlmathml",mode:t.mode,html:e0(e[0]),mathml:e0(e[1])}},htmlBuilder:(r,e)=>{var t=a0(r.html,e,!1);return y.makeFragment(t)},mathmlBuilder:(r,e)=>V0(r.mathml,e)});var ht=function(e){if(/^[-+]? *(\d+(\.\d*)?|\.\d+)$/.test(e))return{number:+e,unit:"bp"};var t=/([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/.exec(e);if(!t)throw new z("Invalid size: '"+e+"' in \\includegraphics");var a={number:+(t[1]+t[2]),unit:t[3]};if(!Tr(a))throw new z("Invalid unit: '"+a.unit+"' in \\includegraphics.");return a};B({type:"includegraphics",names:["\\includegraphics"],props:{numArgs:1,numOptionalArgs:1,argTypes:["raw","url"],allowedInText:!1},handler:(r,e,t)=>{var{parser:a}=r,n={number:0,unit:"em"},s={number:.9,unit:"em"},l={number:0,unit:"em"},h="";if(t[0])for(var c=L(t[0],"raw").string,f=c.split(","),v=0;v{var t=Q(r.height,e),a=0;r.totalheight.number>0&&(a=Q(r.totalheight,e)-t);var n=0;r.width.number>0&&(n=Q(r.width,e));var s={height:T(t+a)};n>0&&(s.width=T(n)),a>0&&(s.verticalAlign=T(-a));var l=new vt(r.src,r.alt,s);return l.height=t,l.depth=a,l},mathmlBuilder:(r,e)=>{var t=new M.MathNode("mglyph",[]);t.setAttribute("alt",r.alt);var a=Q(r.height,e),n=0;if(r.totalheight.number>0&&(n=Q(r.totalheight,e)-a,t.setAttribute("valign",T(-n))),t.setAttribute("height",T(a+n)),r.width.number>0){var s=Q(r.width,e);t.setAttribute("width",T(s))}return t.setAttribute("src",r.src),t}});B({type:"kern",names:["\\kern","\\mkern","\\hskip","\\mskip"],props:{numArgs:1,argTypes:["size"],primitive:!0,allowedInText:!0},handler(r,e){var{parser:t,funcName:a}=r,n=L(e[0],"size");if(t.settings.strict){var s=a[1]==="m",l=n.value.unit==="mu";s?(l||t.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+a+" supports only mu units, "+("not "+n.value.unit+" units")),t.mode!=="math"&&t.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+a+" works only in math mode")):l&&t.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+a+" doesn't support mu units")}return{type:"kern",mode:t.mode,dimension:n.value}},htmlBuilder(r,e){return y.makeGlue(r.dimension,e)},mathmlBuilder(r,e){var t=Q(r.dimension,e);return new M.SpaceNode(t)}});B({type:"lap",names:["\\mathllap","\\mathrlap","\\mathclap"],props:{numArgs:1,allowedInText:!0},handler:(r,e)=>{var{parser:t,funcName:a}=r,n=e[0];return{type:"lap",mode:t.mode,alignment:a.slice(5),body:n}},htmlBuilder:(r,e)=>{var t;r.alignment==="clap"?(t=y.makeSpan([],[G(r.body,e)]),t=y.makeSpan(["inner"],[t],e)):t=y.makeSpan(["inner"],[G(r.body,e)]);var a=y.makeSpan(["fix"],[]),n=y.makeSpan([r.alignment],[t,a],e),s=y.makeSpan(["strut"]);return s.style.height=T(n.height+n.depth),n.depth&&(s.style.verticalAlign=T(-n.depth)),n.children.unshift(s),n=y.makeSpan(["thinbox"],[n],e),y.makeSpan(["mord","vbox"],[n],e)},mathmlBuilder:(r,e)=>{var t=new M.MathNode("mpadded",[X(r.body,e)]);if(r.alignment!=="rlap"){var a=r.alignment==="llap"?"-1":"-0.5";t.setAttribute("lspace",a+"width")}return t.setAttribute("width","0px"),t}});B({type:"styling",names:["\\(","$"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler(r,e){var{funcName:t,parser:a}=r,n=a.mode;a.switchMode("math");var s=t==="\\("?"\\)":"$",l=a.parseExpression(!1,s);return a.expect(s),a.switchMode(n),{type:"styling",mode:a.mode,style:"text",body:l}}});B({type:"text",names:["\\)","\\]"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler(r,e){throw new z("Mismatched "+r.funcName)}});var dr=(r,e)=>{switch(e.style.size){case E.DISPLAY.size:return r.display;case E.TEXT.size:return r.text;case E.SCRIPT.size:return r.script;case E.SCRIPTSCRIPT.size:return r.scriptscript;default:return r.text}};B({type:"mathchoice",names:["\\mathchoice"],props:{numArgs:4,primitive:!0},handler:(r,e)=>{var{parser:t}=r;return{type:"mathchoice",mode:t.mode,display:e0(e[0]),text:e0(e[1]),script:e0(e[2]),scriptscript:e0(e[3])}},htmlBuilder:(r,e)=>{var t=dr(r,e),a=a0(t,e,!1);return y.makeFragment(a)},mathmlBuilder:(r,e)=>{var t=dr(r,e);return V0(t,e)}});var ua=(r,e,t,a,n,s,l)=>{r=y.makeSpan([],[r]);var h=t&&O.isCharacterBox(t),c,f;if(e){var v=G(e,a.havingStyle(n.sup()),a);f={elem:v,kern:Math.max(a.fontMetrics().bigOpSpacing1,a.fontMetrics().bigOpSpacing3-v.depth)}}if(t){var b=G(t,a.havingStyle(n.sub()),a);c={elem:b,kern:Math.max(a.fontMetrics().bigOpSpacing2,a.fontMetrics().bigOpSpacing4-b.height)}}var x;if(f&&c){var w=a.fontMetrics().bigOpSpacing5+c.elem.height+c.elem.depth+c.kern+r.depth+l;x=y.makeVList({positionType:"bottom",positionData:w,children:[{type:"kern",size:a.fontMetrics().bigOpSpacing5},{type:"elem",elem:c.elem,marginLeft:T(-s)},{type:"kern",size:c.kern},{type:"elem",elem:r},{type:"kern",size:f.kern},{type:"elem",elem:f.elem,marginLeft:T(s)},{type:"kern",size:a.fontMetrics().bigOpSpacing5}]},a)}else if(c){var A=r.height-l;x=y.makeVList({positionType:"top",positionData:A,children:[{type:"kern",size:a.fontMetrics().bigOpSpacing5},{type:"elem",elem:c.elem,marginLeft:T(-s)},{type:"kern",size:c.kern},{type:"elem",elem:r}]},a)}else if(f){var q=r.depth+l;x=y.makeVList({positionType:"bottom",positionData:q,children:[{type:"elem",elem:r},{type:"kern",size:f.kern},{type:"elem",elem:f.elem,marginLeft:T(s)},{type:"kern",size:a.fontMetrics().bigOpSpacing5}]},a)}else return r;var _=[x];if(c&&s!==0&&!h){var D=y.makeSpan(["mspace"],[],a);D.style.marginRight=T(s),_.unshift(D)}return y.makeSpan(["mop","op-limits"],_,a)},ha=["\\smallint"],se=(r,e)=>{var t,a,n=!1,s;r.type==="supsub"?(t=r.sup,a=r.sub,s=L(r.base,"op"),n=!0):s=L(r,"op");var l=e.style,h=!1;l.size===E.DISPLAY.size&&s.symbol&&!O.contains(ha,s.name)&&(h=!0);var c;if(s.symbol){var f=h?"Size2-Regular":"Size1-Regular",v="";if((s.name==="\\oiint"||s.name==="\\oiiint")&&(v=s.name.slice(1),s.name=v==="oiint"?"\\iint":"\\iiint"),c=y.makeSymbol(s.name,f,"math",e,["mop","op-symbol",h?"large-op":"small-op"]),v.length>0){var b=c.italic,x=y.staticSvg(v+"Size"+(h?"2":"1"),e);c=y.makeVList({positionType:"individualShift",children:[{type:"elem",elem:c,shift:0},{type:"elem",elem:x,shift:h?.08:0}]},e),s.name="\\"+v,c.classes.unshift("mop"),c.italic=b}}else if(s.body){var w=a0(s.body,e,!0);w.length===1&&w[0]instanceof c0?(c=w[0],c.classes[0]="mop"):c=y.makeSpan(["mop"],w,e)}else{for(var A=[],q=1;q{var t;if(r.symbol)t=new s0("mo",[y0(r.name,r.mode)]),O.contains(ha,r.name)&&t.setAttribute("largeop","false");else if(r.body)t=new s0("mo",m0(r.body,e));else{t=new s0("mi",[new g0(r.name.slice(1))]);var a=new s0("mo",[y0("\u2061","text")]);r.parentIsSupSub?t=new s0("mrow",[t,a]):t=$r([t,a])}return t},rn={"\u220F":"\\prod","\u2210":"\\coprod","\u2211":"\\sum","\u22C0":"\\bigwedge","\u22C1":"\\bigvee","\u22C2":"\\bigcap","\u22C3":"\\bigcup","\u2A00":"\\bigodot","\u2A01":"\\bigoplus","\u2A02":"\\bigotimes","\u2A04":"\\biguplus","\u2A06":"\\bigsqcup"};B({type:"op",names:["\\coprod","\\bigvee","\\bigwedge","\\biguplus","\\bigcap","\\bigcup","\\intop","\\prod","\\sum","\\bigotimes","\\bigoplus","\\bigodot","\\bigsqcup","\\smallint","\u220F","\u2210","\u2211","\u22C0","\u22C1","\u22C2","\u22C3","\u2A00","\u2A01","\u2A02","\u2A04","\u2A06"],props:{numArgs:0},handler:(r,e)=>{var{parser:t,funcName:a}=r,n=a;return n.length===1&&(n=rn[n]),{type:"op",mode:t.mode,limits:!0,parentIsSupSub:!1,symbol:!0,name:n}},htmlBuilder:se,mathmlBuilder:be});B({type:"op",names:["\\mathop"],props:{numArgs:1,primitive:!0},handler:(r,e)=>{var{parser:t}=r,a=e[0];return{type:"op",mode:t.mode,limits:!1,parentIsSupSub:!1,symbol:!1,body:e0(a)}},htmlBuilder:se,mathmlBuilder:be});var an={"\u222B":"\\int","\u222C":"\\iint","\u222D":"\\iiint","\u222E":"\\oint","\u222F":"\\oiint","\u2230":"\\oiiint"};B({type:"op",names:["\\arcsin","\\arccos","\\arctan","\\arctg","\\arcctg","\\arg","\\ch","\\cos","\\cosec","\\cosh","\\cot","\\cotg","\\coth","\\csc","\\ctg","\\cth","\\deg","\\dim","\\exp","\\hom","\\ker","\\lg","\\ln","\\log","\\sec","\\sin","\\sinh","\\sh","\\tan","\\tanh","\\tg","\\th"],props:{numArgs:0},handler(r){var{parser:e,funcName:t}=r;return{type:"op",mode:e.mode,limits:!1,parentIsSupSub:!1,symbol:!1,name:t}},htmlBuilder:se,mathmlBuilder:be});B({type:"op",names:["\\det","\\gcd","\\inf","\\lim","\\max","\\min","\\Pr","\\sup"],props:{numArgs:0},handler(r){var{parser:e,funcName:t}=r;return{type:"op",mode:e.mode,limits:!0,parentIsSupSub:!1,symbol:!1,name:t}},htmlBuilder:se,mathmlBuilder:be});B({type:"op",names:["\\int","\\iint","\\iiint","\\oint","\\oiint","\\oiiint","\u222B","\u222C","\u222D","\u222E","\u222F","\u2230"],props:{numArgs:0},handler(r){var{parser:e,funcName:t}=r,a=t;return a.length===1&&(a=an[a]),{type:"op",mode:e.mode,limits:!1,parentIsSupSub:!1,symbol:!0,name:a}},htmlBuilder:se,mathmlBuilder:be});var ca=(r,e)=>{var t,a,n=!1,s;r.type==="supsub"?(t=r.sup,a=r.sub,s=L(r.base,"operatorname"),n=!0):s=L(r,"operatorname");var l;if(s.body.length>0){for(var h=s.body.map(b=>{var x=b.text;return typeof x=="string"?{type:"textord",mode:b.mode,text:x}:b}),c=a0(h,e.withFont("mathrm"),!0),f=0;f{for(var t=m0(r.body,e.withFont("mathrm")),a=!0,n=0;nv.toText()).join("");t=[new M.TextNode(h)]}var c=new M.MathNode("mi",t);c.setAttribute("mathvariant","normal");var f=new M.MathNode("mo",[y0("\u2061","text")]);return r.parentIsSupSub?new M.MathNode("mrow",[c,f]):M.newDocumentFragment([c,f])};B({type:"operatorname",names:["\\operatorname@","\\operatornamewithlimits"],props:{numArgs:1},handler:(r,e)=>{var{parser:t,funcName:a}=r,n=e[0];return{type:"operatorname",mode:t.mode,body:e0(n),alwaysHandleSupSub:a==="\\operatornamewithlimits",limits:!1,parentIsSupSub:!1}},htmlBuilder:ca,mathmlBuilder:nn});m("\\operatorname","\\@ifstar\\operatornamewithlimits\\operatorname@");j0({type:"ordgroup",htmlBuilder(r,e){return r.semisimple?y.makeFragment(a0(r.body,e,!1)):y.makeSpan(["mord"],a0(r.body,e,!0),e)},mathmlBuilder(r,e){return V0(r.body,e,!0)}});B({type:"overline",names:["\\overline"],props:{numArgs:1},handler(r,e){var{parser:t}=r,a=e[0];return{type:"overline",mode:t.mode,body:a}},htmlBuilder(r,e){var t=G(r.body,e.havingCrampedStyle()),a=y.makeLineSpan("overline-line",e),n=e.fontMetrics().defaultRuleThickness,s=y.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:t},{type:"kern",size:3*n},{type:"elem",elem:a},{type:"kern",size:n}]},e);return y.makeSpan(["mord","overline"],[s],e)},mathmlBuilder(r,e){var t=new M.MathNode("mo",[new M.TextNode("\u203E")]);t.setAttribute("stretchy","true");var a=new M.MathNode("mover",[X(r.body,e),t]);return a.setAttribute("accent","true"),a}});B({type:"phantom",names:["\\phantom"],props:{numArgs:1,allowedInText:!0},handler:(r,e)=>{var{parser:t}=r,a=e[0];return{type:"phantom",mode:t.mode,body:e0(a)}},htmlBuilder:(r,e)=>{var t=a0(r.body,e.withPhantom(),!1);return y.makeFragment(t)},mathmlBuilder:(r,e)=>{var t=m0(r.body,e);return new M.MathNode("mphantom",t)}});B({type:"hphantom",names:["\\hphantom"],props:{numArgs:1,allowedInText:!0},handler:(r,e)=>{var{parser:t}=r,a=e[0];return{type:"hphantom",mode:t.mode,body:a}},htmlBuilder:(r,e)=>{var t=y.makeSpan([],[G(r.body,e.withPhantom())]);if(t.height=0,t.depth=0,t.children)for(var a=0;a{var t=m0(e0(r.body),e),a=new M.MathNode("mphantom",t),n=new M.MathNode("mpadded",[a]);return n.setAttribute("height","0px"),n.setAttribute("depth","0px"),n}});B({type:"vphantom",names:["\\vphantom"],props:{numArgs:1,allowedInText:!0},handler:(r,e)=>{var{parser:t}=r,a=e[0];return{type:"vphantom",mode:t.mode,body:a}},htmlBuilder:(r,e)=>{var t=y.makeSpan(["inner"],[G(r.body,e.withPhantom())]),a=y.makeSpan(["fix"],[]);return y.makeSpan(["mord","rlap"],[t,a],e)},mathmlBuilder:(r,e)=>{var t=m0(e0(r.body),e),a=new M.MathNode("mphantom",t),n=new M.MathNode("mpadded",[a]);return n.setAttribute("width","0px"),n}});B({type:"raisebox",names:["\\raisebox"],props:{numArgs:2,argTypes:["size","hbox"],allowedInText:!0},handler(r,e){var{parser:t}=r,a=L(e[0],"size").value,n=e[1];return{type:"raisebox",mode:t.mode,dy:a,body:n}},htmlBuilder(r,e){var t=G(r.body,e),a=Q(r.dy,e);return y.makeVList({positionType:"shift",positionData:-a,children:[{type:"elem",elem:t}]},e)},mathmlBuilder(r,e){var t=new M.MathNode("mpadded",[X(r.body,e)]),a=r.dy.number+r.dy.unit;return t.setAttribute("voffset",a),t}});B({type:"internal",names:["\\relax"],props:{numArgs:0,allowedInText:!0,allowedInArgument:!0},handler(r){var{parser:e}=r;return{type:"internal",mode:e.mode}}});B({type:"rule",names:["\\rule"],props:{numArgs:2,numOptionalArgs:1,allowedInText:!0,allowedInMath:!0,argTypes:["size","size","size"]},handler(r,e,t){var{parser:a}=r,n=t[0],s=L(e[0],"size"),l=L(e[1],"size");return{type:"rule",mode:a.mode,shift:n&&L(n,"size").value,width:s.value,height:l.value}},htmlBuilder(r,e){var t=y.makeSpan(["mord","rule"],[],e),a=Q(r.width,e),n=Q(r.height,e),s=r.shift?Q(r.shift,e):0;return t.style.borderRightWidth=T(a),t.style.borderTopWidth=T(n),t.style.bottom=T(s),t.width=a,t.height=n+s,t.depth=-s,t.maxFontSize=n*1.125*e.sizeMultiplier,t},mathmlBuilder(r,e){var t=Q(r.width,e),a=Q(r.height,e),n=r.shift?Q(r.shift,e):0,s=e.color&&e.getColor()||"black",l=new M.MathNode("mspace");l.setAttribute("mathbackground",s),l.setAttribute("width",T(t)),l.setAttribute("height",T(a));var h=new M.MathNode("mpadded",[l]);return n>=0?h.setAttribute("height",T(n)):(h.setAttribute("height",T(n)),h.setAttribute("depth",T(-n))),h.setAttribute("voffset",T(n)),h}});function ma(r,e,t){for(var a=a0(r,e,!1),n=e.sizeMultiplier/t.sizeMultiplier,s=0;s{var t=e.havingSize(r.size);return ma(r.body,t,e)};B({type:"sizing",names:pr,props:{numArgs:0,allowedInText:!0},handler:(r,e)=>{var{breakOnTokenText:t,funcName:a,parser:n}=r,s=n.parseExpression(!1,t);return{type:"sizing",mode:n.mode,size:pr.indexOf(a)+1,body:s}},htmlBuilder:sn,mathmlBuilder:(r,e)=>{var t=e.havingSize(r.size),a=m0(r.body,t),n=new M.MathNode("mstyle",a);return n.setAttribute("mathsize",T(t.sizeMultiplier)),n}});B({type:"smash",names:["\\smash"],props:{numArgs:1,numOptionalArgs:1,allowedInText:!0},handler:(r,e,t)=>{var{parser:a}=r,n=!1,s=!1,l=t[0]&&L(t[0],"ordgroup");if(l)for(var h="",c=0;c{var t=y.makeSpan([],[G(r.body,e)]);if(!r.smashHeight&&!r.smashDepth)return t;if(r.smashHeight&&(t.height=0,t.children))for(var a=0;a{var t=new M.MathNode("mpadded",[X(r.body,e)]);return r.smashHeight&&t.setAttribute("height","0px"),r.smashDepth&&t.setAttribute("depth","0px"),t}});B({type:"sqrt",names:["\\sqrt"],props:{numArgs:1,numOptionalArgs:1},handler(r,e,t){var{parser:a}=r,n=t[0],s=e[0];return{type:"sqrt",mode:a.mode,body:s,index:n}},htmlBuilder(r,e){var t=G(r.body,e.havingCrampedStyle());t.height===0&&(t.height=e.fontMetrics().xHeight),t=y.wrapFragment(t,e);var a=e.fontMetrics(),n=a.defaultRuleThickness,s=n;e.style.idt.height+t.depth+l&&(l=(l+b-t.height-t.depth)/2);var x=c.height-t.height-l-f;t.style.paddingLeft=T(v);var w=y.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:t,wrapperClasses:["svg-align"]},{type:"kern",size:-(t.height+x)},{type:"elem",elem:c},{type:"kern",size:f}]},e);if(r.index){var A=e.havingStyle(E.SCRIPTSCRIPT),q=G(r.index,A,e),_=.6*(w.height-w.depth),D=y.makeVList({positionType:"shift",positionData:-_,children:[{type:"elem",elem:q}]},e),N=y.makeSpan(["root"],[D]);return y.makeSpan(["mord","sqrt"],[N,w],e)}else return y.makeSpan(["mord","sqrt"],[w],e)},mathmlBuilder(r,e){var{body:t,index:a}=r;return a?new M.MathNode("mroot",[X(t,e),X(a,e)]):new M.MathNode("msqrt",[X(t,e)])}});var fr={display:E.DISPLAY,text:E.TEXT,script:E.SCRIPT,scriptscript:E.SCRIPTSCRIPT};B({type:"styling",names:["\\displaystyle","\\textstyle","\\scriptstyle","\\scriptscriptstyle"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler(r,e){var{breakOnTokenText:t,funcName:a,parser:n}=r,s=n.parseExpression(!0,t),l=a.slice(1,a.length-5);return{type:"styling",mode:n.mode,style:l,body:s}},htmlBuilder(r,e){var t=fr[r.style],a=e.havingStyle(t).withFont("");return ma(r.body,a,e)},mathmlBuilder(r,e){var t=fr[r.style],a=e.havingStyle(t),n=m0(r.body,a),s=new M.MathNode("mstyle",n),l={display:["0","true"],text:["0","false"],script:["1","false"],scriptscript:["2","false"]},h=l[r.style];return s.setAttribute("scriptlevel",h[0]),s.setAttribute("displaystyle",h[1]),s}});var on=function(e,t){var a=e.base;if(a)if(a.type==="op"){var n=a.limits&&(t.style.size===E.DISPLAY.size||a.alwaysHandleSupSub);return n?se:null}else if(a.type==="operatorname"){var s=a.alwaysHandleSupSub&&(t.style.size===E.DISPLAY.size||a.limits);return s?ca:null}else{if(a.type==="accent")return O.isCharacterBox(a.base)?_t:null;if(a.type==="horizBrace"){var l=!e.sub;return l===a.isOver?la:null}else return null}else return null};j0({type:"supsub",htmlBuilder(r,e){var t=on(r,e);if(t)return t(r,e);var{base:a,sup:n,sub:s}=r,l=G(a,e),h,c,f=e.fontMetrics(),v=0,b=0,x=a&&O.isCharacterBox(a);if(n){var w=e.havingStyle(e.style.sup());h=G(n,w,e),x||(v=l.height-w.fontMetrics().supDrop*w.sizeMultiplier/e.sizeMultiplier)}if(s){var A=e.havingStyle(e.style.sub());c=G(s,A,e),x||(b=l.depth+A.fontMetrics().subDrop*A.sizeMultiplier/e.sizeMultiplier)}var q;e.style===E.DISPLAY?q=f.sup1:e.style.cramped?q=f.sup3:q=f.sup2;var _=e.sizeMultiplier,D=T(.5/f.ptPerEm/_),N=null;if(c){var $=r.base&&r.base.type==="op"&&r.base.name&&(r.base.name==="\\oiint"||r.base.name==="\\oiiint");(l instanceof c0||$)&&(N=T(-l.italic))}var H;if(h&&c){v=Math.max(v,q,h.depth+.25*f.xHeight),b=Math.max(b,f.sub2);var F=f.defaultRuleThickness,P=4*F;if(v-h.depth-(c.height-b)0&&(v+=V,b-=V)}var j=[{type:"elem",elem:c,shift:b,marginRight:D,marginLeft:N},{type:"elem",elem:h,shift:-v,marginRight:D}];H=y.makeVList({positionType:"individualShift",children:j},e)}else if(c){b=Math.max(b,f.sub1,c.height-.8*f.xHeight);var U=[{type:"elem",elem:c,marginLeft:N,marginRight:D}];H=y.makeVList({positionType:"shift",positionData:b,children:U},e)}else if(h)v=Math.max(v,q,h.depth+.25*f.xHeight),H=y.makeVList({positionType:"shift",positionData:-v,children:[{type:"elem",elem:h,marginRight:D}]},e);else throw new Error("supsub must have either sup or sub.");var D0=bt(l,"right")||"mord";return y.makeSpan([D0],[l,y.makeSpan(["msupsub"],[H])],e)},mathmlBuilder(r,e){var t=!1,a,n;r.base&&r.base.type==="horizBrace"&&(n=!!r.sup,n===r.base.isOver&&(t=!0,a=r.base.isOver)),r.base&&(r.base.type==="op"||r.base.type==="operatorname")&&(r.base.parentIsSupSub=!0);var s=[X(r.base,e)];r.sub&&s.push(X(r.sub,e)),r.sup&&s.push(X(r.sup,e));var l;if(t)l=a?"mover":"munder";else if(r.sub)if(r.sup){var f=r.base;f&&f.type==="op"&&f.limits&&e.style===E.DISPLAY||f&&f.type==="operatorname"&&f.alwaysHandleSupSub&&(e.style===E.DISPLAY||f.limits)?l="munderover":l="msubsup"}else{var c=r.base;c&&c.type==="op"&&c.limits&&(e.style===E.DISPLAY||c.alwaysHandleSupSub)||c&&c.type==="operatorname"&&c.alwaysHandleSupSub&&(c.limits||e.style===E.DISPLAY)?l="munder":l="msub"}else{var h=r.base;h&&h.type==="op"&&h.limits&&(e.style===E.DISPLAY||h.alwaysHandleSupSub)||h&&h.type==="operatorname"&&h.alwaysHandleSupSub&&(h.limits||e.style===E.DISPLAY)?l="mover":l="msup"}return new M.MathNode(l,s)}});j0({type:"atom",htmlBuilder(r,e){return y.mathsym(r.text,r.mode,e,["m"+r.family])},mathmlBuilder(r,e){var t=new M.MathNode("mo",[y0(r.text,r.mode)]);if(r.family==="bin"){var a=Dt(r,e);a==="bold-italic"&&t.setAttribute("mathvariant",a)}else r.family==="punct"?t.setAttribute("separator","true"):(r.family==="open"||r.family==="close")&&t.setAttribute("stretchy","false");return t}});var da={mi:"italic",mn:"normal",mtext:"normal"};j0({type:"mathord",htmlBuilder(r,e){return y.makeOrd(r,e,"mathord")},mathmlBuilder(r,e){var t=new M.MathNode("mi",[y0(r.text,r.mode,e)]),a=Dt(r,e)||"italic";return a!==da[t.type]&&t.setAttribute("mathvariant",a),t}});j0({type:"textord",htmlBuilder(r,e){return y.makeOrd(r,e,"textord")},mathmlBuilder(r,e){var t=y0(r.text,r.mode,e),a=Dt(r,e)||"normal",n;return r.mode==="text"?n=new M.MathNode("mtext",[t]):/[0-9]/.test(r.text)?n=new M.MathNode("mn",[t]):r.text==="\\prime"?n=new M.MathNode("mo",[t]):n=new M.MathNode("mi",[t]),a!==da[n.type]&&n.setAttribute("mathvariant",a),n}});var ct={"\\nobreak":"nobreak","\\allowbreak":"allowbreak"},mt={" ":{},"\\ ":{},"~":{className:"nobreak"},"\\space":{},"\\nobreakspace":{className:"nobreak"}};j0({type:"spacing",htmlBuilder(r,e){if(mt.hasOwnProperty(r.text)){var t=mt[r.text].className||"";if(r.mode==="text"){var a=y.makeOrd(r,e,"textord");return a.classes.push(t),a}else return y.makeSpan(["mspace",t],[y.mathsym(r.text,r.mode,e)],e)}else{if(ct.hasOwnProperty(r.text))return y.makeSpan(["mspace",ct[r.text]],[],e);throw new z('Unknown type of space "'+r.text+'"')}},mathmlBuilder(r,e){var t;if(mt.hasOwnProperty(r.text))t=new M.MathNode("mtext",[new M.TextNode("\xA0")]);else{if(ct.hasOwnProperty(r.text))return new M.MathNode("mspace");throw new z('Unknown type of space "'+r.text+'"')}return t}});var vr=()=>{var r=new M.MathNode("mtd",[]);return r.setAttribute("width","50%"),r};j0({type:"tag",mathmlBuilder(r,e){var t=new M.MathNode("mtable",[new M.MathNode("mtr",[vr(),new M.MathNode("mtd",[V0(r.body,e)]),vr(),new M.MathNode("mtd",[V0(r.tag,e)])])]);return t.setAttribute("width","100%"),t}});var gr={"\\text":void 0,"\\textrm":"textrm","\\textsf":"textsf","\\texttt":"texttt","\\textnormal":"textrm"},br={"\\textbf":"textbf","\\textmd":"textmd"},ln={"\\textit":"textit","\\textup":"textup"},yr=(r,e)=>{var t=r.font;if(t){if(gr[t])return e.withTextFontFamily(gr[t]);if(br[t])return e.withTextFontWeight(br[t]);if(t==="\\emph")return e.fontShape==="textit"?e.withTextFontShape("textup"):e.withTextFontShape("textit")}else return e;return e.withTextFontShape(ln[t])};B({type:"text",names:["\\text","\\textrm","\\textsf","\\texttt","\\textnormal","\\textbf","\\textmd","\\textit","\\textup","\\emph"],props:{numArgs:1,argTypes:["text"],allowedInArgument:!0,allowedInText:!0},handler(r,e){var{parser:t,funcName:a}=r,n=e[0];return{type:"text",mode:t.mode,body:e0(n),font:a}},htmlBuilder(r,e){var t=yr(r,e),a=a0(r.body,t,!0);return y.makeSpan(["mord","text"],a,t)},mathmlBuilder(r,e){var t=yr(r,e);return V0(r.body,t)}});B({type:"underline",names:["\\underline"],props:{numArgs:1,allowedInText:!0},handler(r,e){var{parser:t}=r;return{type:"underline",mode:t.mode,body:e[0]}},htmlBuilder(r,e){var t=G(r.body,e),a=y.makeLineSpan("underline-line",e),n=e.fontMetrics().defaultRuleThickness,s=y.makeVList({positionType:"top",positionData:t.height,children:[{type:"kern",size:n},{type:"elem",elem:a},{type:"kern",size:3*n},{type:"elem",elem:t}]},e);return y.makeSpan(["mord","underline"],[s],e)},mathmlBuilder(r,e){var t=new M.MathNode("mo",[new M.TextNode("\u203E")]);t.setAttribute("stretchy","true");var a=new M.MathNode("munder",[X(r.body,e),t]);return a.setAttribute("accentunder","true"),a}});B({type:"vcenter",names:["\\vcenter"],props:{numArgs:1,argTypes:["original"],allowedInText:!1},handler(r,e){var{parser:t}=r;return{type:"vcenter",mode:t.mode,body:e[0]}},htmlBuilder(r,e){var t=G(r.body,e),a=e.fontMetrics().axisHeight,n=.5*(t.height-a-(t.depth+a));return y.makeVList({positionType:"shift",positionData:n,children:[{type:"elem",elem:t}]},e)},mathmlBuilder(r,e){return new M.MathNode("mpadded",[X(r.body,e)],["vcenter"])}});B({type:"verb",names:["\\verb"],props:{numArgs:0,allowedInText:!0},handler(r,e,t){throw new z("\\verb ended by end of line instead of matching delimiter")},htmlBuilder(r,e){for(var t=xr(r),a=[],n=e.havingStyle(e.style.text()),s=0;sr.body.replace(/ /g,r.star?"\u2423":"\xA0"),P0=Er,pa=`[ \r
+ ]`,un="\\\\[a-zA-Z@]+",hn="\\\\[^\uD800-\uDFFF]",cn="("+un+")"+pa+"*",mn=`\\\\(
+|[ \r ]+
+?)[ \r ]*`,kt="[\u0300-\u036F]",dn=new RegExp(kt+"+$"),pn="("+pa+"+)|"+(mn+"|")+"([!-\\[\\]-\u2027\u202A-\uD7FF\uF900-\uFFFF]"+(kt+"*")+"|[\uD800-\uDBFF][\uDC00-\uDFFF]"+(kt+"*")+"|\\\\verb\\*([^]).*?\\4|\\\\verb([^*a-zA-Z]).*?\\5"+("|"+cn)+("|"+hn+")"),Pe=class{constructor(e,t){this.input=void 0,this.settings=void 0,this.tokenRegex=void 0,this.catcodes=void 0,this.input=e,this.settings=t,this.tokenRegex=new RegExp(pn,"g"),this.catcodes={"%":14,"~":13}}setCatcode(e,t){this.catcodes[e]=t}lex(){var e=this.input,t=this.tokenRegex.lastIndex;if(t===e.length)return new b0("EOF",new d0(this,t,t));var a=this.tokenRegex.exec(e);if(a===null||a.index!==t)throw new z("Unexpected character: '"+e[t]+"'",new b0(e[t],new d0(this,t,t+1)));var n=a[6]||a[3]||(a[2]?"\\ ":" ");if(this.catcodes[n]===14){var s=e.indexOf(`
+`,this.tokenRegex.lastIndex);return s===-1?(this.tokenRegex.lastIndex=e.length,this.settings.reportNonstrict("commentAtEnd","% comment has no terminating newline; LaTeX would fail because of commenting the end of math mode (e.g. $)")):this.tokenRegex.lastIndex=s+1,this.lex()}return new b0(n,new d0(this,t,this.tokenRegex.lastIndex))}},Mt=class{constructor(e,t){e===void 0&&(e={}),t===void 0&&(t={}),this.current=void 0,this.builtins=void 0,this.undefStack=void 0,this.current=t,this.builtins=e,this.undefStack=[]}beginGroup(){this.undefStack.push({})}endGroup(){if(this.undefStack.length===0)throw new z("Unbalanced namespace destruction: attempt to pop global namespace; please report this as a bug");var e=this.undefStack.pop();for(var t in e)e.hasOwnProperty(t)&&(e[t]==null?delete this.current[t]:this.current[t]=e[t])}endGroups(){for(;this.undefStack.length>0;)this.endGroup()}has(e){return this.current.hasOwnProperty(e)||this.builtins.hasOwnProperty(e)}get(e){return this.current.hasOwnProperty(e)?this.current[e]:this.builtins[e]}set(e,t,a){if(a===void 0&&(a=!1),a){for(var n=0;n0&&(this.undefStack[this.undefStack.length-1][e]=t)}else{var s=this.undefStack[this.undefStack.length-1];s&&!s.hasOwnProperty(e)&&(s[e]=this.current[e])}t==null?delete this.current[e]:this.current[e]=t}},fn=aa;m("\\noexpand",function(r){var e=r.popToken();return r.isExpandable(e.text)&&(e.noexpand=!0,e.treatAsRelax=!0),{tokens:[e],numArgs:0}});m("\\expandafter",function(r){var e=r.popToken();return r.expandOnce(!0),{tokens:[e],numArgs:0}});m("\\@firstoftwo",function(r){var e=r.consumeArgs(2);return{tokens:e[0],numArgs:0}});m("\\@secondoftwo",function(r){var e=r.consumeArgs(2);return{tokens:e[1],numArgs:0}});m("\\@ifnextchar",function(r){var e=r.consumeArgs(3);r.consumeSpaces();var t=r.future();return e[0].length===1&&e[0][0].text===t.text?{tokens:e[1],numArgs:0}:{tokens:e[2],numArgs:0}});m("\\@ifstar","\\@ifnextchar *{\\@firstoftwo{#1}}");m("\\TextOrMath",function(r){var e=r.consumeArgs(2);return r.mode==="text"?{tokens:e[0],numArgs:0}:{tokens:e[1],numArgs:0}});var wr={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,a:10,A:10,b:11,B:11,c:12,C:12,d:13,D:13,e:14,E:14,f:15,F:15};m("\\char",function(r){var e=r.popToken(),t,a="";if(e.text==="'")t=8,e=r.popToken();else if(e.text==='"')t=16,e=r.popToken();else if(e.text==="`")if(e=r.popToken(),e.text[0]==="\\")a=e.text.charCodeAt(1);else{if(e.text==="EOF")throw new z("\\char` missing argument");a=e.text.charCodeAt(0)}else t=10;if(t){if(a=wr[e.text],a==null||a>=t)throw new z("Invalid base-"+t+" digit "+e.text);for(var n;(n=wr[r.future().text])!=null&&n{var n=r.consumeArg().tokens;if(n.length!==1)throw new z("\\newcommand's first argument must be a macro name");var s=n[0].text,l=r.isDefined(s);if(l&&!e)throw new z("\\newcommand{"+s+"} attempting to redefine "+(s+"; use \\renewcommand"));if(!l&&!t)throw new z("\\renewcommand{"+s+"} when command "+s+" does not yet exist; use \\newcommand");var h=0;if(n=r.consumeArg().tokens,n.length===1&&n[0].text==="["){for(var c="",f=r.expandNextToken();f.text!=="]"&&f.text!=="EOF";)c+=f.text,f=r.expandNextToken();if(!c.match(/^\s*[0-9]+\s*$/))throw new z("Invalid number of arguments: "+c);h=parseInt(c),n=r.consumeArg().tokens}return l&&a||r.macros.set(s,{tokens:n,numArgs:h}),""};m("\\newcommand",r=>Ft(r,!1,!0,!1));m("\\renewcommand",r=>Ft(r,!0,!1,!1));m("\\providecommand",r=>Ft(r,!0,!0,!0));m("\\message",r=>{var e=r.consumeArgs(1)[0];return console.log(e.reverse().map(t=>t.text).join("")),""});m("\\errmessage",r=>{var e=r.consumeArgs(1)[0];return console.error(e.reverse().map(t=>t.text).join("")),""});m("\\show",r=>{var e=r.popToken(),t=e.text;return console.log(e,r.macros.get(t),P0[t],Y.math[t],Y.text[t]),""});m("\\bgroup","{");m("\\egroup","}");m("~","\\nobreakspace");m("\\lq","`");m("\\rq","'");m("\\aa","\\r a");m("\\AA","\\r A");m("\\textcopyright","\\html@mathml{\\textcircled{c}}{\\char`\xA9}");m("\\copyright","\\TextOrMath{\\textcopyright}{\\text{\\textcopyright}}");m("\\textregistered","\\html@mathml{\\textcircled{\\scriptsize R}}{\\char`\xAE}");m("\u212C","\\mathscr{B}");m("\u2130","\\mathscr{E}");m("\u2131","\\mathscr{F}");m("\u210B","\\mathscr{H}");m("\u2110","\\mathscr{I}");m("\u2112","\\mathscr{L}");m("\u2133","\\mathscr{M}");m("\u211B","\\mathscr{R}");m("\u212D","\\mathfrak{C}");m("\u210C","\\mathfrak{H}");m("\u2128","\\mathfrak{Z}");m("\\Bbbk","\\Bbb{k}");m("\xB7","\\cdotp");m("\\llap","\\mathllap{\\textrm{#1}}");m("\\rlap","\\mathrlap{\\textrm{#1}}");m("\\clap","\\mathclap{\\textrm{#1}}");m("\\mathstrut","\\vphantom{(}");m("\\underbar","\\underline{\\text{#1}}");m("\\not",'\\html@mathml{\\mathrel{\\mathrlap\\@not}}{\\char"338}');m("\\neq","\\html@mathml{\\mathrel{\\not=}}{\\mathrel{\\char`\u2260}}");m("\\ne","\\neq");m("\u2260","\\neq");m("\\notin","\\html@mathml{\\mathrel{{\\in}\\mathllap{/\\mskip1mu}}}{\\mathrel{\\char`\u2209}}");m("\u2209","\\notin");m("\u2258","\\html@mathml{\\mathrel{=\\kern{-1em}\\raisebox{0.4em}{$\\scriptsize\\frown$}}}{\\mathrel{\\char`\u2258}}");m("\u2259","\\html@mathml{\\stackrel{\\tiny\\wedge}{=}}{\\mathrel{\\char`\u2258}}");m("\u225A","\\html@mathml{\\stackrel{\\tiny\\vee}{=}}{\\mathrel{\\char`\u225A}}");m("\u225B","\\html@mathml{\\stackrel{\\scriptsize\\star}{=}}{\\mathrel{\\char`\u225B}}");m("\u225D","\\html@mathml{\\stackrel{\\tiny\\mathrm{def}}{=}}{\\mathrel{\\char`\u225D}}");m("\u225E","\\html@mathml{\\stackrel{\\tiny\\mathrm{m}}{=}}{\\mathrel{\\char`\u225E}}");m("\u225F","\\html@mathml{\\stackrel{\\tiny?}{=}}{\\mathrel{\\char`\u225F}}");m("\u27C2","\\perp");m("\u203C","\\mathclose{!\\mkern-0.8mu!}");m("\u220C","\\notni");m("\u231C","\\ulcorner");m("\u231D","\\urcorner");m("\u231E","\\llcorner");m("\u231F","\\lrcorner");m("\xA9","\\copyright");m("\xAE","\\textregistered");m("\uFE0F","\\textregistered");m("\\ulcorner",'\\html@mathml{\\@ulcorner}{\\mathop{\\char"231c}}');m("\\urcorner",'\\html@mathml{\\@urcorner}{\\mathop{\\char"231d}}');m("\\llcorner",'\\html@mathml{\\@llcorner}{\\mathop{\\char"231e}}');m("\\lrcorner",'\\html@mathml{\\@lrcorner}{\\mathop{\\char"231f}}');m("\\vdots","{\\varvdots\\rule{0pt}{15pt}}");m("\u22EE","\\vdots");m("\\varGamma","\\mathit{\\Gamma}");m("\\varDelta","\\mathit{\\Delta}");m("\\varTheta","\\mathit{\\Theta}");m("\\varLambda","\\mathit{\\Lambda}");m("\\varXi","\\mathit{\\Xi}");m("\\varPi","\\mathit{\\Pi}");m("\\varSigma","\\mathit{\\Sigma}");m("\\varUpsilon","\\mathit{\\Upsilon}");m("\\varPhi","\\mathit{\\Phi}");m("\\varPsi","\\mathit{\\Psi}");m("\\varOmega","\\mathit{\\Omega}");m("\\substack","\\begin{subarray}{c}#1\\end{subarray}");m("\\colon","\\nobreak\\mskip2mu\\mathpunct{}\\mathchoice{\\mkern-3mu}{\\mkern-3mu}{}{}{:}\\mskip6mu\\relax");m("\\boxed","\\fbox{$\\displaystyle{#1}$}");m("\\iff","\\DOTSB\\;\\Longleftrightarrow\\;");m("\\implies","\\DOTSB\\;\\Longrightarrow\\;");m("\\impliedby","\\DOTSB\\;\\Longleftarrow\\;");m("\\dddot","{\\overset{\\raisebox{-0.1ex}{\\normalsize ...}}{#1}}");m("\\ddddot","{\\overset{\\raisebox{-0.1ex}{\\normalsize ....}}{#1}}");var Sr={",":"\\dotsc","\\not":"\\dotsb","+":"\\dotsb","=":"\\dotsb","<":"\\dotsb",">":"\\dotsb","-":"\\dotsb","*":"\\dotsb",":":"\\dotsb","\\DOTSB":"\\dotsb","\\coprod":"\\dotsb","\\bigvee":"\\dotsb","\\bigwedge":"\\dotsb","\\biguplus":"\\dotsb","\\bigcap":"\\dotsb","\\bigcup":"\\dotsb","\\prod":"\\dotsb","\\sum":"\\dotsb","\\bigotimes":"\\dotsb","\\bigoplus":"\\dotsb","\\bigodot":"\\dotsb","\\bigsqcup":"\\dotsb","\\And":"\\dotsb","\\longrightarrow":"\\dotsb","\\Longrightarrow":"\\dotsb","\\longleftarrow":"\\dotsb","\\Longleftarrow":"\\dotsb","\\longleftrightarrow":"\\dotsb","\\Longleftrightarrow":"\\dotsb","\\mapsto":"\\dotsb","\\longmapsto":"\\dotsb","\\hookrightarrow":"\\dotsb","\\doteq":"\\dotsb","\\mathbin":"\\dotsb","\\mathrel":"\\dotsb","\\relbar":"\\dotsb","\\Relbar":"\\dotsb","\\xrightarrow":"\\dotsb","\\xleftarrow":"\\dotsb","\\DOTSI":"\\dotsi","\\int":"\\dotsi","\\oint":"\\dotsi","\\iint":"\\dotsi","\\iiint":"\\dotsi","\\iiiint":"\\dotsi","\\idotsint":"\\dotsi","\\DOTSX":"\\dotsx"};m("\\dots",function(r){var e="\\dotso",t=r.expandAfterFuture().text;return t in Sr?e=Sr[t]:(t.slice(0,4)==="\\not"||t in Y.math&&O.contains(["bin","rel"],Y.math[t].group))&&(e="\\dotsb"),e});var Ht={")":!0,"]":!0,"\\rbrack":!0,"\\}":!0,"\\rbrace":!0,"\\rangle":!0,"\\rceil":!0,"\\rfloor":!0,"\\rgroup":!0,"\\rmoustache":!0,"\\right":!0,"\\bigr":!0,"\\biggr":!0,"\\Bigr":!0,"\\Biggr":!0,$:!0,";":!0,".":!0,",":!0};m("\\dotso",function(r){var e=r.future().text;return e in Ht?"\\ldots\\,":"\\ldots"});m("\\dotsc",function(r){var e=r.future().text;return e in Ht&&e!==","?"\\ldots\\,":"\\ldots"});m("\\cdots",function(r){var e=r.future().text;return e in Ht?"\\@cdots\\,":"\\@cdots"});m("\\dotsb","\\cdots");m("\\dotsm","\\cdots");m("\\dotsi","\\!\\cdots");m("\\dotsx","\\ldots\\,");m("\\DOTSI","\\relax");m("\\DOTSB","\\relax");m("\\DOTSX","\\relax");m("\\tmspace","\\TextOrMath{\\kern#1#3}{\\mskip#1#2}\\relax");m("\\,","\\tmspace+{3mu}{.1667em}");m("\\thinspace","\\,");m("\\>","\\mskip{4mu}");m("\\:","\\tmspace+{4mu}{.2222em}");m("\\medspace","\\:");m("\\;","\\tmspace+{5mu}{.2777em}");m("\\thickspace","\\;");m("\\!","\\tmspace-{3mu}{.1667em}");m("\\negthinspace","\\!");m("\\negmedspace","\\tmspace-{4mu}{.2222em}");m("\\negthickspace","\\tmspace-{5mu}{.277em}");m("\\enspace","\\kern.5em ");m("\\enskip","\\hskip.5em\\relax");m("\\quad","\\hskip1em\\relax");m("\\qquad","\\hskip2em\\relax");m("\\tag","\\@ifstar\\tag@literal\\tag@paren");m("\\tag@paren","\\tag@literal{({#1})}");m("\\tag@literal",r=>{if(r.macros.get("\\df@tag"))throw new z("Multiple \\tag");return"\\gdef\\df@tag{\\text{#1}}"});m("\\bmod","\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}\\mathbin{\\rm mod}\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}");m("\\pod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern8mu}{\\mkern8mu}{\\mkern8mu}(#1)");m("\\pmod","\\pod{{\\rm mod}\\mkern6mu#1}");m("\\mod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern12mu}{\\mkern12mu}{\\mkern12mu}{\\rm mod}\\,\\,#1");m("\\newline","\\\\\\relax");m("\\TeX","\\textrm{\\html@mathml{T\\kern-.1667em\\raisebox{-.5ex}{E}\\kern-.125emX}{TeX}}");var fa=T(z0["Main-Regular"][84][1]-.7*z0["Main-Regular"][65][1]);m("\\LaTeX","\\textrm{\\html@mathml{"+("L\\kern-.36em\\raisebox{"+fa+"}{\\scriptstyle A}")+"\\kern-.15em\\TeX}{LaTeX}}");m("\\KaTeX","\\textrm{\\html@mathml{"+("K\\kern-.17em\\raisebox{"+fa+"}{\\scriptstyle A}")+"\\kern-.15em\\TeX}{KaTeX}}");m("\\hspace","\\@ifstar\\@hspacer\\@hspace");m("\\@hspace","\\hskip #1\\relax");m("\\@hspacer","\\rule{0pt}{0pt}\\hskip #1\\relax");m("\\ordinarycolon",":");m("\\vcentcolon","\\mathrel{\\mathop\\ordinarycolon}");m("\\dblcolon",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-.9mu}\\vcentcolon}}{\\mathop{\\char"2237}}');m("\\coloneqq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2254}}');m("\\Coloneqq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2237\\char"3d}}');m("\\coloneq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"3a\\char"2212}}');m("\\Coloneq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"2237\\char"2212}}');m("\\eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2255}}');m("\\Eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"3d\\char"2237}}');m("\\eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2239}}');m("\\Eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"2212\\char"2237}}');m("\\colonapprox",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"3a\\char"2248}}');m("\\Colonapprox",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"2237\\char"2248}}');m("\\colonsim",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"3a\\char"223c}}');m("\\Colonsim",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"2237\\char"223c}}');m("\u2237","\\dblcolon");m("\u2239","\\eqcolon");m("\u2254","\\coloneqq");m("\u2255","\\eqqcolon");m("\u2A74","\\Coloneqq");m("\\ratio","\\vcentcolon");m("\\coloncolon","\\dblcolon");m("\\colonequals","\\coloneqq");m("\\coloncolonequals","\\Coloneqq");m("\\equalscolon","\\eqqcolon");m("\\equalscoloncolon","\\Eqqcolon");m("\\colonminus","\\coloneq");m("\\coloncolonminus","\\Coloneq");m("\\minuscolon","\\eqcolon");m("\\minuscoloncolon","\\Eqcolon");m("\\coloncolonapprox","\\Colonapprox");m("\\coloncolonsim","\\Colonsim");m("\\simcolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\vcentcolon}");m("\\simcoloncolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\dblcolon}");m("\\approxcolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\vcentcolon}");m("\\approxcoloncolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\dblcolon}");m("\\notni","\\html@mathml{\\not\\ni}{\\mathrel{\\char`\u220C}}");m("\\limsup","\\DOTSB\\operatorname*{lim\\,sup}");m("\\liminf","\\DOTSB\\operatorname*{lim\\,inf}");m("\\injlim","\\DOTSB\\operatorname*{inj\\,lim}");m("\\projlim","\\DOTSB\\operatorname*{proj\\,lim}");m("\\varlimsup","\\DOTSB\\operatorname*{\\overline{lim}}");m("\\varliminf","\\DOTSB\\operatorname*{\\underline{lim}}");m("\\varinjlim","\\DOTSB\\operatorname*{\\underrightarrow{lim}}");m("\\varprojlim","\\DOTSB\\operatorname*{\\underleftarrow{lim}}");m("\\gvertneqq","\\html@mathml{\\@gvertneqq}{\u2269}");m("\\lvertneqq","\\html@mathml{\\@lvertneqq}{\u2268}");m("\\ngeqq","\\html@mathml{\\@ngeqq}{\u2271}");m("\\ngeqslant","\\html@mathml{\\@ngeqslant}{\u2271}");m("\\nleqq","\\html@mathml{\\@nleqq}{\u2270}");m("\\nleqslant","\\html@mathml{\\@nleqslant}{\u2270}");m("\\nshortmid","\\html@mathml{\\@nshortmid}{\u2224}");m("\\nshortparallel","\\html@mathml{\\@nshortparallel}{\u2226}");m("\\nsubseteqq","\\html@mathml{\\@nsubseteqq}{\u2288}");m("\\nsupseteqq","\\html@mathml{\\@nsupseteqq}{\u2289}");m("\\varsubsetneq","\\html@mathml{\\@varsubsetneq}{\u228A}");m("\\varsubsetneqq","\\html@mathml{\\@varsubsetneqq}{\u2ACB}");m("\\varsupsetneq","\\html@mathml{\\@varsupsetneq}{\u228B}");m("\\varsupsetneqq","\\html@mathml{\\@varsupsetneqq}{\u2ACC}");m("\\imath","\\html@mathml{\\@imath}{\u0131}");m("\\jmath","\\html@mathml{\\@jmath}{\u0237}");m("\\llbracket","\\html@mathml{\\mathopen{[\\mkern-3.2mu[}}{\\mathopen{\\char`\u27E6}}");m("\\rrbracket","\\html@mathml{\\mathclose{]\\mkern-3.2mu]}}{\\mathclose{\\char`\u27E7}}");m("\u27E6","\\llbracket");m("\u27E7","\\rrbracket");m("\\lBrace","\\html@mathml{\\mathopen{\\{\\mkern-3.2mu[}}{\\mathopen{\\char`\u2983}}");m("\\rBrace","\\html@mathml{\\mathclose{]\\mkern-3.2mu\\}}}{\\mathclose{\\char`\u2984}}");m("\u2983","\\lBrace");m("\u2984","\\rBrace");m("\\minuso","\\mathbin{\\html@mathml{{\\mathrlap{\\mathchoice{\\kern{0.145em}}{\\kern{0.145em}}{\\kern{0.1015em}}{\\kern{0.0725em}}\\circ}{-}}}{\\char`\u29B5}}");m("\u29B5","\\minuso");m("\\darr","\\downarrow");m("\\dArr","\\Downarrow");m("\\Darr","\\Downarrow");m("\\lang","\\langle");m("\\rang","\\rangle");m("\\uarr","\\uparrow");m("\\uArr","\\Uparrow");m("\\Uarr","\\Uparrow");m("\\N","\\mathbb{N}");m("\\R","\\mathbb{R}");m("\\Z","\\mathbb{Z}");m("\\alef","\\aleph");m("\\alefsym","\\aleph");m("\\Alpha","\\mathrm{A}");m("\\Beta","\\mathrm{B}");m("\\bull","\\bullet");m("\\Chi","\\mathrm{X}");m("\\clubs","\\clubsuit");m("\\cnums","\\mathbb{C}");m("\\Complex","\\mathbb{C}");m("\\Dagger","\\ddagger");m("\\diamonds","\\diamondsuit");m("\\empty","\\emptyset");m("\\Epsilon","\\mathrm{E}");m("\\Eta","\\mathrm{H}");m("\\exist","\\exists");m("\\harr","\\leftrightarrow");m("\\hArr","\\Leftrightarrow");m("\\Harr","\\Leftrightarrow");m("\\hearts","\\heartsuit");m("\\image","\\Im");m("\\infin","\\infty");m("\\Iota","\\mathrm{I}");m("\\isin","\\in");m("\\Kappa","\\mathrm{K}");m("\\larr","\\leftarrow");m("\\lArr","\\Leftarrow");m("\\Larr","\\Leftarrow");m("\\lrarr","\\leftrightarrow");m("\\lrArr","\\Leftrightarrow");m("\\Lrarr","\\Leftrightarrow");m("\\Mu","\\mathrm{M}");m("\\natnums","\\mathbb{N}");m("\\Nu","\\mathrm{N}");m("\\Omicron","\\mathrm{O}");m("\\plusmn","\\pm");m("\\rarr","\\rightarrow");m("\\rArr","\\Rightarrow");m("\\Rarr","\\Rightarrow");m("\\real","\\Re");m("\\reals","\\mathbb{R}");m("\\Reals","\\mathbb{R}");m("\\Rho","\\mathrm{P}");m("\\sdot","\\cdot");m("\\sect","\\S");m("\\spades","\\spadesuit");m("\\sub","\\subset");m("\\sube","\\subseteq");m("\\supe","\\supseteq");m("\\Tau","\\mathrm{T}");m("\\thetasym","\\vartheta");m("\\weierp","\\wp");m("\\Zeta","\\mathrm{Z}");m("\\argmin","\\DOTSB\\operatorname*{arg\\,min}");m("\\argmax","\\DOTSB\\operatorname*{arg\\,max}");m("\\plim","\\DOTSB\\mathop{\\operatorname{plim}}\\limits");m("\\bra","\\mathinner{\\langle{#1}|}");m("\\ket","\\mathinner{|{#1}\\rangle}");m("\\braket","\\mathinner{\\langle{#1}\\rangle}");m("\\Bra","\\left\\langle#1\\right|");m("\\Ket","\\left|#1\\right\\rangle");var va=r=>e=>{var t=e.consumeArg().tokens,a=e.consumeArg().tokens,n=e.consumeArg().tokens,s=e.consumeArg().tokens,l=e.macros.get("|"),h=e.macros.get("\\|");e.macros.beginGroup();var c=b=>x=>{r&&(x.macros.set("|",l),n.length&&x.macros.set("\\|",h));var w=b;if(!b&&n.length){var A=x.future();A.text==="|"&&(x.popToken(),w=!0)}return{tokens:w?n:a,numArgs:0}};e.macros.set("|",c(!1)),n.length&&e.macros.set("\\|",c(!0));var f=e.consumeArg().tokens,v=e.expandTokens([...s,...f,...t]);return e.macros.endGroup(),{tokens:v.reverse(),numArgs:0}};m("\\bra@ket",va(!1));m("\\bra@set",va(!0));m("\\Braket","\\bra@ket{\\left\\langle}{\\,\\middle\\vert\\,}{\\,\\middle\\vert\\,}{\\right\\rangle}");m("\\Set","\\bra@set{\\left\\{\\:}{\\;\\middle\\vert\\;}{\\;\\middle\\Vert\\;}{\\:\\right\\}}");m("\\set","\\bra@set{\\{\\,}{\\mid}{}{\\,\\}}");m("\\angln","{\\angl n}");m("\\blue","\\textcolor{##6495ed}{#1}");m("\\orange","\\textcolor{##ffa500}{#1}");m("\\pink","\\textcolor{##ff00af}{#1}");m("\\red","\\textcolor{##df0030}{#1}");m("\\green","\\textcolor{##28ae7b}{#1}");m("\\gray","\\textcolor{gray}{#1}");m("\\purple","\\textcolor{##9d38bd}{#1}");m("\\blueA","\\textcolor{##ccfaff}{#1}");m("\\blueB","\\textcolor{##80f6ff}{#1}");m("\\blueC","\\textcolor{##63d9ea}{#1}");m("\\blueD","\\textcolor{##11accd}{#1}");m("\\blueE","\\textcolor{##0c7f99}{#1}");m("\\tealA","\\textcolor{##94fff5}{#1}");m("\\tealB","\\textcolor{##26edd5}{#1}");m("\\tealC","\\textcolor{##01d1c1}{#1}");m("\\tealD","\\textcolor{##01a995}{#1}");m("\\tealE","\\textcolor{##208170}{#1}");m("\\greenA","\\textcolor{##b6ffb0}{#1}");m("\\greenB","\\textcolor{##8af281}{#1}");m("\\greenC","\\textcolor{##74cf70}{#1}");m("\\greenD","\\textcolor{##1fab54}{#1}");m("\\greenE","\\textcolor{##0d923f}{#1}");m("\\goldA","\\textcolor{##ffd0a9}{#1}");m("\\goldB","\\textcolor{##ffbb71}{#1}");m("\\goldC","\\textcolor{##ff9c39}{#1}");m("\\goldD","\\textcolor{##e07d10}{#1}");m("\\goldE","\\textcolor{##a75a05}{#1}");m("\\redA","\\textcolor{##fca9a9}{#1}");m("\\redB","\\textcolor{##ff8482}{#1}");m("\\redC","\\textcolor{##f9685d}{#1}");m("\\redD","\\textcolor{##e84d39}{#1}");m("\\redE","\\textcolor{##bc2612}{#1}");m("\\maroonA","\\textcolor{##ffbde0}{#1}");m("\\maroonB","\\textcolor{##ff92c6}{#1}");m("\\maroonC","\\textcolor{##ed5fa6}{#1}");m("\\maroonD","\\textcolor{##ca337c}{#1}");m("\\maroonE","\\textcolor{##9e034e}{#1}");m("\\purpleA","\\textcolor{##ddd7ff}{#1}");m("\\purpleB","\\textcolor{##c6b9fc}{#1}");m("\\purpleC","\\textcolor{##aa87ff}{#1}");m("\\purpleD","\\textcolor{##7854ab}{#1}");m("\\purpleE","\\textcolor{##543b78}{#1}");m("\\mintA","\\textcolor{##f5f9e8}{#1}");m("\\mintB","\\textcolor{##edf2df}{#1}");m("\\mintC","\\textcolor{##e0e5cc}{#1}");m("\\grayA","\\textcolor{##f6f7f7}{#1}");m("\\grayB","\\textcolor{##f0f1f2}{#1}");m("\\grayC","\\textcolor{##e3e5e6}{#1}");m("\\grayD","\\textcolor{##d6d8da}{#1}");m("\\grayE","\\textcolor{##babec2}{#1}");m("\\grayF","\\textcolor{##888d93}{#1}");m("\\grayG","\\textcolor{##626569}{#1}");m("\\grayH","\\textcolor{##3b3e40}{#1}");m("\\grayI","\\textcolor{##21242c}{#1}");m("\\kaBlue","\\textcolor{##314453}{#1}");m("\\kaGreen","\\textcolor{##71B307}{#1}");var ga={"^":!0,_:!0,"\\limits":!0,"\\nolimits":!0},zt=class{constructor(e,t,a){this.settings=void 0,this.expansionCount=void 0,this.lexer=void 0,this.macros=void 0,this.stack=void 0,this.mode=void 0,this.settings=t,this.expansionCount=0,this.feed(e),this.macros=new Mt(fn,t.macros),this.mode=a,this.stack=[]}feed(e){this.lexer=new Pe(e,this.settings)}switchMode(e){this.mode=e}beginGroup(){this.macros.beginGroup()}endGroup(){this.macros.endGroup()}endGroups(){this.macros.endGroups()}future(){return this.stack.length===0&&this.pushToken(this.lexer.lex()),this.stack[this.stack.length-1]}popToken(){return this.future(),this.stack.pop()}pushToken(e){this.stack.push(e)}pushTokens(e){this.stack.push(...e)}scanArgument(e){var t,a,n;if(e){if(this.consumeSpaces(),this.future().text!=="[")return null;t=this.popToken(),{tokens:n,end:a}=this.consumeArg(["]"])}else({tokens:n,start:t,end:a}=this.consumeArg());return this.pushToken(new b0("EOF",a.loc)),this.pushTokens(n),t.range(a,"")}consumeSpaces(){for(;;){var e=this.future();if(e.text===" ")this.stack.pop();else break}}consumeArg(e){var t=[],a=e&&e.length>0;a||this.consumeSpaces();var n=this.future(),s,l=0,h=0;do{if(s=this.popToken(),t.push(s),s.text==="{")++l;else if(s.text==="}"){if(--l,l===-1)throw new z("Extra }",s)}else if(s.text==="EOF")throw new z("Unexpected end of input in a macro argument, expected '"+(e&&a?e[h]:"}")+"'",s);if(e&&a)if((l===0||l===1&&e[h]==="{")&&s.text===e[h]){if(++h,h===e.length){t.splice(-h,h);break}}else h=0}while(l!==0||a);return n.text==="{"&&t[t.length-1].text==="}"&&(t.pop(),t.shift()),t.reverse(),{tokens:t,start:n,end:s}}consumeArgs(e,t){if(t){if(t.length!==e+1)throw new z("The length of delimiters doesn't match the number of args!");for(var a=t[0],n=0;nthis.settings.maxExpand)throw new z("Too many expansions: infinite loop or need to increase maxExpand setting")}expandOnce(e){var t=this.popToken(),a=t.text,n=t.noexpand?null:this._getExpansion(a);if(n==null||e&&n.unexpandable){if(e&&n==null&&a[0]==="\\"&&!this.isDefined(a))throw new z("Undefined control sequence: "+a);return this.pushToken(t),!1}this.countExpansion(1);var s=n.tokens,l=this.consumeArgs(n.numArgs,n.delimiters);if(n.numArgs){s=s.slice();for(var h=s.length-1;h>=0;--h){var c=s[h];if(c.text==="#"){if(h===0)throw new z("Incomplete placeholder at end of macro body",c);if(c=s[--h],c.text==="#")s.splice(h+1,1);else if(/^[1-9]$/.test(c.text))s.splice(h,2,...l[+c.text-1]);else throw new z("Not a valid argument number",c)}}}return this.pushTokens(s),s.length}expandAfterFuture(){return this.expandOnce(),this.future()}expandNextToken(){for(;;)if(this.expandOnce()===!1){var e=this.stack.pop();return e.treatAsRelax&&(e.text="\\relax"),e}throw new Error}expandMacro(e){return this.macros.has(e)?this.expandTokens([new b0(e)]):void 0}expandTokens(e){var t=[],a=this.stack.length;for(this.pushTokens(e);this.stack.length>a;)if(this.expandOnce(!0)===!1){var n=this.stack.pop();n.treatAsRelax&&(n.noexpand=!1,n.treatAsRelax=!1),t.push(n)}return this.countExpansion(t.length),t}expandMacroAsText(e){var t=this.expandMacro(e);return t&&t.map(a=>a.text).join("")}_getExpansion(e){var t=this.macros.get(e);if(t==null)return t;if(e.length===1){var a=this.lexer.catcodes[e];if(a!=null&&a!==13)return}var n=typeof t=="function"?t(this):t;if(typeof n=="string"){var s=0;if(n.indexOf("#")!==-1)for(var l=n.replace(/##/g,"");l.indexOf("#"+(s+1))!==-1;)++s;for(var h=new Pe(n,this.settings),c=[],f=h.lex();f.text!=="EOF";)c.push(f),f=h.lex();c.reverse();var v={tokens:c,numArgs:s};return v}return n}isDefined(e){return this.macros.has(e)||P0.hasOwnProperty(e)||Y.math.hasOwnProperty(e)||Y.text.hasOwnProperty(e)||ga.hasOwnProperty(e)}isExpandable(e){var t=this.macros.get(e);return t!=null?typeof t=="string"||typeof t=="function"||!t.unexpandable:P0.hasOwnProperty(e)&&!P0[e].primitive}},kr=/^[₊₋₌₍₎₀₁₂₃₄₅₆₇₈₉ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓᵦᵧᵨᵩᵪ]/,Ne=Object.freeze({"\u208A":"+","\u208B":"-","\u208C":"=","\u208D":"(","\u208E":")","\u2080":"0","\u2081":"1","\u2082":"2","\u2083":"3","\u2084":"4","\u2085":"5","\u2086":"6","\u2087":"7","\u2088":"8","\u2089":"9","\u2090":"a","\u2091":"e","\u2095":"h","\u1D62":"i","\u2C7C":"j","\u2096":"k","\u2097":"l","\u2098":"m","\u2099":"n","\u2092":"o","\u209A":"p","\u1D63":"r","\u209B":"s","\u209C":"t","\u1D64":"u","\u1D65":"v","\u2093":"x","\u1D66":"\u03B2","\u1D67":"\u03B3","\u1D68":"\u03C1","\u1D69":"\u03D5","\u1D6A":"\u03C7","\u207A":"+","\u207B":"-","\u207C":"=","\u207D":"(","\u207E":")","\u2070":"0","\xB9":"1","\xB2":"2","\xB3":"3","\u2074":"4","\u2075":"5","\u2076":"6","\u2077":"7","\u2078":"8","\u2079":"9","\u1D2C":"A","\u1D2E":"B","\u1D30":"D","\u1D31":"E","\u1D33":"G","\u1D34":"H","\u1D35":"I","\u1D36":"J","\u1D37":"K","\u1D38":"L","\u1D39":"M","\u1D3A":"N","\u1D3C":"O","\u1D3E":"P","\u1D3F":"R","\u1D40":"T","\u1D41":"U","\u2C7D":"V","\u1D42":"W","\u1D43":"a","\u1D47":"b","\u1D9C":"c","\u1D48":"d","\u1D49":"e","\u1DA0":"f","\u1D4D":"g",\u02B0:"h","\u2071":"i",\u02B2:"j","\u1D4F":"k",\u02E1:"l","\u1D50":"m",\u207F:"n","\u1D52":"o","\u1D56":"p",\u02B3:"r",\u02E2:"s","\u1D57":"t","\u1D58":"u","\u1D5B":"v",\u02B7:"w",\u02E3:"x",\u02B8:"y","\u1DBB":"z","\u1D5D":"\u03B2","\u1D5E":"\u03B3","\u1D5F":"\u03B4","\u1D60":"\u03D5","\u1D61":"\u03C7","\u1DBF":"\u03B8"}),dt={"\u0301":{text:"\\'",math:"\\acute"},"\u0300":{text:"\\`",math:"\\grave"},"\u0308":{text:'\\"',math:"\\ddot"},"\u0303":{text:"\\~",math:"\\tilde"},"\u0304":{text:"\\=",math:"\\bar"},"\u0306":{text:"\\u",math:"\\breve"},"\u030C":{text:"\\v",math:"\\check"},"\u0302":{text:"\\^",math:"\\hat"},"\u0307":{text:"\\.",math:"\\dot"},"\u030A":{text:"\\r",math:"\\mathring"},"\u030B":{text:"\\H"},"\u0327":{text:"\\c"}},Mr={\u00E1:"a\u0301",\u00E0:"a\u0300",\u00E4:"a\u0308",\u01DF:"a\u0308\u0304",\u00E3:"a\u0303",\u0101:"a\u0304",\u0103:"a\u0306",\u1EAF:"a\u0306\u0301",\u1EB1:"a\u0306\u0300",\u1EB5:"a\u0306\u0303",\u01CE:"a\u030C",\u00E2:"a\u0302",\u1EA5:"a\u0302\u0301",\u1EA7:"a\u0302\u0300",\u1EAB:"a\u0302\u0303",\u0227:"a\u0307",\u01E1:"a\u0307\u0304",\u00E5:"a\u030A",\u01FB:"a\u030A\u0301",\u1E03:"b\u0307",\u0107:"c\u0301",\u1E09:"c\u0327\u0301",\u010D:"c\u030C",\u0109:"c\u0302",\u010B:"c\u0307",\u00E7:"c\u0327",\u010F:"d\u030C",\u1E0B:"d\u0307",\u1E11:"d\u0327",\u00E9:"e\u0301",\u00E8:"e\u0300",\u00EB:"e\u0308",\u1EBD:"e\u0303",\u0113:"e\u0304",\u1E17:"e\u0304\u0301",\u1E15:"e\u0304\u0300",\u0115:"e\u0306",\u1E1D:"e\u0327\u0306",\u011B:"e\u030C",\u00EA:"e\u0302",\u1EBF:"e\u0302\u0301",\u1EC1:"e\u0302\u0300",\u1EC5:"e\u0302\u0303",\u0117:"e\u0307",\u0229:"e\u0327",\u1E1F:"f\u0307",\u01F5:"g\u0301",\u1E21:"g\u0304",\u011F:"g\u0306",\u01E7:"g\u030C",\u011D:"g\u0302",\u0121:"g\u0307",\u0123:"g\u0327",\u1E27:"h\u0308",\u021F:"h\u030C",\u0125:"h\u0302",\u1E23:"h\u0307",\u1E29:"h\u0327",\u00ED:"i\u0301",\u00EC:"i\u0300",\u00EF:"i\u0308",\u1E2F:"i\u0308\u0301",\u0129:"i\u0303",\u012B:"i\u0304",\u012D:"i\u0306",\u01D0:"i\u030C",\u00EE:"i\u0302",\u01F0:"j\u030C",\u0135:"j\u0302",\u1E31:"k\u0301",\u01E9:"k\u030C",\u0137:"k\u0327",\u013A:"l\u0301",\u013E:"l\u030C",\u013C:"l\u0327",\u1E3F:"m\u0301",\u1E41:"m\u0307",\u0144:"n\u0301",\u01F9:"n\u0300",\u00F1:"n\u0303",\u0148:"n\u030C",\u1E45:"n\u0307",\u0146:"n\u0327",\u00F3:"o\u0301",\u00F2:"o\u0300",\u00F6:"o\u0308",\u022B:"o\u0308\u0304",\u00F5:"o\u0303",\u1E4D:"o\u0303\u0301",\u1E4F:"o\u0303\u0308",\u022D:"o\u0303\u0304",\u014D:"o\u0304",\u1E53:"o\u0304\u0301",\u1E51:"o\u0304\u0300",\u014F:"o\u0306",\u01D2:"o\u030C",\u00F4:"o\u0302",\u1ED1:"o\u0302\u0301",\u1ED3:"o\u0302\u0300",\u1ED7:"o\u0302\u0303",\u022F:"o\u0307",\u0231:"o\u0307\u0304",\u0151:"o\u030B",\u1E55:"p\u0301",\u1E57:"p\u0307",\u0155:"r\u0301",\u0159:"r\u030C",\u1E59:"r\u0307",\u0157:"r\u0327",\u015B:"s\u0301",\u1E65:"s\u0301\u0307",\u0161:"s\u030C",\u1E67:"s\u030C\u0307",\u015D:"s\u0302",\u1E61:"s\u0307",\u015F:"s\u0327",\u1E97:"t\u0308",\u0165:"t\u030C",\u1E6B:"t\u0307",\u0163:"t\u0327",\u00FA:"u\u0301",\u00F9:"u\u0300",\u00FC:"u\u0308",\u01D8:"u\u0308\u0301",\u01DC:"u\u0308\u0300",\u01D6:"u\u0308\u0304",\u01DA:"u\u0308\u030C",\u0169:"u\u0303",\u1E79:"u\u0303\u0301",\u016B:"u\u0304",\u1E7B:"u\u0304\u0308",\u016D:"u\u0306",\u01D4:"u\u030C",\u00FB:"u\u0302",\u016F:"u\u030A",\u0171:"u\u030B",\u1E7D:"v\u0303",\u1E83:"w\u0301",\u1E81:"w\u0300",\u1E85:"w\u0308",\u0175:"w\u0302",\u1E87:"w\u0307",\u1E98:"w\u030A",\u1E8D:"x\u0308",\u1E8B:"x\u0307",\u00FD:"y\u0301",\u1EF3:"y\u0300",\u00FF:"y\u0308",\u1EF9:"y\u0303",\u0233:"y\u0304",\u0177:"y\u0302",\u1E8F:"y\u0307",\u1E99:"y\u030A",\u017A:"z\u0301",\u017E:"z\u030C",\u1E91:"z\u0302",\u017C:"z\u0307",\u00C1:"A\u0301",\u00C0:"A\u0300",\u00C4:"A\u0308",\u01DE:"A\u0308\u0304",\u00C3:"A\u0303",\u0100:"A\u0304",\u0102:"A\u0306",\u1EAE:"A\u0306\u0301",\u1EB0:"A\u0306\u0300",\u1EB4:"A\u0306\u0303",\u01CD:"A\u030C",\u00C2:"A\u0302",\u1EA4:"A\u0302\u0301",\u1EA6:"A\u0302\u0300",\u1EAA:"A\u0302\u0303",\u0226:"A\u0307",\u01E0:"A\u0307\u0304",\u00C5:"A\u030A",\u01FA:"A\u030A\u0301",\u1E02:"B\u0307",\u0106:"C\u0301",\u1E08:"C\u0327\u0301",\u010C:"C\u030C",\u0108:"C\u0302",\u010A:"C\u0307",\u00C7:"C\u0327",\u010E:"D\u030C",\u1E0A:"D\u0307",\u1E10:"D\u0327",\u00C9:"E\u0301",\u00C8:"E\u0300",\u00CB:"E\u0308",\u1EBC:"E\u0303",\u0112:"E\u0304",\u1E16:"E\u0304\u0301",\u1E14:"E\u0304\u0300",\u0114:"E\u0306",\u1E1C:"E\u0327\u0306",\u011A:"E\u030C",\u00CA:"E\u0302",\u1EBE:"E\u0302\u0301",\u1EC0:"E\u0302\u0300",\u1EC4:"E\u0302\u0303",\u0116:"E\u0307",\u0228:"E\u0327",\u1E1E:"F\u0307",\u01F4:"G\u0301",\u1E20:"G\u0304",\u011E:"G\u0306",\u01E6:"G\u030C",\u011C:"G\u0302",\u0120:"G\u0307",\u0122:"G\u0327",\u1E26:"H\u0308",\u021E:"H\u030C",\u0124:"H\u0302",\u1E22:"H\u0307",\u1E28:"H\u0327",\u00CD:"I\u0301",\u00CC:"I\u0300",\u00CF:"I\u0308",\u1E2E:"I\u0308\u0301",\u0128:"I\u0303",\u012A:"I\u0304",\u012C:"I\u0306",\u01CF:"I\u030C",\u00CE:"I\u0302",\u0130:"I\u0307",\u0134:"J\u0302",\u1E30:"K\u0301",\u01E8:"K\u030C",\u0136:"K\u0327",\u0139:"L\u0301",\u013D:"L\u030C",\u013B:"L\u0327",\u1E3E:"M\u0301",\u1E40:"M\u0307",\u0143:"N\u0301",\u01F8:"N\u0300",\u00D1:"N\u0303",\u0147:"N\u030C",\u1E44:"N\u0307",\u0145:"N\u0327",\u00D3:"O\u0301",\u00D2:"O\u0300",\u00D6:"O\u0308",\u022A:"O\u0308\u0304",\u00D5:"O\u0303",\u1E4C:"O\u0303\u0301",\u1E4E:"O\u0303\u0308",\u022C:"O\u0303\u0304",\u014C:"O\u0304",\u1E52:"O\u0304\u0301",\u1E50:"O\u0304\u0300",\u014E:"O\u0306",\u01D1:"O\u030C",\u00D4:"O\u0302",\u1ED0:"O\u0302\u0301",\u1ED2:"O\u0302\u0300",\u1ED6:"O\u0302\u0303",\u022E:"O\u0307",\u0230:"O\u0307\u0304",\u0150:"O\u030B",\u1E54:"P\u0301",\u1E56:"P\u0307",\u0154:"R\u0301",\u0158:"R\u030C",\u1E58:"R\u0307",\u0156:"R\u0327",\u015A:"S\u0301",\u1E64:"S\u0301\u0307",\u0160:"S\u030C",\u1E66:"S\u030C\u0307",\u015C:"S\u0302",\u1E60:"S\u0307",\u015E:"S\u0327",\u0164:"T\u030C",\u1E6A:"T\u0307",\u0162:"T\u0327",\u00DA:"U\u0301",\u00D9:"U\u0300",\u00DC:"U\u0308",\u01D7:"U\u0308\u0301",\u01DB:"U\u0308\u0300",\u01D5:"U\u0308\u0304",\u01D9:"U\u0308\u030C",\u0168:"U\u0303",\u1E78:"U\u0303\u0301",\u016A:"U\u0304",\u1E7A:"U\u0304\u0308",\u016C:"U\u0306",\u01D3:"U\u030C",\u00DB:"U\u0302",\u016E:"U\u030A",\u0170:"U\u030B",\u1E7C:"V\u0303",\u1E82:"W\u0301",\u1E80:"W\u0300",\u1E84:"W\u0308",\u0174:"W\u0302",\u1E86:"W\u0307",\u1E8C:"X\u0308",\u1E8A:"X\u0307",\u00DD:"Y\u0301",\u1EF2:"Y\u0300",\u0178:"Y\u0308",\u1EF8:"Y\u0303",\u0232:"Y\u0304",\u0176:"Y\u0302",\u1E8E:"Y\u0307",\u0179:"Z\u0301",\u017D:"Z\u030C",\u1E90:"Z\u0302",\u017B:"Z\u0307",\u03AC:"\u03B1\u0301",\u1F70:"\u03B1\u0300",\u1FB1:"\u03B1\u0304",\u1FB0:"\u03B1\u0306",\u03AD:"\u03B5\u0301",\u1F72:"\u03B5\u0300",\u03AE:"\u03B7\u0301",\u1F74:"\u03B7\u0300",\u03AF:"\u03B9\u0301",\u1F76:"\u03B9\u0300",\u03CA:"\u03B9\u0308",\u0390:"\u03B9\u0308\u0301",\u1FD2:"\u03B9\u0308\u0300",\u1FD1:"\u03B9\u0304",\u1FD0:"\u03B9\u0306",\u03CC:"\u03BF\u0301",\u1F78:"\u03BF\u0300",\u03CD:"\u03C5\u0301",\u1F7A:"\u03C5\u0300",\u03CB:"\u03C5\u0308",\u03B0:"\u03C5\u0308\u0301",\u1FE2:"\u03C5\u0308\u0300",\u1FE1:"\u03C5\u0304",\u1FE0:"\u03C5\u0306",\u03CE:"\u03C9\u0301",\u1F7C:"\u03C9\u0300",\u038E:"\u03A5\u0301",\u1FEA:"\u03A5\u0300",\u03AB:"\u03A5\u0308",\u1FE9:"\u03A5\u0304",\u1FE8:"\u03A5\u0306",\u038F:"\u03A9\u0301",\u1FFA:"\u03A9\u0300"},Ge=class r{constructor(e,t){this.mode=void 0,this.gullet=void 0,this.settings=void 0,this.leftrightDepth=void 0,this.nextToken=void 0,this.mode="math",this.gullet=new zt(e,t,this.mode),this.settings=t,this.leftrightDepth=0}expect(e,t){if(t===void 0&&(t=!0),this.fetch().text!==e)throw new z("Expected '"+e+"', got '"+this.fetch().text+"'",this.fetch());t&&this.consume()}consume(){this.nextToken=null}fetch(){return this.nextToken==null&&(this.nextToken=this.gullet.expandNextToken()),this.nextToken}switchMode(e){this.mode=e,this.gullet.switchMode(e)}parse(){this.settings.globalGroup||this.gullet.beginGroup(),this.settings.colorIsTextColor&&this.gullet.macros.set("\\color","\\textcolor");try{var e=this.parseExpression(!1);return this.expect("EOF"),this.settings.globalGroup||this.gullet.endGroup(),e}finally{this.gullet.endGroups()}}subparse(e){var t=this.nextToken;this.consume(),this.gullet.pushToken(new b0("}")),this.gullet.pushTokens(e);var a=this.parseExpression(!1);return this.expect("}"),this.nextToken=t,a}parseExpression(e,t){for(var a=[];;){this.mode==="math"&&this.consumeSpaces();var n=this.fetch();if(r.endOfExpression.indexOf(n.text)!==-1||t&&n.text===t||e&&P0[n.text]&&P0[n.text].infix)break;var s=this.parseAtom(t);if(s){if(s.type==="internal")continue}else break;a.push(s)}return this.mode==="text"&&this.formLigatures(a),this.handleInfixNodes(a)}handleInfixNodes(e){for(var t=-1,a,n=0;n=0&&this.settings.reportNonstrict("unicodeTextInMathMode",'Latin-1/Unicode text character "'+t[0]+'" used in math mode',e);var h=Y[this.mode][t].group,c=d0.range(e),f;if(i1.hasOwnProperty(h)){var v=h;f={type:"atom",mode:this.mode,family:v,loc:c,text:t}}else f={type:h,mode:this.mode,loc:c,text:t};l=f}else if(t.charCodeAt(0)>=128)this.settings.strict&&(Ar(t.charCodeAt(0))?this.mode==="math"&&this.settings.reportNonstrict("unicodeTextInMathMode",'Unicode text character "'+t[0]+'" used in math mode',e):this.settings.reportNonstrict("unknownSymbol",'Unrecognized Unicode character "'+t[0]+'"'+(" ("+t.charCodeAt(0)+")"),e)),l={type:"textord",mode:"text",loc:d0.range(e),text:t};else return null;if(this.consume(),s)for(var b=0;b=0;n--)r[n].loc.start>a&&(t+=" ",a=r[n].loc.start),t+=r[n].text,a+=r[n].text.length;var s=W.go(S.go(t,e));return s},S={go:function(r,e){if(!r)return[];e===void 0&&(e="ce");var t="0",a={};a.parenthesisLevel=0,r=r.replace(/\n/g," "),r=r.replace(/[\u2212\u2013\u2014\u2010]/g,"-"),r=r.replace(/[\u2026]/g,"...");for(var n,s=10,l=[];;){n!==r?(s=10,n=r):s--;var h=S.stateMachines[e],c=h.transitions[t]||h.transitions["*"];e:for(var f=0;f0){if(b.revisit||(r=v.remainder),!b.toContinue)break e}else return l}}if(s<=0)throw["MhchemBugU","mhchem bug U. Please report."]}},concatArray:function(r,e){if(e)if(Array.isArray(e))for(var t=0;t":/^[=<>]/,"#":/^[#\u2261]/,"+":/^\+/,"-$":/^-(?=[\s_},;\]/]|$|\([a-z]+\))/,"-9":/^-(?=[0-9])/,"- orbital overlap":/^-(?=(?:[spd]|sp)(?:$|[\s,;\)\]\}]))/,"-":/^-/,"pm-operator":/^(?:\\pm|\$\\pm\$|\+-|\+\/-)/,operator:/^(?:\+|(?:[\-=<>]|<<|>>|\\approx|\$\\approx\$)(?=\s|$|-?[0-9]))/,arrowUpDown:/^(?:v|\(v\)|\^|\(\^\))(?=$|[\s,;\)\]\}])/,"\\bond{(...)}":function(r){return S.patterns.findObserveGroups(r,"\\bond{","","","}")},"->":/^(?:<->|<-->|->|<-|<=>>|<<=>|<=>|[\u2192\u27F6\u21CC])/,CMT:/^[CMT](?=\[)/,"[(...)]":function(r){return S.patterns.findObserveGroups(r,"[","","","]")},"1st-level escape":/^(&|\\\\|\\hline)\s*/,"\\,":/^(?:\\[,\ ;:])/,"\\x{}{}":function(r){return S.patterns.findObserveGroups(r,"",/^\\[a-zA-Z]+\{/,"}","","","{","}","",!0)},"\\x{}":function(r){return S.patterns.findObserveGroups(r,"",/^\\[a-zA-Z]+\{/,"}","")},"\\ca":/^\\ca(?:\s+|(?![a-zA-Z]))/,"\\x":/^(?:\\[a-zA-Z]+\s*|\\[_&{}%])/,orbital:/^(?:[0-9]{1,2}[spdfgh]|[0-9]{0,2}sp)(?=$|[^a-zA-Z])/,others:/^[\/~|]/,"\\frac{(...)}":function(r){return S.patterns.findObserveGroups(r,"\\frac{","","","}","{","","","}")},"\\overset{(...)}":function(r){return S.patterns.findObserveGroups(r,"\\overset{","","","}","{","","","}")},"\\underset{(...)}":function(r){return S.patterns.findObserveGroups(r,"\\underset{","","","}","{","","","}")},"\\underbrace{(...)}":function(r){return S.patterns.findObserveGroups(r,"\\underbrace{","","","}_","{","","","}")},"\\color{(...)}0":function(r){return S.patterns.findObserveGroups(r,"\\color{","","","}")},"\\color{(...)}{(...)}1":function(r){return S.patterns.findObserveGroups(r,"\\color{","","","}","{","","","}")},"\\color(...){(...)}2":function(r){return S.patterns.findObserveGroups(r,"\\color","\\","",/^(?=\{)/,"{","","","}")},"\\ce{(...)}":function(r){return S.patterns.findObserveGroups(r,"\\ce{","","","}")},oxidation$:/^(?:[+-][IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/,"d-oxidation$":/^(?:[+-]?\s?[IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/,"roman numeral":/^[IVX]+/,"1/2$":/^[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+(?:\$[a-z]\$|[a-z])?$/,amount:function(r){var e;if(e=r.match(/^(?:(?:(?:\([+\-]?[0-9]+\/[0-9]+\)|[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+|[+\-]?[0-9]+[.,][0-9]+|[+\-]?\.[0-9]+|[+\-]?[0-9]+)(?:[a-z](?=\s*[A-Z]))?)|[+\-]?[a-z](?=\s*[A-Z])|\+(?!\s))/),e)return{match_:e[0],remainder:r.substr(e[0].length)};var t=S.patterns.findObserveGroups(r,"","$","$","");return t&&(e=t.match_.match(/^\$(?:\(?[+\-]?(?:[0-9]*[a-z]?[+\-])?[0-9]*[a-z](?:[+\-][0-9]*[a-z]?)?\)?|\+|-)\$$/),e)?{match_:e[0],remainder:r.substr(e[0].length)}:null},amount2:function(r){return this.amount(r)},"(KV letters),":/^(?:[A-Z][a-z]{0,2}|i)(?=,)/,formula$:function(r){if(r.match(/^\([a-z]+\)$/))return null;var e=r.match(/^(?:[a-z]|(?:[0-9\ \+\-\,\.\(\)]+[a-z])+[0-9\ \+\-\,\.\(\)]*|(?:[a-z][0-9\ \+\-\,\.\(\)]+)+[a-z]?)$/);return e?{match_:e[0],remainder:r.substr(e[0].length)}:null},uprightEntities:/^(?:pH|pOH|pC|pK|iPr|iBu)(?=$|[^a-zA-Z])/,"/":/^\s*(\/)\s*/,"//":/^\s*(\/\/)\s*/,"*":/^\s*[*.]\s*/},findObserveGroups:function(r,e,t,a,n,s,l,h,c,f){var v=function(D,N){if(typeof N=="string")return D.indexOf(N)!==0?null:N;var $=D.match(N);return $?$[0]:null},b=function(D,N,$){for(var H=0;N0,null},x=v(r,e);if(x===null||(r=r.substr(x.length),x=v(r,t),x===null))return null;var w=b(r,x.length,a||n);if(w===null)return null;var A=r.substring(0,a?w.endMatchEnd:w.endMatchBegin);if(s||l){var q=this.findObserveGroups(r.substr(w.endMatchEnd),s,l,h,c);if(q===null)return null;var _=[A,q.match_];return{match_:f?_.join(""):_,remainder:q.remainder}}else return{match_:A,remainder:r.substr(w.endMatchEnd)}},match_:function(r,e){var t=S.patterns.patterns[r];if(t===void 0)throw["MhchemBugP","mhchem bug P. Please report. ("+r+")"];if(typeof t=="function")return S.patterns.patterns[r](e);var a=e.match(t);if(a){var n;return a[2]?n=[a[1],a[2]]:a[1]?n=a[1]:n=a[0],{match_:n,remainder:e.substr(a[0].length)}}return null}},actions:{"a=":function(r,e){r.a=(r.a||"")+e},"b=":function(r,e){r.b=(r.b||"")+e},"p=":function(r,e){r.p=(r.p||"")+e},"o=":function(r,e){r.o=(r.o||"")+e},"q=":function(r,e){r.q=(r.q||"")+e},"d=":function(r,e){r.d=(r.d||"")+e},"rm=":function(r,e){r.rm=(r.rm||"")+e},"text=":function(r,e){r.text_=(r.text_||"")+e},insert:function(r,e,t){return{type_:t}},"insert+p1":function(r,e,t){return{type_:t,p1:e}},"insert+p1+p2":function(r,e,t){return{type_:t,p1:e[0],p2:e[1]}},copy:function(r,e){return e},rm:function(r,e){return{type_:"rm",p1:e||""}},text:function(r,e){return S.go(e,"text")},"{text}":function(r,e){var t=["{"];return S.concatArray(t,S.go(e,"text")),t.push("}"),t},"tex-math":function(r,e){return S.go(e,"tex-math")},"tex-math tight":function(r,e){return S.go(e,"tex-math tight")},bond:function(r,e,t){return{type_:"bond",kind_:t||e}},"color0-output":function(r,e){return{type_:"color0",color:e[0]}},ce:function(r,e){return S.go(e)},"1/2":function(r,e){var t=[];e.match(/^[+\-]/)&&(t.push(e.substr(0,1)),e=e.substr(1));var a=e.match(/^([0-9]+|\$[a-z]\$|[a-z])\/([0-9]+)(\$[a-z]\$|[a-z])?$/);return a[1]=a[1].replace(/\$/g,""),t.push({type_:"frac",p1:a[1],p2:a[2]}),a[3]&&(a[3]=a[3].replace(/\$/g,""),t.push({type_:"tex-math",p1:a[3]})),t},"9,9":function(r,e){return S.go(e,"9,9")}},createTransitions:function(r){var e,t,a,n,s={};for(e in r)for(t in r[e])for(a=t.split("|"),r[e][t].stateArray=a,n=0;n":{"0|1|2|3":{action_:"r=",nextState:"r"},"a|as":{action_:["output","r="],nextState:"r"},"*":{action_:["output","r="],nextState:"r"}},"+":{o:{action_:"d= kv",nextState:"d"},"d|D":{action_:"d=",nextState:"d"},q:{action_:"d=",nextState:"qd"},"qd|qD":{action_:"d=",nextState:"qd"},dq:{action_:["output","d="],nextState:"d"},3:{action_:["sb=false","output","operator"],nextState:"0"}},amount:{"0|2":{action_:"a=",nextState:"a"}},"pm-operator":{"0|1|2|a|as":{action_:["sb=false","output",{type_:"operator",option:"\\pm"}],nextState:"0"}},operator:{"0|1|2|a|as":{action_:["sb=false","output","operator"],nextState:"0"}},"-$":{"o|q":{action_:["charge or bond","output"],nextState:"qd"},d:{action_:"d=",nextState:"d"},D:{action_:["output",{type_:"bond",option:"-"}],nextState:"3"},q:{action_:"d=",nextState:"qd"},qd:{action_:"d=",nextState:"qd"},"qD|dq":{action_:["output",{type_:"bond",option:"-"}],nextState:"3"}},"-9":{"3|o":{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"3"}},"- orbital overlap":{o:{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"2"},d:{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"2"}},"-":{"0|1|2":{action_:[{type_:"output",option:1},"beginsWithBond=true",{type_:"bond",option:"-"}],nextState:"3"},3:{action_:{type_:"bond",option:"-"}},a:{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"2"},as:{action_:[{type_:"output",option:2},{type_:"bond",option:"-"}],nextState:"3"},b:{action_:"b="},o:{action_:{type_:"- after o/d",option:!1},nextState:"2"},q:{action_:{type_:"- after o/d",option:!1},nextState:"2"},"d|qd|dq":{action_:{type_:"- after o/d",option:!0},nextState:"2"},"D|qD|p":{action_:["output",{type_:"bond",option:"-"}],nextState:"3"}},amount2:{"1|3":{action_:"a=",nextState:"a"}},letters:{"0|1|2|3|a|as|b|p|bp|o":{action_:"o=",nextState:"o"},"q|dq":{action_:["output","o="],nextState:"o"},"d|D|qd|qD":{action_:"o after d",nextState:"o"}},digits:{o:{action_:"q=",nextState:"q"},"d|D":{action_:"q=",nextState:"dq"},q:{action_:["output","o="],nextState:"o"},a:{action_:"o=",nextState:"o"}},"space A":{"b|p|bp":{}},space:{a:{nextState:"as"},0:{action_:"sb=false"},"1|2":{action_:"sb=true"},"r|rt|rd|rdt|rdq":{action_:"output",nextState:"0"},"*":{action_:["output","sb=true"],nextState:"1"}},"1st-level escape":{"1|2":{action_:["output",{type_:"insert+p1",option:"1st-level escape"}]},"*":{action_:["output",{type_:"insert+p1",option:"1st-level escape"}],nextState:"0"}},"[(...)]":{"r|rt":{action_:"rd=",nextState:"rd"},"rd|rdt":{action_:"rq=",nextState:"rdq"}},"...":{"o|d|D|dq|qd|qD":{action_:["output",{type_:"bond",option:"..."}],nextState:"3"},"*":{action_:[{type_:"output",option:1},{type_:"insert",option:"ellipsis"}],nextState:"1"}},". |* ":{"*":{action_:["output",{type_:"insert",option:"addition compound"}],nextState:"1"}},"state of aggregation $":{"*":{action_:["output","state of aggregation"],nextState:"1"}},"{[(":{"a|as|o":{action_:["o=","output","parenthesisLevel++"],nextState:"2"},"0|1|2|3":{action_:["o=","output","parenthesisLevel++"],nextState:"2"},"*":{action_:["output","o=","output","parenthesisLevel++"],nextState:"2"}},")]}":{"0|1|2|3|b|p|bp|o":{action_:["o=","parenthesisLevel--"],nextState:"o"},"a|as|d|D|q|qd|qD|dq":{action_:["output","o=","parenthesisLevel--"],nextState:"o"}},", ":{"*":{action_:["output","comma"],nextState:"0"}},"^_":{"*":{}},"^{(...)}|^($...$)":{"0|1|2|as":{action_:"b=",nextState:"b"},p:{action_:"b=",nextState:"bp"},"3|o":{action_:"d= kv",nextState:"D"},q:{action_:"d=",nextState:"qD"},"d|D|qd|qD|dq":{action_:["output","d="],nextState:"D"}},"^a|^\\x{}{}|^\\x{}|^\\x|'":{"0|1|2|as":{action_:"b=",nextState:"b"},p:{action_:"b=",nextState:"bp"},"3|o":{action_:"d= kv",nextState:"d"},q:{action_:"d=",nextState:"qd"},"d|qd|D|qD":{action_:"d="},dq:{action_:["output","d="],nextState:"d"}},"_{(state of aggregation)}$":{"d|D|q|qd|qD|dq":{action_:["output","q="],nextState:"q"}},"_{(...)}|_($...$)|_9|_\\x{}{}|_\\x{}|_\\x":{"0|1|2|as":{action_:"p=",nextState:"p"},b:{action_:"p=",nextState:"bp"},"3|o":{action_:"q=",nextState:"q"},"d|D":{action_:"q=",nextState:"dq"},"q|qd|qD|dq":{action_:["output","q="],nextState:"q"}},"=<>":{"0|1|2|3|a|as|o|q|d|D|qd|qD|dq":{action_:[{type_:"output",option:2},"bond"],nextState:"3"}},"#":{"0|1|2|3|a|as|o":{action_:[{type_:"output",option:2},{type_:"bond",option:"#"}],nextState:"3"}},"{}":{"*":{action_:{type_:"output",option:1},nextState:"1"}},"{...}":{"0|1|2|3|a|as|b|p|bp":{action_:"o=",nextState:"o"},"o|d|D|q|qd|qD|dq":{action_:["output","o="],nextState:"o"}},"$...$":{a:{action_:"a="},"0|1|2|3|as|b|p|bp|o":{action_:"o=",nextState:"o"},"as|o":{action_:"o="},"q|d|D|qd|qD|dq":{action_:["output","o="],nextState:"o"}},"\\bond{(...)}":{"*":{action_:[{type_:"output",option:2},"bond"],nextState:"3"}},"\\frac{(...)}":{"*":{action_:[{type_:"output",option:1},"frac-output"],nextState:"3"}},"\\overset{(...)}":{"*":{action_:[{type_:"output",option:2},"overset-output"],nextState:"3"}},"\\underset{(...)}":{"*":{action_:[{type_:"output",option:2},"underset-output"],nextState:"3"}},"\\underbrace{(...)}":{"*":{action_:[{type_:"output",option:2},"underbrace-output"],nextState:"3"}},"\\color{(...)}{(...)}1|\\color(...){(...)}2":{"*":{action_:[{type_:"output",option:2},"color-output"],nextState:"3"}},"\\color{(...)}0":{"*":{action_:[{type_:"output",option:2},"color0-output"]}},"\\ce{(...)}":{"*":{action_:[{type_:"output",option:2},"ce"],nextState:"3"}},"\\,":{"*":{action_:[{type_:"output",option:1},"copy"],nextState:"1"}},"\\x{}{}|\\x{}|\\x":{"0|1|2|3|a|as|b|p|bp|o|c0":{action_:["o=","output"],nextState:"3"},"*":{action_:["output","o=","output"],nextState:"3"}},others:{"*":{action_:[{type_:"output",option:1},"copy"],nextState:"3"}},else2:{a:{action_:"a to o",nextState:"o",revisit:!0},as:{action_:["output","sb=true"],nextState:"1",revisit:!0},"r|rt|rd|rdt|rdq":{action_:["output"],nextState:"0",revisit:!0},"*":{action_:["output","copy"],nextState:"3"}}}),actions:{"o after d":function(r,e){var t;if((r.d||"").match(/^[0-9]+$/)){var a=r.d;r.d=void 0,t=this.output(r),r.b=a}else t=this.output(r);return S.actions["o="](r,e),t},"d= kv":function(r,e){r.d=e,r.dType="kv"},"charge or bond":function(r,e){if(r.beginsWithBond){var t=[];return S.concatArray(t,this.output(r)),S.concatArray(t,S.actions.bond(r,e,"-")),t}else r.d=e},"- after o/d":function(r,e,t){var a=S.patterns.match_("orbital",r.o||""),n=S.patterns.match_("one lowercase greek letter $",r.o||""),s=S.patterns.match_("one lowercase latin letter $",r.o||""),l=S.patterns.match_("$one lowercase latin letter$ $",r.o||""),h=e==="-"&&(a&&a.remainder===""||n||s||l);h&&!r.a&&!r.b&&!r.p&&!r.d&&!r.q&&!a&&s&&(r.o="$"+r.o+"$");var c=[];return h?(S.concatArray(c,this.output(r)),c.push({type_:"hyphen"})):(a=S.patterns.match_("digits",r.d||""),t&&a&&a.remainder===""?(S.concatArray(c,S.actions["d="](r,e)),S.concatArray(c,this.output(r))):(S.concatArray(c,this.output(r)),S.concatArray(c,S.actions.bond(r,e,"-")))),c},"a to o":function(r){r.o=r.a,r.a=void 0},"sb=true":function(r){r.sb=!0},"sb=false":function(r){r.sb=!1},"beginsWithBond=true":function(r){r.beginsWithBond=!0},"beginsWithBond=false":function(r){r.beginsWithBond=!1},"parenthesisLevel++":function(r){r.parenthesisLevel++},"parenthesisLevel--":function(r){r.parenthesisLevel--},"state of aggregation":function(r,e){return{type_:"state of aggregation",p1:S.go(e,"o")}},comma:function(r,e){var t=e.replace(/\s*$/,""),a=t!==e;return a&&r.parenthesisLevel===0?{type_:"comma enumeration L",p1:t}:{type_:"comma enumeration M",p1:t}},output:function(r,e,t){var a;if(!r.r)a=[],!r.a&&!r.b&&!r.p&&!r.o&&!r.q&&!r.d&&!t||(r.sb&&a.push({type_:"entitySkip"}),!r.o&&!r.q&&!r.d&&!r.b&&!r.p&&t!==2?(r.o=r.a,r.a=void 0):!r.o&&!r.q&&!r.d&&(r.b||r.p)?(r.o=r.a,r.d=r.b,r.q=r.p,r.a=r.b=r.p=void 0):r.o&&r.dType==="kv"&&S.patterns.match_("d-oxidation$",r.d||"")?r.dType="oxidation":r.o&&r.dType==="kv"&&!r.q&&(r.dType=void 0),a.push({type_:"chemfive",a:S.go(r.a,"a"),b:S.go(r.b,"bd"),p:S.go(r.p,"pq"),o:S.go(r.o,"o"),q:S.go(r.q,"pq"),d:S.go(r.d,r.dType==="oxidation"?"oxidation":"bd"),dType:r.dType}));else{var n;r.rdt==="M"?n=S.go(r.rd,"tex-math"):r.rdt==="T"?n=[{type_:"text",p1:r.rd||""}]:n=S.go(r.rd);var s;r.rqt==="M"?s=S.go(r.rq,"tex-math"):r.rqt==="T"?s=[{type_:"text",p1:r.rq||""}]:s=S.go(r.rq),a={type_:"arrow",r:r.r,rd:n,rq:s}}for(var l in r)l!=="parenthesisLevel"&&l!=="beginsWithBond"&&delete r[l];return a},"oxidation-output":function(r,e){var t=["{"];return S.concatArray(t,S.go(e,"oxidation")),t.push("}"),t},"frac-output":function(r,e){return{type_:"frac-ce",p1:S.go(e[0]),p2:S.go(e[1])}},"overset-output":function(r,e){return{type_:"overset",p1:S.go(e[0]),p2:S.go(e[1])}},"underset-output":function(r,e){return{type_:"underset",p1:S.go(e[0]),p2:S.go(e[1])}},"underbrace-output":function(r,e){return{type_:"underbrace",p1:S.go(e[0]),p2:S.go(e[1])}},"color-output":function(r,e){return{type_:"color",color1:e[0],color2:S.go(e[1])}},"r=":function(r,e){r.r=e},"rdt=":function(r,e){r.rdt=e},"rd=":function(r,e){r.rd=e},"rqt=":function(r,e){r.rqt=e},"rq=":function(r,e){r.rq=e},operator:function(r,e,t){return{type_:"operator",kind_:t||e}}}},a:{transitions:S.createTransitions({empty:{"*":{}},"1/2$":{0:{action_:"1/2"}},else:{0:{nextState:"1",revisit:!0}},"$(...)$":{"*":{action_:"tex-math tight",nextState:"1"}},",":{"*":{action_:{type_:"insert",option:"commaDecimal"}}},else2:{"*":{action_:"copy"}}}),actions:{}},o:{transitions:S.createTransitions({empty:{"*":{}},"1/2$":{0:{action_:"1/2"}},else:{0:{nextState:"1",revisit:!0}},letters:{"*":{action_:"rm"}},"\\ca":{"*":{action_:{type_:"insert",option:"circa"}}},"\\x{}{}|\\x{}|\\x":{"*":{action_:"copy"}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},"{(...)}":{"*":{action_:"{text}"}},else2:{"*":{action_:"copy"}}}),actions:{}},text:{transitions:S.createTransitions({empty:{"*":{action_:"output"}},"{...}":{"*":{action_:"text="}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},"\\greek":{"*":{action_:["output","rm"]}},"\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:["output","copy"]}},else:{"*":{action_:"text="}}}),actions:{output:function(r){if(r.text_){var e={type_:"text",p1:r.text_};for(var t in r)delete r[t];return e}}}},pq:{transitions:S.createTransitions({empty:{"*":{}},"state of aggregation $":{"*":{action_:"state of aggregation"}},i$:{0:{nextState:"!f",revisit:!0}},"(KV letters),":{0:{action_:"rm",nextState:"0"}},formula$:{0:{nextState:"f",revisit:!0}},"1/2$":{0:{action_:"1/2"}},else:{0:{nextState:"!f",revisit:!0}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},"{(...)}":{"*":{action_:"text"}},"a-z":{f:{action_:"tex-math"}},letters:{"*":{action_:"rm"}},"-9.,9":{"*":{action_:"9,9"}},",":{"*":{action_:{type_:"insert+p1",option:"comma enumeration S"}}},"\\color{(...)}{(...)}1|\\color(...){(...)}2":{"*":{action_:"color-output"}},"\\color{(...)}0":{"*":{action_:"color0-output"}},"\\ce{(...)}":{"*":{action_:"ce"}},"\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"copy"}},else2:{"*":{action_:"copy"}}}),actions:{"state of aggregation":function(r,e){return{type_:"state of aggregation subscript",p1:S.go(e,"o")}},"color-output":function(r,e){return{type_:"color",color1:e[0],color2:S.go(e[1],"pq")}}}},bd:{transitions:S.createTransitions({empty:{"*":{}},x$:{0:{nextState:"!f",revisit:!0}},formula$:{0:{nextState:"f",revisit:!0}},else:{0:{nextState:"!f",revisit:!0}},"-9.,9 no missing 0":{"*":{action_:"9,9"}},".":{"*":{action_:{type_:"insert",option:"electron dot"}}},"a-z":{f:{action_:"tex-math"}},x:{"*":{action_:{type_:"insert",option:"KV x"}}},letters:{"*":{action_:"rm"}},"'":{"*":{action_:{type_:"insert",option:"prime"}}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},"{(...)}":{"*":{action_:"text"}},"\\color{(...)}{(...)}1|\\color(...){(...)}2":{"*":{action_:"color-output"}},"\\color{(...)}0":{"*":{action_:"color0-output"}},"\\ce{(...)}":{"*":{action_:"ce"}},"\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"copy"}},else2:{"*":{action_:"copy"}}}),actions:{"color-output":function(r,e){return{type_:"color",color1:e[0],color2:S.go(e[1],"bd")}}}},oxidation:{transitions:S.createTransitions({empty:{"*":{}},"roman numeral":{"*":{action_:"roman-numeral"}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},else:{"*":{action_:"copy"}}}),actions:{"roman-numeral":function(r,e){return{type_:"roman numeral",p1:e||""}}}},"tex-math":{transitions:S.createTransitions({empty:{"*":{action_:"output"}},"\\ce{(...)}":{"*":{action_:["output","ce"]}},"{...}|\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"o="}},else:{"*":{action_:"o="}}}),actions:{output:function(r){if(r.o){var e={type_:"tex-math",p1:r.o};for(var t in r)delete r[t];return e}}}},"tex-math tight":{transitions:S.createTransitions({empty:{"*":{action_:"output"}},"\\ce{(...)}":{"*":{action_:["output","ce"]}},"{...}|\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"o="}},"-|+":{"*":{action_:"tight operator"}},else:{"*":{action_:"o="}}}),actions:{"tight operator":function(r,e){r.o=(r.o||"")+"{"+e+"}"},output:function(r){if(r.o){var e={type_:"tex-math",p1:r.o};for(var t in r)delete r[t];return e}}}},"9,9":{transitions:S.createTransitions({empty:{"*":{}},",":{"*":{action_:"comma"}},else:{"*":{action_:"copy"}}}),actions:{comma:function(){return{type_:"commaDecimal"}}}},pu:{transitions:S.createTransitions({empty:{"*":{action_:"output"}},space$:{"*":{action_:["output","space"]}},"{[(|)]}":{"0|a":{action_:"copy"}},"(-)(9)^(-9)":{0:{action_:"number^",nextState:"a"}},"(-)(9.,9)(e)(99)":{0:{action_:"enumber",nextState:"a"}},space:{"0|a":{}},"pm-operator":{"0|a":{action_:{type_:"operator",option:"\\pm"},nextState:"0"}},operator:{"0|a":{action_:"copy",nextState:"0"}},"//":{d:{action_:"o=",nextState:"/"}},"/":{d:{action_:"o=",nextState:"/"}},"{...}|else":{"0|d":{action_:"d=",nextState:"d"},a:{action_:["space","d="],nextState:"d"},"/|q":{action_:"q=",nextState:"q"}}}),actions:{enumber:function(r,e){var t=[];return e[0]==="+-"||e[0]==="+/-"?t.push("\\pm "):e[0]&&t.push(e[0]),e[1]&&(S.concatArray(t,S.go(e[1],"pu-9,9")),e[2]&&(e[2].match(/[,.]/)?S.concatArray(t,S.go(e[2],"pu-9,9")):t.push(e[2])),e[3]=e[4]||e[3],e[3]&&(e[3]=e[3].trim(),e[3]==="e"||e[3].substr(0,1)==="*"?t.push({type_:"cdot"}):t.push({type_:"times"}))),e[3]&&t.push("10^{"+e[5]+"}"),t},"number^":function(r,e){var t=[];return e[0]==="+-"||e[0]==="+/-"?t.push("\\pm "):e[0]&&t.push(e[0]),S.concatArray(t,S.go(e[1],"pu-9,9")),t.push("^{"+e[2]+"}"),t},operator:function(r,e,t){return{type_:"operator",kind_:t||e}},space:function(){return{type_:"pu-space-1"}},output:function(r){var e,t=S.patterns.match_("{(...)}",r.d||"");t&&t.remainder===""&&(r.d=t.match_);var a=S.patterns.match_("{(...)}",r.q||"");if(a&&a.remainder===""&&(r.q=a.match_),r.d&&(r.d=r.d.replace(/\u00B0C|\^oC|\^{o}C/g,"{}^{\\circ}C"),r.d=r.d.replace(/\u00B0F|\^oF|\^{o}F/g,"{}^{\\circ}F")),r.q){r.q=r.q.replace(/\u00B0C|\^oC|\^{o}C/g,"{}^{\\circ}C"),r.q=r.q.replace(/\u00B0F|\^oF|\^{o}F/g,"{}^{\\circ}F");var n={d:S.go(r.d,"pu"),q:S.go(r.q,"pu")};r.o==="//"?e={type_:"pu-frac",p1:n.d,p2:n.q}:(e=n.d,n.d.length>1||n.q.length>1?e.push({type_:" / "}):e.push({type_:"/"}),S.concatArray(e,n.q))}else e=S.go(r.d,"pu-2");for(var s in r)delete r[s];return e}}},"pu-2":{transitions:S.createTransitions({empty:{"*":{action_:"output"}},"*":{"*":{action_:["output","cdot"],nextState:"0"}},"\\x":{"*":{action_:"rm="}},space:{"*":{action_:["output","space"],nextState:"0"}},"^{(...)}|^(-1)":{1:{action_:"^(-1)"}},"-9.,9":{0:{action_:"rm=",nextState:"0"},1:{action_:"^(-1)",nextState:"0"}},"{...}|else":{"*":{action_:"rm=",nextState:"1"}}}),actions:{cdot:function(){return{type_:"tight cdot"}},"^(-1)":function(r,e){r.rm+="^{"+e+"}"},space:function(){return{type_:"pu-space-2"}},output:function(r){var e=[];if(r.rm){var t=S.patterns.match_("{(...)}",r.rm||"");t&&t.remainder===""?e=S.go(t.match_,"pu"):e={type_:"rm",p1:r.rm}}for(var a in r)delete r[a];return e}}},"pu-9,9":{transitions:S.createTransitions({empty:{0:{action_:"output-0"},o:{action_:"output-o"}},",":{0:{action_:["output-0","comma"],nextState:"o"}},".":{0:{action_:["output-0","copy"],nextState:"o"}},else:{"*":{action_:"text="}}}),actions:{comma:function(){return{type_:"commaDecimal"}},"output-0":function(r){var e=[];if(r.text_=r.text_||"",r.text_.length>4){var t=r.text_.length%3;t===0&&(t=3);for(var a=r.text_.length-3;a>0;a-=3)e.push(r.text_.substr(a,3)),e.push({type_:"1000 separator"});e.push(r.text_.substr(0,t)),e.reverse()}else e.push(r.text_);for(var n in r)delete r[n];return e},"output-o":function(r){var e=[];if(r.text_=r.text_||"",r.text_.length>4){for(var t=r.text_.length-3,a=0;a":return"rightarrow";case"\u2192":return"rightarrow";case"\u27F6":return"rightarrow";case"<-":return"leftarrow";case"<->":return"leftrightarrow";case"<-->":return"rightleftarrows";case"<=>":return"rightleftharpoons";case"\u21CC":return"rightleftharpoons";case"<=>>":return"rightequilibrium";case"<<=>":return"leftequilibrium";default:throw["MhchemBugT","mhchem bug T. Please report."]}},_getBond:function(r){switch(r){case"-":return"{-}";case"1":return"{-}";case"=":return"{=}";case"2":return"{=}";case"#":return"{\\equiv}";case"3":return"{\\equiv}";case"~":return"{\\tripledash}";case"~-":return"{\\mathrlap{\\raisebox{-.1em}{$-$}}\\raisebox{.1em}{$\\tripledash$}}";case"~=":return"{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$\\tripledash$}}-}";case"~--":return"{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$\\tripledash$}}-}";case"-~-":return"{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$-$}}\\tripledash}";case"...":return"{{\\cdot}{\\cdot}{\\cdot}}";case"....":return"{{\\cdot}{\\cdot}{\\cdot}{\\cdot}}";case"->":return"{\\rightarrow}";case"<-":return"{\\leftarrow}";case"<":return"{<}";case">":return"{>}";default:throw["MhchemBugT","mhchem bug T. Please report."]}},_getOperator:function(r){switch(r){case"+":return" {}+{} ";case"-":return" {}-{} ";case"=":return" {}={} ";case"<":return" {}<{} ";case">":return" {}>{} ";case"<<":return" {}\\ll{} ";case">>":return" {}\\gg{} ";case"\\pm":return" {}\\pm{} ";case"\\approx":return" {}\\approx{} ";case"$\\approx$":return" {}\\approx{} ";case"v":return" \\downarrow{} ";case"(v)":return" \\downarrow{} ";case"^":return" \\uparrow{} ";case"(^)":return" \\uparrow{} ";default:throw["MhchemBugT","mhchem bug T. Please report."]}}};var wn=function(r){let e=r.data,t=e.expression,a=e.options,n=r.header;n.warnings=[],a.strict=="warn"&&(a.strict=(l,h)=>{n.warnings.push(`katex: LaTeX-incompatible input and strict mode is set to 'warn': ${h} [${l}]`)});let s=oe.renderToString(t,a);et({header:n,data:{output:s}})};Wt(wn);})();
diff --git a/internal/warpc/js/renderkatex.js b/internal/warpc/js/renderkatex.js
new file mode 100644
index 000000000..7c8ac25ee
--- /dev/null
+++ b/internal/warpc/js/renderkatex.js
@@ -0,0 +1,25 @@
+import { readInput, writeOutput } from './common';
+import katex from 'katex';
+import 'katex/contrib/mhchem/mhchem.js';
+
+const render = function (input) {
+ const data = input.data;
+ const expression = data.expression;
+ const options = data.options;
+ const header = input.header;
+ header.warnings = [];
+
+ if (options.strict == 'warn') {
+ // By default, KaTeX will write to console.warn, that's a little hard to handle.
+ options.strict = (errorCode, errorMsg) => {
+ header.warnings.push(
+ `katex: LaTeX-incompatible input and strict mode is set to 'warn': ${errorMsg} [${errorCode}]`,
+ );
+ };
+ }
+ // Any error thrown here will be caught by the common.js readInput function.
+ const output = katex.renderToString(expression, options);
+ writeOutput({ header: header, data: { output: output } });
+};
+
+readInput(render);
diff --git a/internal/warpc/katex.go b/internal/warpc/katex.go
new file mode 100644
index 000000000..75c20117f
--- /dev/null
+++ b/internal/warpc/katex.go
@@ -0,0 +1,76 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package warpc
+
+import (
+ _ "embed"
+)
+
+//go:embed wasm/renderkatex.wasm
+var katexWasm []byte
+
+// See https://katex.org/docs/options.html
+type KatexInput struct {
+ Expression string `json:"expression"`
+ Options KatexOptions `json:"options"`
+}
+
+// KatexOptions defines the options for the KaTeX rendering.
+// See https://katex.org/docs/options.html
+type KatexOptions struct {
+ // html, mathml (default), htmlAndMathml
+ Output string `json:"output"`
+
+ // If true, display math in display mode, false in inline mode.
+ DisplayMode bool `json:"displayMode"`
+
+ // Render \tags on the left side instead of the right.
+ Leqno bool `json:"leqno"`
+
+ // If true, render flush left with a 2em left margin.
+ Fleqn bool `json:"fleqn"`
+
+ // The color used for typesetting errors.
+ // A color string given in the format "#XXX" or "#XXXXXX"
+ ErrorColor string `json:"errorColor"`
+
+ // A collection of custom macros.
+ Macros map[string]string `json:"macros,omitempty"`
+
+ // Specifies a minimum thickness, in ems, for fraction lines.
+ MinRuleThickness float64 `json:"minRuleThickness"`
+
+ // If true, KaTeX will throw a ParseError when it encounters an unsupported command.
+ ThrowOnError bool `json:"throwOnError"`
+
+ // Controls how KaTeX handles LaTeX features that offer convenience but
+ // aren't officially supported, one of error (default), ignore, or warn.
+ //
+ // - error: Throws an error when convenient, unsupported LaTeX features
+ // are encountered.
+ // - ignore: Allows convenient, unsupported LaTeX features without any
+ // feedback.
+ // - warn: Emits a warning when convenient, unsupported LaTeX features are
+ // encountered.
+ //
+ // The "newLineInDisplayMode" error code, which flags the use of \\
+ // or \newline in display mode outside an array or tabular environment, is
+ // intentionally designed not to throw an error, despite this behavior
+ // being questionable.
+ Strict string `json:"strict"`
+}
+
+type KatexOutput struct {
+ Output string `json:"output"`
+}
diff --git a/internal/warpc/warpc.go b/internal/warpc/warpc.go
new file mode 100644
index 000000000..e21fefa8a
--- /dev/null
+++ b/internal/warpc/warpc.go
@@ -0,0 +1,589 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package warpc
+
+import (
+ "bytes"
+ "context"
+ _ "embed"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/gohugoio/hugo/common/hugio"
+ "golang.org/x/sync/errgroup"
+
+ "github.com/tetratelabs/wazero"
+ "github.com/tetratelabs/wazero/api"
+ "github.com/tetratelabs/wazero/experimental"
+ "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
+)
+
+const currentVersion = 1
+
+//go:embed wasm/quickjs.wasm
+var quickjsWasm []byte
+
+// Header is in both the request and response.
+type Header struct {
+ // Major version of the protocol.
+ Version uint16 `json:"version"`
+
+ // Unique ID for the request.
+ // Note that this only needs to be unique within the current request set time window.
+ ID uint32 `json:"id"`
+
+ // Set in the response if there was an error.
+ Err string `json:"err"`
+
+ // Warnings is a list of warnings that may be returned in the response.
+ Warnings []string `json:"warnings,omitempty"`
+}
+
+type Message[T any] struct {
+ Header Header `json:"header"`
+ Data T `json:"data"`
+}
+
+func (m Message[T]) GetID() uint32 {
+ return m.Header.ID
+}
+
+type Dispatcher[Q, R any] interface {
+ Execute(ctx context.Context, q Message[Q]) (Message[R], error)
+ Close() error
+}
+
+func (p *dispatcherPool[Q, R]) getDispatcher() *dispatcher[Q, R] {
+ i := int(p.counter.Add(1)) % len(p.dispatchers)
+ return p.dispatchers[i]
+}
+
+func (p *dispatcherPool[Q, R]) Close() error {
+ return p.close()
+}
+
+type dispatcher[Q, R any] struct {
+ zero Message[R]
+
+ mu sync.RWMutex
+ encMu sync.Mutex
+
+ pending map[uint32]*call[Q, R]
+
+ inOut *inOut
+
+ shutdown bool
+ closing bool
+}
+
+type inOut struct {
+ sync.Mutex
+ stdin hugio.ReadWriteCloser
+ stdout hugio.ReadWriteCloser
+ dec *json.Decoder
+ enc *json.Encoder
+}
+
+var ErrShutdown = fmt.Errorf("dispatcher is shutting down")
+
+var timerPool = sync.Pool{}
+
+func getTimer(d time.Duration) *time.Timer {
+ if v := timerPool.Get(); v != nil {
+ timer := v.(*time.Timer)
+ timer.Reset(d)
+ return timer
+ }
+ return time.NewTimer(d)
+}
+
+func putTimer(t *time.Timer) {
+ if !t.Stop() {
+ select {
+ case <-t.C:
+ default:
+ }
+ }
+ timerPool.Put(t)
+}
+
+// Execute sends a request to the dispatcher and waits for the response.
+func (p *dispatcherPool[Q, R]) Execute(ctx context.Context, q Message[Q]) (Message[R], error) {
+ d := p.getDispatcher()
+ if q.GetID() == 0 {
+ return d.zero, errors.New("ID must not be 0 (note that this must be unique within the current request set time window)")
+ }
+
+ call, err := d.newCall(q)
+ if err != nil {
+ return d.zero, err
+ }
+
+ if err := d.send(call); err != nil {
+ return d.zero, err
+ }
+
+ timer := getTimer(30 * time.Second)
+ defer putTimer(timer)
+
+ select {
+ case call = <-call.donec:
+ case <-p.donec:
+ return d.zero, p.Err()
+ case <-ctx.Done():
+ return d.zero, ctx.Err()
+ case <-timer.C:
+ return d.zero, errors.New("timeout")
+ }
+
+ if call.err != nil {
+ return d.zero, call.err
+ }
+
+ resp, err := call.response, p.Err()
+
+ if err == nil && resp.Header.Err != "" {
+ err = errors.New(resp.Header.Err)
+ }
+ return resp, err
+}
+
+func (d *dispatcher[Q, R]) newCall(q Message[Q]) (*call[Q, R], error) {
+ call := &call[Q, R]{
+ donec: make(chan *call[Q, R], 1),
+ request: q,
+ }
+
+ if d.shutdown || d.closing {
+ call.err = ErrShutdown
+ call.done()
+ return call, nil
+ }
+
+ d.mu.Lock()
+ d.pending[q.GetID()] = call
+ d.mu.Unlock()
+
+ return call, nil
+}
+
+func (d *dispatcher[Q, R]) send(call *call[Q, R]) error {
+ d.mu.RLock()
+ if d.closing || d.shutdown {
+ d.mu.RUnlock()
+ return ErrShutdown
+ }
+ d.mu.RUnlock()
+
+ d.encMu.Lock()
+ defer d.encMu.Unlock()
+ err := d.inOut.enc.Encode(call.request)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (d *dispatcher[Q, R]) input() {
+ var inputErr error
+
+ for d.inOut.dec.More() {
+ var r Message[R]
+ if err := d.inOut.dec.Decode(&r); err != nil {
+ inputErr = fmt.Errorf("decoding response: %w", err)
+ break
+ }
+
+ d.mu.Lock()
+ call, found := d.pending[r.GetID()]
+ if !found {
+ d.mu.Unlock()
+ panic(fmt.Errorf("call with ID %d not found", r.GetID()))
+ }
+ delete(d.pending, r.GetID())
+ d.mu.Unlock()
+ call.response = r
+ call.done()
+ }
+
+ // Terminate pending calls.
+ d.shutdown = true
+ if inputErr != nil {
+ isEOF := inputErr == io.EOF || strings.Contains(inputErr.Error(), "already closed")
+ if isEOF {
+ if d.closing {
+ inputErr = ErrShutdown
+ } else {
+ inputErr = io.ErrUnexpectedEOF
+ }
+ }
+ }
+
+ d.mu.Lock()
+ defer d.mu.Unlock()
+ for _, call := range d.pending {
+ call.err = inputErr
+ call.done()
+ }
+}
+
+type call[Q, R any] struct {
+ request Message[Q]
+ response Message[R]
+ err error
+ donec chan *call[Q, R]
+}
+
+func (call *call[Q, R]) done() {
+ select {
+ case call.donec <- call:
+ default:
+ }
+}
+
+// Binary represents a WebAssembly binary.
+type Binary struct {
+ // The name of the binary.
+ // For quickjs, this must match the instance import name, "javy_quickjs_provider_v2".
+ // For the main module, we only use this for caching.
+ Name string
+
+ // THe wasm binary.
+ Data []byte
+}
+
+type Options struct {
+ Ctx context.Context
+
+ Infof func(format string, v ...any)
+
+ Warnf func(format string, v ...any)
+
+ // E.g. quickjs wasm. May be omitted if not needed.
+ Runtime Binary
+
+ // The main module to instantiate.
+ Main Binary
+
+ CompilationCacheDir string
+ PoolSize int
+
+ // Memory limit in MiB.
+ Memory int
+}
+
+type CompileModuleContext struct {
+ Opts Options
+ Runtime wazero.Runtime
+}
+
+type CompiledModule struct {
+ // Runtime (e.g. QuickJS) may be nil if not needed (e.g. embedded in Module).
+ Runtime wazero.CompiledModule
+
+ // If Runtime is not nil, this should be the name of the instance.
+ RuntimeName string
+
+ // The main module to instantiate.
+ // This will be insantiated multiple times in a pool,
+ // so it does not need a name.
+ Module wazero.CompiledModule
+}
+
+// Start creates a new dispatcher pool.
+func Start[Q, R any](opts Options) (Dispatcher[Q, R], error) {
+ if opts.Main.Data == nil {
+ return nil, errors.New("Main.Data must be set")
+ }
+ if opts.Main.Name == "" {
+ return nil, errors.New("Main.Name must be set")
+ }
+
+ if opts.Runtime.Data != nil && opts.Runtime.Name == "" {
+ return nil, errors.New("Runtime.Name must be set")
+ }
+
+ if opts.PoolSize == 0 {
+ opts.PoolSize = 1
+ }
+
+ return newDispatcher[Q, R](opts)
+}
+
+type dispatcherPool[Q, R any] struct {
+ counter atomic.Uint32
+ dispatchers []*dispatcher[Q, R]
+ close func() error
+ opts Options
+
+ errc chan error
+ donec chan struct{}
+}
+
+func (p *dispatcherPool[Q, R]) SendIfErr(err error) {
+ if err != nil {
+ p.errc <- err
+ }
+}
+
+func (p *dispatcherPool[Q, R]) Err() error {
+ select {
+ case err := <-p.errc:
+ return err
+ default:
+ return nil
+ }
+}
+
+func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) {
+ if opts.Ctx == nil {
+ opts.Ctx = context.Background()
+ }
+
+ if opts.Infof == nil {
+ opts.Infof = func(format string, v ...any) {
+ // noop
+ }
+ }
+ if opts.Warnf == nil {
+ opts.Warnf = func(format string, v ...any) {
+ // noop
+ }
+ }
+
+ if opts.Memory <= 0 {
+ // 32 MiB
+ opts.Memory = 32
+ }
+
+ ctx := opts.Ctx
+
+ // Page size is 64KB.
+ numPages := opts.Memory * 1024 / 64
+ runtimeConfig := wazero.NewRuntimeConfig().WithMemoryLimitPages(uint32(numPages))
+
+ if opts.CompilationCacheDir != "" {
+ compilationCache, err := wazero.NewCompilationCacheWithDir(opts.CompilationCacheDir)
+ if err != nil {
+ return nil, err
+ }
+ runtimeConfig = runtimeConfig.WithCompilationCache(compilationCache)
+ }
+
+ // Create a new WebAssembly Runtime.
+ r := wazero.NewRuntimeWithConfig(opts.Ctx, runtimeConfig)
+
+ // Instantiate WASI, which implements system I/O such as console output.
+ if _, err := wasi_snapshot_preview1.Instantiate(ctx, r); err != nil {
+ return nil, err
+ }
+
+ inOuts := make([]*inOut, opts.PoolSize)
+ for i := range opts.PoolSize {
+ var stdin, stdout hugio.ReadWriteCloser
+
+ stdin = hugio.NewPipeReadWriteCloser()
+ stdout = hugio.NewPipeReadWriteCloser()
+
+ inOuts[i] = &inOut{
+ stdin: stdin,
+ stdout: stdout,
+ dec: json.NewDecoder(stdout),
+ enc: json.NewEncoder(stdin),
+ }
+ }
+
+ var (
+ runtimeModule wazero.CompiledModule
+ mainModule wazero.CompiledModule
+ err error
+ )
+
+ if opts.Runtime.Data != nil {
+ runtimeModule, err = r.CompileModule(ctx, opts.Runtime.Data)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ mainModule, err = r.CompileModule(ctx, opts.Main.Data)
+ if err != nil {
+ return nil, err
+ }
+
+ toErr := func(what string, errBuff bytes.Buffer, err error) error {
+ return fmt.Errorf("%s: %s: %w", what, errBuff.String(), err)
+ }
+
+ run := func() error {
+ g, ctx := errgroup.WithContext(ctx)
+ for _, c := range inOuts {
+ c := c
+ g.Go(func() error {
+ var errBuff bytes.Buffer
+ ctx := context.WithoutCancel(ctx)
+ configBase := wazero.NewModuleConfig().WithStderr(&errBuff).WithStdout(c.stdout).WithStdin(c.stdin).WithStartFunctions()
+ if opts.Runtime.Data != nil {
+ // This needs to be anonymous, it will be resolved in the import resolver below.
+ runtimeInstance, err := r.InstantiateModule(ctx, runtimeModule, configBase.WithName(""))
+ if err != nil {
+ return toErr("quickjs", errBuff, err)
+ }
+ ctx = experimental.WithImportResolver(ctx,
+ func(name string) api.Module {
+ if name == opts.Runtime.Name {
+ return runtimeInstance
+ }
+ return nil
+ },
+ )
+ }
+
+ mainInstance, err := r.InstantiateModule(ctx, mainModule, configBase.WithName(""))
+ if err != nil {
+ return toErr(opts.Main.Name, errBuff, err)
+ }
+ if _, err := mainInstance.ExportedFunction("_start").Call(ctx); err != nil {
+ return toErr(opts.Main.Name, errBuff, err)
+ }
+
+ // The console.log in the Javy/quickjs WebAssembly module will write to stderr.
+ // In non-error situations, write that to the provided infof logger.
+ if errBuff.Len() > 0 {
+ opts.Infof("%s", errBuff.String())
+ }
+
+ return nil
+ })
+ }
+ return g.Wait()
+ }
+
+ dp := &dispatcherPool[Q, R]{
+ dispatchers: make([]*dispatcher[Q, R], len(inOuts)),
+ opts: opts,
+
+ errc: make(chan error, 10),
+ donec: make(chan struct{}),
+ }
+
+ go func() {
+ // This will block until stdin is closed or it encounters an error.
+ err := run()
+ dp.SendIfErr(err)
+ close(dp.donec)
+ }()
+
+ for i := range inOuts {
+ d := &dispatcher[Q, R]{
+ pending: make(map[uint32]*call[Q, R]),
+ inOut: inOuts[i],
+ }
+ go d.input()
+ dp.dispatchers[i] = d
+ }
+
+ dp.close = func() error {
+ for _, d := range dp.dispatchers {
+ d.closing = true
+ if err := d.inOut.stdin.Close(); err != nil {
+ return err
+ }
+ if err := d.inOut.stdout.Close(); err != nil {
+ return err
+ }
+ }
+
+ // We need to wait for the WebAssembly instances to finish executing before we can close the runtime.
+ <-dp.donec
+
+ if err := r.Close(ctx); err != nil {
+ return err
+ }
+
+ // Return potential late compilation errors.
+ return dp.Err()
+ }
+
+ return dp, dp.Err()
+}
+
+type lazyDispatcher[Q, R any] struct {
+ opts Options
+
+ dispatcher Dispatcher[Q, R]
+ startOnce sync.Once
+ started bool
+ startErr error
+}
+
+func (d *lazyDispatcher[Q, R]) start() (Dispatcher[Q, R], error) {
+ d.startOnce.Do(func() {
+ start := time.Now()
+ d.dispatcher, d.startErr = Start[Q, R](d.opts)
+ d.started = true
+ d.opts.Infof("started dispatcher in %s", time.Since(start))
+ })
+ return d.dispatcher, d.startErr
+}
+
+// Dispatchers holds all the dispatchers for the warpc package.
+type Dispatchers struct {
+ katex *lazyDispatcher[KatexInput, KatexOutput]
+}
+
+func (d *Dispatchers) Katex() (Dispatcher[KatexInput, KatexOutput], error) {
+ return d.katex.start()
+}
+
+func (d *Dispatchers) Close() error {
+ var errs []error
+ if d.katex.started {
+ if err := d.katex.dispatcher.Close(); err != nil {
+ errs = append(errs, err)
+ }
+ }
+ if len(errs) == 0 {
+ return nil
+ }
+ return fmt.Errorf("%v", errs)
+}
+
+// AllDispatchers creates all the dispatchers for the warpc package.
+// Note that the individual dispatchers are started lazily.
+// Remember to call Close on the returned Dispatchers when done.
+func AllDispatchers(katexOpts Options) *Dispatchers {
+ if katexOpts.Runtime.Data == nil {
+ katexOpts.Runtime = Binary{Name: "javy_quickjs_provider_v2", Data: quickjsWasm}
+ }
+ if katexOpts.Main.Data == nil {
+ katexOpts.Main = Binary{Name: "renderkatex", Data: katexWasm}
+ }
+
+ if katexOpts.Infof == nil {
+ katexOpts.Infof = func(format string, v ...any) {
+ // noop
+ }
+ }
+
+ return &Dispatchers{
+ katex: &lazyDispatcher[KatexInput, KatexOutput]{opts: katexOpts},
+ }
+}
diff --git a/internal/warpc/warpc_test.go b/internal/warpc/warpc_test.go
new file mode 100644
index 000000000..2ee4c3de5
--- /dev/null
+++ b/internal/warpc/warpc_test.go
@@ -0,0 +1,475 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package warpc
+
+import (
+ "context"
+ _ "embed"
+ "fmt"
+ "sync"
+ "sync/atomic"
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+)
+
+//go:embed wasm/greet.wasm
+var greetWasm []byte
+
+type person struct {
+ Name string `json:"name"`
+}
+
+func TestKatex(t *testing.T) {
+ c := qt.New(t)
+
+ opts := Options{
+ PoolSize: 8,
+ Runtime: quickjsBinary,
+ Main: katexBinary,
+ }
+
+ d, err := Start[KatexInput, KatexOutput](opts)
+ c.Assert(err, qt.IsNil)
+
+ defer d.Close()
+
+ runExpression := func(c *qt.C, id uint32, expression string) (Message[KatexOutput], error) {
+ c.Helper()
+
+ ctx := context.Background()
+
+ input := KatexInput{
+ Expression: expression,
+ Options: KatexOptions{
+ Output: "html",
+ DisplayMode: true,
+ ThrowOnError: true,
+ },
+ }
+
+ message := Message[KatexInput]{
+ Header: Header{
+ Version: currentVersion,
+ ID: uint32(id),
+ },
+ Data: input,
+ }
+
+ return d.Execute(ctx, message)
+ }
+
+ c.Run("Simple", func(c *qt.C) {
+ id := uint32(32)
+ result, err := runExpression(c, id, "c = \\pm\\sqrt{a^2 + b^2}")
+ c.Assert(err, qt.IsNil)
+ c.Assert(result.GetID(), qt.Equals, id)
+ })
+
+ c.Run("Chemistry", func(c *qt.C) {
+ id := uint32(32)
+ result, err := runExpression(c, id, "C_p[\\ce{H2O(l)}] = \\pu{75.3 J // mol K}")
+ c.Assert(err, qt.IsNil)
+ c.Assert(result.GetID(), qt.Equals, id)
+ })
+
+ c.Run("Invalid expression", func(c *qt.C) {
+ id := uint32(32)
+ result, err := runExpression(c, id, "c & \\foo\\")
+ c.Assert(err, qt.IsNotNil)
+ c.Assert(result.GetID(), qt.Equals, id)
+ })
+}
+
+func TestGreet(t *testing.T) {
+ c := qt.New(t)
+ opts := Options{
+ PoolSize: 1,
+ Runtime: quickjsBinary,
+ Main: greetBinary,
+ Infof: t.Logf,
+ }
+
+ for range 2 {
+ func() {
+ d, err := Start[person, greeting](opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer func() {
+ c.Assert(d.Close(), qt.IsNil)
+ }()
+
+ ctx := context.Background()
+
+ inputMessage := Message[person]{
+ Header: Header{
+ Version: currentVersion,
+ },
+ Data: person{
+ Name: "Person",
+ },
+ }
+
+ for j := range 20 {
+ inputMessage.Header.ID = uint32(j + 1)
+ g, err := d.Execute(ctx, inputMessage)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if g.Data.Greeting != "Hello Person!" {
+ t.Fatalf("got: %v", g)
+ }
+ if g.GetID() != inputMessage.GetID() {
+ t.Fatalf("%d vs %d", g.GetID(), inputMessage.GetID())
+ }
+ }
+ }()
+ }
+}
+
+func TestGreetParallel(t *testing.T) {
+ c := qt.New(t)
+
+ opts := Options{
+ Runtime: quickjsBinary,
+ Main: greetBinary,
+ PoolSize: 4,
+ }
+ d, err := Start[person, greeting](opts)
+ c.Assert(err, qt.IsNil)
+ defer func() {
+ c.Assert(d.Close(), qt.IsNil)
+ }()
+
+ var wg sync.WaitGroup
+
+ for i := 1; i <= 10; i++ {
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+
+ ctx := context.Background()
+
+ for j := range 5 {
+ base := i * 100
+ id := uint32(base + j)
+
+ inputPerson := person{
+ Name: fmt.Sprintf("Person %d", id),
+ }
+ inputMessage := Message[person]{
+ Header: Header{
+ Version: currentVersion,
+ ID: id,
+ },
+ Data: inputPerson,
+ }
+ g, err := d.Execute(ctx, inputMessage)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ c.Assert(g.Data.Greeting, qt.Equals, fmt.Sprintf("Hello Person %d!", id))
+ c.Assert(g.GetID(), qt.Equals, inputMessage.GetID())
+
+ }
+ }(i)
+
+ }
+
+ wg.Wait()
+}
+
+func TestKatexParallel(t *testing.T) {
+ c := qt.New(t)
+
+ opts := Options{
+ Runtime: quickjsBinary,
+ Main: katexBinary,
+ PoolSize: 6,
+ }
+ d, err := Start[KatexInput, KatexOutput](opts)
+ c.Assert(err, qt.IsNil)
+ defer func() {
+ c.Assert(d.Close(), qt.IsNil)
+ }()
+
+ var wg sync.WaitGroup
+
+ for i := 1; i <= 10; i++ {
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+
+ ctx := context.Background()
+
+ for j := range 1 {
+ base := i * 100
+ id := uint32(base + j)
+
+ input := katexInputTemplate
+ inputMessage := Message[KatexInput]{
+ Header: Header{
+ Version: currentVersion,
+ ID: id,
+ },
+ Data: input,
+ }
+
+ result, err := d.Execute(ctx, inputMessage)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ if result.GetID() != inputMessage.GetID() {
+ t.Errorf("%d vs %d", result.GetID(), inputMessage.GetID())
+ return
+ }
+ }
+ }(i)
+
+ }
+
+ wg.Wait()
+}
+
+func BenchmarkExecuteKatex(b *testing.B) {
+ opts := Options{
+ Runtime: quickjsBinary,
+ Main: katexBinary,
+ }
+ d, err := Start[KatexInput, KatexOutput](opts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ defer d.Close()
+
+ ctx := context.Background()
+
+ input := katexInputTemplate
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ message := Message[KatexInput]{
+ Header: Header{
+ Version: currentVersion,
+ ID: uint32(i + 1),
+ },
+ Data: input,
+ }
+
+ result, err := d.Execute(ctx, message)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ if result.GetID() != message.GetID() {
+ b.Fatalf("%d vs %d", result.GetID(), message.GetID())
+ }
+
+ }
+}
+
+func BenchmarkKatexStartStop(b *testing.B) {
+ optsTemplate := Options{
+ Runtime: quickjsBinary,
+ Main: katexBinary,
+ CompilationCacheDir: b.TempDir(),
+ }
+
+ runBench := func(b *testing.B, opts Options) {
+ for i := 0; i < b.N; i++ {
+ d, err := Start[KatexInput, KatexOutput](opts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ if err := d.Close(); err != nil {
+ b.Fatal(err)
+ }
+ }
+ }
+
+ for _, poolSize := range []int{1, 8, 16} {
+
+ name := fmt.Sprintf("PoolSize%d", poolSize)
+
+ b.Run(name, func(b *testing.B) {
+ opts := optsTemplate
+ opts.PoolSize = poolSize
+ runBench(b, opts)
+ })
+
+ }
+}
+
+var katexInputTemplate = KatexInput{
+ Expression: "c = \\pm\\sqrt{a^2 + b^2}",
+ Options: KatexOptions{Output: "html", DisplayMode: true},
+}
+
+func BenchmarkExecuteKatexPara(b *testing.B) {
+ optsTemplate := Options{
+ Runtime: quickjsBinary,
+ Main: katexBinary,
+ }
+
+ runBench := func(b *testing.B, opts Options) {
+ d, err := Start[KatexInput, KatexOutput](opts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ defer d.Close()
+
+ ctx := context.Background()
+
+ b.ResetTimer()
+
+ var id atomic.Uint32
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ message := Message[KatexInput]{
+ Header: Header{
+ Version: currentVersion,
+ ID: id.Add(1),
+ },
+ Data: katexInputTemplate,
+ }
+
+ result, err := d.Execute(ctx, message)
+ if err != nil {
+ b.Fatal(err)
+ }
+ if result.GetID() != message.GetID() {
+ b.Fatalf("%d vs %d", result.GetID(), message.GetID())
+ }
+ }
+ })
+ }
+
+ for _, poolSize := range []int{1, 8, 16} {
+ name := fmt.Sprintf("PoolSize%d", poolSize)
+
+ b.Run(name, func(b *testing.B) {
+ opts := optsTemplate
+ opts.PoolSize = poolSize
+ runBench(b, opts)
+ })
+ }
+}
+
+func BenchmarkExecuteGreet(b *testing.B) {
+ opts := Options{
+ Runtime: quickjsBinary,
+ Main: greetBinary,
+ }
+ d, err := Start[person, greeting](opts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ defer d.Close()
+
+ ctx := context.Background()
+
+ input := person{
+ Name: "Person",
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ message := Message[person]{
+ Header: Header{
+ Version: currentVersion,
+ ID: uint32(i + 1),
+ },
+ Data: input,
+ }
+ result, err := d.Execute(ctx, message)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ if result.GetID() != message.GetID() {
+ b.Fatalf("%d vs %d", result.GetID(), message.GetID())
+ }
+
+ }
+}
+
+func BenchmarkExecuteGreetPara(b *testing.B) {
+ opts := Options{
+ Runtime: quickjsBinary,
+ Main: greetBinary,
+ PoolSize: 8,
+ }
+
+ d, err := Start[person, greeting](opts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ defer d.Close()
+
+ ctx := context.Background()
+
+ inputTemplate := person{
+ Name: "Person",
+ }
+
+ b.ResetTimer()
+
+ var id atomic.Uint32
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ message := Message[person]{
+ Header: Header{
+ Version: currentVersion,
+ ID: id.Add(1),
+ },
+ Data: inputTemplate,
+ }
+
+ result, err := d.Execute(ctx, message)
+ if err != nil {
+ b.Fatal(err)
+ }
+ if result.GetID() != message.GetID() {
+ b.Fatalf("%d vs %d", result.GetID(), message.GetID())
+ }
+ }
+ })
+}
+
+type greeting struct {
+ Greeting string `json:"greeting"`
+}
+
+var (
+ greetBinary = Binary{
+ Name: "greet",
+ Data: greetWasm,
+ }
+
+ katexBinary = Binary{
+ Name: "renderkatex",
+ Data: katexWasm,
+ }
+
+ quickjsBinary = Binary{
+ Name: "javy_quickjs_provider_v2",
+ Data: quickjsWasm,
+ }
+)
diff --git a/internal/warpc/wasm/greet.wasm b/internal/warpc/wasm/greet.wasm
new file mode 100644
index 000000000..944199b40
Binary files /dev/null and b/internal/warpc/wasm/greet.wasm differ
diff --git a/internal/warpc/wasm/quickjs.wasm b/internal/warpc/wasm/quickjs.wasm
new file mode 100644
index 000000000..569c53a23
Binary files /dev/null and b/internal/warpc/wasm/quickjs.wasm differ
diff --git a/internal/warpc/wasm/renderkatex.wasm b/internal/warpc/wasm/renderkatex.wasm
new file mode 100644
index 000000000..b8b21c16b
Binary files /dev/null and b/internal/warpc/wasm/renderkatex.wasm differ
diff --git a/internal/warpc/watchtestscripts.sh b/internal/warpc/watchtestscripts.sh
new file mode 100755
index 000000000..fbc90b648
--- /dev/null
+++ b/internal/warpc/watchtestscripts.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+trap exit SIGINT
+
+while true; do find . -type f -name "*.js" | entr -pd ./build.sh; done
\ No newline at end of file
diff --git a/langs/i18n/i18n_test.go b/langs/i18n/i18n_test.go
index 8d34e069d..a23cee539 100644
--- a/langs/i18n/i18n_test.go
+++ b/langs/i18n/i18n_test.go
@@ -23,8 +23,6 @@ import (
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config/testconfig"
- "github.com/gohugoio/hugo/tpl/tplimpl"
-
"github.com/gohugoio/hugo/resources/page"
"github.com/spf13/afero"
@@ -472,7 +470,6 @@ func prepareTranslationProvider(t testing.TB, test i18nTest, cfg config.Provider
func prepareDeps(afs afero.Fs, cfg config.Provider) (*deps.Deps, *TranslationProvider) {
d := testconfig.GetTestDeps(afs, cfg)
translationProvider := NewTranslationProvider()
- d.TemplateProvider = tplimpl.DefaultTemplateProvider
d.TranslationProvider = translationProvider
d.Site = page.NewDummyHugoSite(d.Conf)
if err := d.Compile(nil); err != nil {
diff --git a/langs/i18n/translationProvider.go b/langs/i18n/translationProvider.go
index 9bbd5d9f6..9ede538d2 100644
--- a/langs/i18n/translationProvider.go
+++ b/langs/i18n/translationProvider.go
@@ -61,6 +61,7 @@ func (tp *TranslationProvider) NewResource(dst *deps.Deps) error {
hugofs.WalkwayConfig{
Fs: dst.BaseFs.I18n.Fs,
IgnoreFile: dst.SourceSpec.IgnoreFile,
+ PathParser: dst.SourceSpec.Cfg.PathParser(),
WalkFn: func(path string, info hugofs.FileMetaInfo) error {
if info.IsDir() {
return nil
diff --git a/langs/language_test.go b/langs/language_test.go
index 543f4a133..33240f3f4 100644
--- a/langs/language_test.go
+++ b/langs/language_test.go
@@ -29,13 +29,13 @@ func TestCollator(t *testing.T) {
coll := &Collator{c: collate.New(language.English, collate.Loose)}
- for i := 0; i < 10; i++ {
+ for range 10 {
wg.Add(1)
go func() {
coll.Lock()
defer coll.Unlock()
defer wg.Done()
- for j := 0; j < 10; j++ {
+ for range 10 {
k := coll.CompareStrings("abc", "def")
c.Assert(k, qt.Equals, -1)
}
@@ -48,7 +48,7 @@ func BenchmarkCollator(b *testing.B) {
s := []string{"foo", "bar", "éntre", "baz", "qux", "quux", "corge", "grault", "garply", "waldo", "fred", "plugh", "xyzzy", "thud"}
doWork := func(coll *Collator) {
- for i := 0; i < len(s); i++ {
+ for i := range s {
for j := i + 1; j < len(s); j++ {
_ = coll.CompareStrings(s[i], s[j])
}
diff --git a/lazy/init.go b/lazy/init.go
index 7b88a5351..bef3867a9 100644
--- a/lazy/init.go
+++ b/lazy/init.go
@@ -36,7 +36,7 @@ type Init struct {
prev *Init
children []*Init
- init onceMore
+ init OnceMore
out any
err error
f func(context.Context) (any, error)
diff --git a/lazy/init_test.go b/lazy/init_test.go
index 96a959494..94736fab8 100644
--- a/lazy/init_test.go
+++ b/lazy/init_test.go
@@ -79,7 +79,7 @@ func TestInit(t *testing.T) {
// Add some concurrency and randomness to verify thread safety and
// init order.
- for i := 0; i < 100; i++ {
+ for i := range 100 {
wg.Add(1)
go func(i int) {
defer wg.Done()
diff --git a/lazy/once.go b/lazy/once.go
index c6abcd884..dac689df3 100644
--- a/lazy/once.go
+++ b/lazy/once.go
@@ -18,19 +18,19 @@ import (
"sync/atomic"
)
-// onceMore is similar to sync.Once.
+// OnceMore is similar to sync.Once.
//
// Additional features are:
// * it can be reset, so the action can be repeated if needed
// * it has methods to check if it's done or in progress
-type onceMore struct {
+type OnceMore struct {
mu sync.Mutex
lock uint32
done uint32
}
-func (t *onceMore) Do(f func()) {
+func (t *OnceMore) Do(f func()) {
if atomic.LoadUint32(&t.done) == 1 {
return
}
@@ -53,15 +53,15 @@ func (t *onceMore) Do(f func()) {
f()
}
-func (t *onceMore) InProgress() bool {
+func (t *OnceMore) InProgress() bool {
return atomic.LoadUint32(&t.lock) == 1
}
-func (t *onceMore) Done() bool {
+func (t *OnceMore) Done() bool {
return atomic.LoadUint32(&t.done) == 1
}
-func (t *onceMore) ResetWithLock() *sync.Mutex {
+func (t *OnceMore) ResetWithLock() *sync.Mutex {
t.mu.Lock()
defer atomic.StoreUint32(&t.done, 0)
return &t.mu
diff --git a/livereload/gen/livereload-hugo-plugin.js b/livereload/gen/livereload-hugo-plugin.js
new file mode 100644
index 000000000..c4c6aa487
--- /dev/null
+++ b/livereload/gen/livereload-hugo-plugin.js
@@ -0,0 +1,34 @@
+/*
+Hugo adds a specific prefix, "__hugo_navigate", to the path in certain situations to signal
+navigation to another content page.
+*/
+function HugoReload() {}
+
+HugoReload.identifier = 'hugoReloader';
+HugoReload.version = '0.9';
+
+HugoReload.prototype.reload = function (path, options) {
+ var prefix = '__hugo_navigate';
+
+ if (path.lastIndexOf(prefix, 0) !== 0) {
+ return false;
+ }
+
+ path = path.substring(prefix.length);
+
+ var portChanged = options.overrideURL && options.overrideURL != window.location.port;
+
+ if (!portChanged && window.location.pathname === path) {
+ window.location.reload();
+ } else {
+ if (portChanged) {
+ window.location = location.protocol + '//' + location.hostname + ':' + options.overrideURL + path;
+ } else {
+ window.location.pathname = path;
+ }
+ }
+
+ return true;
+};
+
+LiveReload.addPlugin(HugoReload);
diff --git a/livereload/gen/main.go b/livereload/gen/main.go
new file mode 100644
index 000000000..d69ff9206
--- /dev/null
+++ b/livereload/gen/main.go
@@ -0,0 +1,61 @@
+//go:generate go run main.go
+package main
+
+import (
+ _ "embed"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "os"
+
+ "github.com/evanw/esbuild/pkg/api"
+)
+
+//go:embed livereload-hugo-plugin.js
+var livereloadHugoPluginJS string
+
+func main() {
+ // 4.0.2
+ // To upgrade to a new version, change to the commit hash of the version you want to upgrade to
+ // then run mage generate from the root.
+ const liveReloadCommit = "d803a41804d2d71e0814c4e9e3233e78991024d9"
+ liveReloadSourceURL := fmt.Sprintf("https://raw.githubusercontent.com/livereload/livereload-js/%s/dist/livereload.js", liveReloadCommit)
+
+ func() {
+ resp, err := http.Get(liveReloadSourceURL)
+ must(err)
+ defer resp.Body.Close()
+
+ b, err := io.ReadAll(resp.Body)
+ must(err)
+
+ // Write the unminified livereload.js file.
+ err = os.WriteFile("../livereload.js", b, 0o644)
+ must(err)
+
+ // Bundle and minify with ESBuild.
+ result := api.Build(api.BuildOptions{
+ Stdin: &api.StdinOptions{
+ Contents: string(b) + livereloadHugoPluginJS,
+ },
+ Outfile: "../livereload.min.js",
+ Bundle: true,
+ Target: api.ES2015,
+ Write: true,
+ MinifyWhitespace: true,
+ MinifyIdentifiers: true,
+ MinifySyntax: true,
+ })
+
+ if len(result.Errors) > 0 {
+ log.Fatal(result.Errors)
+ }
+ }()
+}
+
+func must(err error) {
+ if err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/livereload/livereload.go b/livereload/livereload.go
index f24b42f37..0d24ada98 100644
--- a/livereload/livereload.go
+++ b/livereload/livereload.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Hugo Authors. All rights reserved.
+// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -50,6 +50,7 @@ import (
)
// Prefix to signal to LiveReload that we need to navigate to another path.
+// Do not change this.
const hugoNavigatePrefix = "__hugo_navigate"
var upgrader = &websocket.Upgrader{
@@ -144,48 +145,8 @@ func ServeJS(w http.ResponseWriter, r *http.Request) {
}
func liveReloadJS() []byte {
- return []byte(livereloadJS + hugoLiveReloadPlugin)
+ return []byte(livereloadJS)
}
-var (
- // This is a patched version, see https://github.com/livereload/livereload-js/pull/84
- //go:embed livereload.js
- livereloadJS string
- hugoLiveReloadPlugin = fmt.Sprintf(`
-/*
-Hugo adds a specific prefix, "__hugo_navigate", to the path in certain situations to signal
-navigation to another content page.
-*/
-
-function HugoReload() {}
-
-HugoReload.identifier = 'hugoReloader';
-HugoReload.version = '0.9';
-
-HugoReload.prototype.reload = function(path, options) {
- var prefix = %q;
-
- if (path.lastIndexOf(prefix, 0) !== 0) {
- return false
- }
-
- path = path.substring(prefix.length);
-
- var portChanged = options.overrideURL && options.overrideURL != window.location.port
-
- if (!portChanged && window.location.pathname === path) {
- window.location.reload();
- } else {
- if (portChanged) {
- window.location = location.protocol + "//" + location.hostname + ":" + options.overrideURL + path;
- } else {
- window.location.pathname = path;
- }
- }
-
- return true;
-};
-
-LiveReload.addPlugin(HugoReload)
-`, hugoNavigatePrefix)
-)
+//go:embed livereload.min.js
+var livereloadJS string
diff --git a/livereload/livereload.js b/livereload/livereload.js
index 1f4aa0fbc..f7b1e3884 100644
--- a/livereload/livereload.js
+++ b/livereload/livereload.js
@@ -1 +1,3795 @@
-!function(){return function e(t,o,n){function r(s,c){if(!o[s]){if(!t[s]){var a="function"==typeof require&&require;if(!c&&a)return a(s,!0);if(i)return i(s,!0);var l=new Error("Cannot find module '"+s+"'");throw l.code="MODULE_NOT_FOUND",l}var h=o[s]={exports:{}};t[s][0].call(h.exports,function(e){return r(t[s][1][e]||e)},h,h.exports,e,t,o,n)}return o[s].exports}for(var i="function"==typeof require&&require,s=0;sh;)if((c=a[h++])!=c)return!0}else for(;l>h;h++)if((e||h in a)&&a[h]===o)return e||h||0;return!e&&-1}}},{"./_to-absolute-index":38,"./_to-iobject":40,"./_to-length":41}],5:[function(e,t,o){var n={}.toString;t.exports=function(e){return n.call(e).slice(8,-1)}},{}],6:[function(e,t,o){var n=t.exports={version:"2.6.5"};"number"==typeof __e&&(__e=n)},{}],7:[function(e,t,o){var n=e("./_a-function");t.exports=function(e,t,o){if(n(e),void 0===t)return e;switch(o){case 1:return function(o){return e.call(t,o)};case 2:return function(o,n){return e.call(t,o,n)};case 3:return function(o,n,r){return e.call(t,o,n,r)}}return function(){return e.apply(t,arguments)}}},{"./_a-function":1}],8:[function(e,t,o){t.exports=function(e){if(null==e)throw TypeError("Can't call method on "+e);return e}},{}],9:[function(e,t,o){t.exports=!e("./_fails")(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},{"./_fails":13}],10:[function(e,t,o){var n=e("./_is-object"),r=e("./_global").document,i=n(r)&&n(r.createElement);t.exports=function(e){return i?r.createElement(e):{}}},{"./_global":15,"./_is-object":21}],11:[function(e,t,o){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},{}],12:[function(e,t,o){var n=e("./_global"),r=e("./_core"),i=e("./_hide"),s=e("./_redefine"),c=e("./_ctx"),a=function(e,t,o){var l,h,u,d,f=e&a.F,p=e&a.G,_=e&a.S,m=e&a.P,g=e&a.B,y=p?n:_?n[t]||(n[t]={}):(n[t]||{}).prototype,v=p?r:r[t]||(r[t]={}),w=v.prototype||(v.prototype={});for(l in p&&(o=t),o)u=((h=!f&&y&&void 0!==y[l])?y:o)[l],d=g&&h?c(u,n):m&&"function"==typeof u?c(Function.call,u):u,y&&s(y,l,u,e&a.U),v[l]!=u&&i(v,l,d),m&&w[l]!=u&&(w[l]=u)};n.core=r,a.F=1,a.G=2,a.S=4,a.P=8,a.B=16,a.W=32,a.U=64,a.R=128,t.exports=a},{"./_core":6,"./_ctx":7,"./_global":15,"./_hide":17,"./_redefine":34}],13:[function(e,t,o){t.exports=function(e){try{return!!e()}catch(e){return!0}}},{}],14:[function(e,t,o){t.exports=e("./_shared")("native-function-to-string",Function.toString)},{"./_shared":37}],15:[function(e,t,o){var n=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},{}],16:[function(e,t,o){var n={}.hasOwnProperty;t.exports=function(e,t){return n.call(e,t)}},{}],17:[function(e,t,o){var n=e("./_object-dp"),r=e("./_property-desc");t.exports=e("./_descriptors")?function(e,t,o){return n.f(e,t,r(1,o))}:function(e,t,o){return e[t]=o,e}},{"./_descriptors":9,"./_object-dp":28,"./_property-desc":33}],18:[function(e,t,o){var n=e("./_global").document;t.exports=n&&n.documentElement},{"./_global":15}],19:[function(e,t,o){t.exports=!e("./_descriptors")&&!e("./_fails")(function(){return 7!=Object.defineProperty(e("./_dom-create")("div"),"a",{get:function(){return 7}}).a})},{"./_descriptors":9,"./_dom-create":10,"./_fails":13}],20:[function(e,t,o){var n=e("./_cof");t.exports=Object("z").propertyIsEnumerable(0)?Object:function(e){return"String"==n(e)?e.split(""):Object(e)}},{"./_cof":5}],21:[function(e,t,o){t.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},{}],22:[function(e,t,o){"use strict";var n=e("./_object-create"),r=e("./_property-desc"),i=e("./_set-to-string-tag"),s={};e("./_hide")(s,e("./_wks")("iterator"),function(){return this}),t.exports=function(e,t,o){e.prototype=n(s,{next:r(1,o)}),i(e,t+" Iterator")}},{"./_hide":17,"./_object-create":27,"./_property-desc":33,"./_set-to-string-tag":35,"./_wks":45}],23:[function(e,t,o){"use strict";var n=e("./_library"),r=e("./_export"),i=e("./_redefine"),s=e("./_hide"),c=e("./_iterators"),a=e("./_iter-create"),l=e("./_set-to-string-tag"),h=e("./_object-gpo"),u=e("./_wks")("iterator"),d=!([].keys&&"next"in[].keys()),f=function(){return this};t.exports=function(e,t,o,p,_,m,g){a(o,t,p);var y,v,w,b=function(e){if(!d&&e in L)return L[e];switch(e){case"keys":case"values":return function(){return new o(this,e)}}return function(){return new o(this,e)}},S=t+" Iterator",R="values"==_,k=!1,L=e.prototype,x=L[u]||L["@@iterator"]||_&&L[_],j=x||b(_),C=_?R?b("entries"):j:void 0,O="Array"==t&&L.entries||x;if(O&&(w=h(O.call(new e)))!==Object.prototype&&w.next&&(l(w,S,!0),n||"function"==typeof w[u]||s(w,u,f)),R&&x&&"values"!==x.name&&(k=!0,j=function(){return x.call(this)}),n&&!g||!d&&!k&&L[u]||s(L,u,j),c[t]=j,c[S]=f,_)if(y={values:R?j:b("values"),keys:m?j:b("keys"),entries:C},g)for(v in y)v in L||i(L,v,y[v]);else r(r.P+r.F*(d||k),t,y);return y}},{"./_export":12,"./_hide":17,"./_iter-create":22,"./_iterators":25,"./_library":26,"./_object-gpo":30,"./_redefine":34,"./_set-to-string-tag":35,"./_wks":45}],24:[function(e,t,o){t.exports=function(e,t){return{value:t,done:!!e}}},{}],25:[function(e,t,o){t.exports={}},{}],26:[function(e,t,o){t.exports=!1},{}],27:[function(e,t,o){var n=e("./_an-object"),r=e("./_object-dps"),i=e("./_enum-bug-keys"),s=e("./_shared-key")("IE_PROTO"),c=function(){},a=function(){var t,o=e("./_dom-create")("iframe"),n=i.length;for(o.style.display="none",e("./_html").appendChild(o),o.src="javascript:",(t=o.contentWindow.document).open(),t.write(" -->
+
+## With Block
+
+
+
+## XSS
+
+
+
+
+## More
+
+This is a word.
+
+This is a word.
+
+This is a word.
+
+This is a word.
+
+This is a word.
+
+
+-- layouts/_default/single.html --
+{{ .Content }}
+`
+
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+ b.AssertFileContent("public/p1/index.html",
+ "! ",
+ "! ",
+ "! ",
+ "! script",
+ )
+ b.AssertLogContains("! WARN")
+
+ b = hugolib.Test(t, strings.ReplaceAll(files, "markup.goldmark.renderer.unsafe = false", "markup.goldmark.renderer.unsafe = true"), hugolib.TestOptWarn())
+ b.AssertFileContent("public/p1/index.html",
+ "! ",
+ "",
+ "",
+ )
+ b.AssertLogContains("! WARN")
+}
diff --git a/markup/goldmark/hugocontext/hugocontext.go b/markup/goldmark/hugocontext/hugocontext.go
index b9c548dac..7a556083c 100644
--- a/markup/goldmark/hugocontext/hugocontext.go
+++ b/markup/goldmark/hugocontext/hugocontext.go
@@ -16,20 +16,24 @@ package hugocontext
import (
"bytes"
"fmt"
+ "regexp"
"strconv"
"github.com/gohugoio/hugo/bufferpool"
+ "github.com/gohugoio/hugo/common/constants"
+ "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/markup/goldmark/internal/render"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
-func New() goldmark.Extender {
- return &hugoContextExtension{}
+func New(logger loggers.Logger) goldmark.Extender {
+ return &hugoContextExtension{logger: logger}
}
// Wrap wraps the given byte slice in a Hugo context that used to determine the correct Page
@@ -37,14 +41,19 @@ func New() goldmark.Extender {
func Wrap(b []byte, pid uint64) string {
buf := bufferpool.GetBuffer()
defer bufferpool.PutBuffer(buf)
- buf.Write(prefix)
+ buf.Write(hugoCtxPrefix)
buf.WriteString(" pid=")
buf.WriteString(strconv.FormatUint(pid, 10))
- buf.Write(endDelim)
+ buf.Write(hugoCtxEndDelim)
buf.WriteByte('\n')
buf.Write(b)
- buf.Write(prefix)
- buf.Write(closingDelimAndNewline)
+ // To make sure that we're able to parse it, make sure it ends with a newline.
+ if len(b) > 0 && b[len(b)-1] != '\n' {
+ buf.WriteByte('\n')
+ }
+ buf.Write(hugoCtxPrefix)
+ buf.Write(hugoCtxClosingDelim)
+ buf.WriteByte('\n')
return buf.String()
}
@@ -89,45 +98,147 @@ func (h *HugoContext) Kind() ast.NodeKind {
}
var (
- prefix = []byte("{{__hugo_ctx")
- endDelim = []byte("}}")
- closingDelimAndNewline = []byte("/}}\n")
+ hugoCtxPrefix = []byte("{{__hugo_ctx")
+ hugoCtxEndDelim = []byte("}}")
+ hugoCtxClosingDelim = []byte("/}}")
+ hugoCtxRe = regexp.MustCompile(`{{__hugo_ctx( pid=\d+)?/?}}\n?`)
)
var _ parser.InlineParser = (*hugoContextParser)(nil)
type hugoContextParser struct{}
-func (s *hugoContextParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
- line, _ := block.PeekLine()
- if !bytes.HasPrefix(line, prefix) {
+func (a *hugoContextParser) Trigger() []byte {
+ return []byte{'{'}
+}
+
+func (s *hugoContextParser) Parse(parent ast.Node, reader text.Reader, pc parser.Context) ast.Node {
+ line, _ := reader.PeekLine()
+ if !bytes.HasPrefix(line, hugoCtxPrefix) {
return nil
}
- end := bytes.Index(line, endDelim)
+ end := bytes.Index(line, hugoCtxEndDelim)
if end == -1 {
return nil
}
- block.Advance(end + len(endDelim) + 1) // +1 for the newline
+ reader.Advance(end + len(hugoCtxEndDelim) + 1) // +1 for the newline
if line[end-1] == '/' {
return &HugoContext{Closing: true}
}
- attrBytes := line[len(prefix)+1 : end]
+ attrBytes := line[len(hugoCtxPrefix)+1 : end]
h := &HugoContext{}
h.parseAttrs(attrBytes)
return h
}
-func (a *hugoContextParser) Trigger() []byte {
- return []byte{'{'}
+type hugoContextRenderer struct {
+ logger loggers.Logger
+ html.Config
}
-type hugoContextRenderer struct{}
+func (r *hugoContextRenderer) SetOption(name renderer.OptionName, value any) {
+ r.Config.SetOption(name, value)
+}
func (r *hugoContextRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(kindHugoContext, r.handleHugoContext)
+ reg.Register(ast.KindRawHTML, r.renderRawHTML)
+ reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)
+}
+
+func (r *hugoContextRenderer) stripHugoCtx(b []byte) ([]byte, bool) {
+ if !bytes.Contains(b, hugoCtxPrefix) {
+ return b, false
+ }
+ return hugoCtxRe.ReplaceAll(b, nil), true
+}
+
+func (r *hugoContextRenderer) logRawHTMLEmittedWarn(w util.BufWriter) {
+ r.logger.Warnidf(constants.WarnGoldmarkRawHTML, "Raw HTML omitted while rendering %q; see https://gohugo.io/getting-started/configuration-markup/#rendererunsafe", r.getPage(w))
+}
+
+func (r *hugoContextRenderer) getPage(w util.BufWriter) any {
+ var p any
+ ctx, ok := w.(*render.Context)
+ if ok {
+ p, _ = render.GetPageAndPageInner(ctx)
+ }
+ return p
+}
+
+func (r *hugoContextRenderer) isHTMLComment(b []byte) bool {
+ return len(b) > 4 && b[0] == '<' && b[1] == '!' && b[2] == '-' && b[3] == '-'
+}
+
+// HTML rendering based on Goldmark implementation.
+func (r *hugoContextRenderer) renderHTMLBlock(
+ w util.BufWriter, source []byte, node ast.Node, entering bool,
+) (ast.WalkStatus, error) {
+ n := node.(*ast.HTMLBlock)
+
+ if entering {
+ if r.Unsafe {
+ l := n.Lines().Len()
+ for i := range l {
+ line := n.Lines().At(i)
+ linev := line.Value(source)
+ var stripped bool
+ linev, stripped = r.stripHugoCtx(linev)
+ if stripped {
+ r.logger.Warnidf(constants.WarnRenderShortcodesInHTML, ".RenderShortcodes detected inside HTML block in %q; this may not be what you intended, see https://gohugo.io/methods/page/rendershortcodes/#limitations", r.getPage(w))
+ }
+ r.Writer.SecureWrite(w, linev)
+ }
+ } else {
+ l := n.Lines().At(0)
+ v := l.Value(source)
+ if !r.isHTMLComment(v) {
+ r.logRawHTMLEmittedWarn(w)
+ _, _ = w.WriteString("\n")
+ }
+ }
+ } else {
+ if n.HasClosure() {
+ if r.Unsafe {
+ closure := n.ClosureLine
+ r.Writer.SecureWrite(w, closure.Value(source))
+ } else {
+ l := n.Lines().At(0)
+ v := l.Value(source)
+ if !r.isHTMLComment(v) {
+ _, _ = w.WriteString("\n")
+ }
+ }
+ }
+ }
+ return ast.WalkContinue, nil
+}
+
+func (r *hugoContextRenderer) renderRawHTML(
+ w util.BufWriter, source []byte, node ast.Node, entering bool,
+) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkSkipChildren, nil
+ }
+ n := node.(*ast.RawHTML)
+ l := n.Segments.Len()
+ if r.Unsafe {
+ for i := range l {
+ segment := n.Segments.At(i)
+ _, _ = w.Write(segment.Value(source))
+ }
+ return ast.WalkSkipChildren, nil
+ }
+ segment := n.Segments.At(0)
+ v := segment.Value(source)
+ if !r.isHTMLComment(v) {
+ r.logRawHTMLEmittedWarn(w)
+ _, _ = w.WriteString("")
+ }
+ return ast.WalkSkipChildren, nil
}
func (r *hugoContextRenderer) handleHugoContext(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
@@ -148,18 +259,59 @@ func (r *hugoContextRenderer) handleHugoContext(w util.BufWriter, source []byte,
return ast.WalkContinue, nil
}
-type hugoContextExtension struct{}
+type hugoContextTransformer struct{}
+
+var _ parser.ASTTransformer = (*hugoContextTransformer)(nil)
+
+func (a *hugoContextTransformer) Transform(n *ast.Document, reader text.Reader, pc parser.Context) {
+ ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
+ s := ast.WalkContinue
+ if !entering || n.Kind() != kindHugoContext {
+ return s, nil
+ }
+
+ if p, ok := n.Parent().(*ast.Paragraph); ok {
+ if p.ChildCount() == 1 {
+ // Avoid empty paragraphs.
+ p.Parent().ReplaceChild(p.Parent(), p, n)
+ } else {
+ if t, ok := n.PreviousSibling().(*ast.Text); ok {
+ // Remove the newline produced by the Hugo context markers.
+ if t.SoftLineBreak() {
+ if t.Segment.Len() == 0 {
+ p.RemoveChild(p, t)
+ } else {
+ t.SetSoftLineBreak(false)
+ }
+ }
+ }
+ }
+ }
+
+ return s, nil
+ })
+}
+
+type hugoContextExtension struct {
+ logger loggers.Logger
+}
func (a *hugoContextExtension) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(
parser.WithInlineParsers(
util.Prioritized(&hugoContextParser{}, 50),
),
+ parser.WithASTTransformers(util.Prioritized(&hugoContextTransformer{}, 10)),
)
m.Renderer().AddOptions(
renderer.WithNodeRenderers(
- util.Prioritized(&hugoContextRenderer{}, 50),
+ util.Prioritized(&hugoContextRenderer{
+ logger: a.logger,
+ Config: html.Config{
+ Writer: html.DefaultWriter,
+ },
+ }, 50),
),
)
}
diff --git a/markup/goldmark/hugocontext/hugocontext_test.go b/markup/goldmark/hugocontext/hugocontext_test.go
index 4a6eb80f5..62769f4d0 100644
--- a/markup/goldmark/hugocontext/hugocontext_test.go
+++ b/markup/goldmark/hugocontext/hugocontext_test.go
@@ -24,7 +24,7 @@ func TestWrap(t *testing.T) {
b := []byte("test")
- c.Assert(Wrap(b, 42), qt.Equals, "{{__hugo_ctx pid=42}}\ntest{{__hugo_ctx/}}\n")
+ c.Assert(Wrap(b, 42), qt.Equals, "{{__hugo_ctx pid=42}}\ntest\n{{__hugo_ctx/}}\n")
}
func BenchmarkWrap(b *testing.B) {
diff --git a/markup/goldmark/internal/extensions/attributes/attributes.go b/markup/goldmark/internal/extensions/attributes/attributes.go
index feb3d915b..50ccb2ed4 100644
--- a/markup/goldmark/internal/extensions/attributes/attributes.go
+++ b/markup/goldmark/internal/extensions/attributes/attributes.go
@@ -1,8 +1,13 @@
package attributes
import (
+ "strings"
+
+ "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
+ "github.com/gohugoio/hugo/markup/goldmark/internal/render"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
+ east "github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
@@ -14,24 +19,29 @@ import (
var (
kindAttributesBlock = ast.NewNodeKind("AttributesBlock")
+ attrNameID = []byte("id")
- defaultParser = new(attrParser)
- defaultTransformer = new(transformer)
- attributes goldmark.Extender = new(attrExtension)
+ defaultParser = new(attrParser)
)
-func New() goldmark.Extender {
- return attributes
+func New(cfg goldmark_config.Parser) goldmark.Extender {
+ return &attrExtension{cfg: cfg}
}
-type attrExtension struct{}
+type attrExtension struct {
+ cfg goldmark_config.Parser
+}
func (a *attrExtension) Extend(m goldmark.Markdown) {
+ if a.cfg.Attribute.Block {
+ m.Parser().AddOptions(
+ parser.WithBlockParsers(
+ util.Prioritized(defaultParser, 100)),
+ )
+ }
m.Parser().AddOptions(
- parser.WithBlockParsers(
- util.Prioritized(defaultParser, 100)),
parser.WithASTTransformers(
- util.Prioritized(defaultTransformer, 100),
+ util.Prioritized(&transformer{cfg: a.cfg}, 100),
),
)
}
@@ -92,18 +102,47 @@ func (a *attributesBlock) Kind() ast.NodeKind {
return kindAttributesBlock
}
-type transformer struct{}
+type transformer struct {
+ cfg goldmark_config.Parser
+}
+
+func (a *transformer) isFragmentNode(n ast.Node) bool {
+ switch n.Kind() {
+ case east.KindDefinitionTerm, ast.KindHeading:
+ return true
+ default:
+ return false
+ }
+}
func (a *transformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
- attributes := make([]ast.Node, 0, 500)
+ var attributes []ast.Node
+ var solitaryAttributeNodes []ast.Node
+ if a.cfg.Attribute.Block {
+ attributes = make([]ast.Node, 0, 100)
+ }
ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
- if entering && node.Kind() == kindAttributesBlock {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+
+ if a.isFragmentNode(node) {
+ if id, found := node.Attribute(attrNameID); !found {
+ a.generateAutoID(node, reader, pc)
+ } else {
+ pc.IDs().Put(id.([]byte))
+ }
+ }
+
+ if a.cfg.Attribute.Block && node.Kind() == kindAttributesBlock {
// Attributes for fenced code blocks are handled in their own extension,
// but note that we currently only support code block attributes when
// CodeFences=true.
if node.PreviousSibling() != nil && node.PreviousSibling().Kind() != ast.KindFencedCodeBlock && !node.HasBlankPreviousLines() {
attributes = append(attributes, node)
return ast.WalkSkipChildren, nil
+ } else {
+ solitaryAttributeNodes = append(solitaryAttributeNodes, node)
}
}
@@ -122,4 +161,44 @@ func (a *transformer) Transform(node *ast.Document, reader text.Reader, pc parse
// remove attributes node
attr.Parent().RemoveChild(attr.Parent(), attr)
}
+
+ // Remove any solitary attribute nodes.
+ for _, n := range solitaryAttributeNodes {
+ n.Parent().RemoveChild(n.Parent(), n)
+ }
+}
+
+func (a *transformer) generateAutoID(n ast.Node, reader text.Reader, pc parser.Context) {
+ var text []byte
+ switch n := n.(type) {
+ case *ast.Heading:
+ if a.cfg.AutoHeadingID {
+ text = textHeadingID(n, reader)
+ }
+ case *east.DefinitionTerm:
+ if a.cfg.AutoDefinitionTermID {
+ text = []byte(render.TextPlain(n, reader.Source()))
+ }
+ }
+
+ if len(text) > 0 {
+ headingID := pc.IDs().Generate(text, n.Kind())
+ n.SetAttribute(attrNameID, headingID)
+ }
+}
+
+// Markdown settext headers can have multiple lines, use the last line for the ID.
+func textHeadingID(n *ast.Heading, reader text.Reader) []byte {
+ text := render.TextPlain(n, reader.Source())
+ if n.Lines().Len() > 1 {
+
+ // For multiline headings, Goldmark's extension for headings returns the last line.
+ // We have a slightly different approach, but in most cases the end result should be the same.
+ // Instead of looking at the text segments in Lines (see #13405 for issues with that),
+ // we split the text above and use the last line.
+ parts := strings.Split(text, "\n")
+ text = parts[len(parts)-1]
+ }
+
+ return []byte(text)
}
diff --git a/markup/goldmark/internal/extensions/attributes/attributes_integration_test.go b/markup/goldmark/internal/extensions/attributes/attributes_integration_test.go
new file mode 100644
index 000000000..e56c52550
--- /dev/null
+++ b/markup/goldmark/internal/extensions/attributes/attributes_integration_test.go
@@ -0,0 +1,110 @@
+package attributes_test
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/hugolib"
+)
+
+func TestDescriptionListAutoID(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+[markup.goldmark.parser]
+autoHeadingID = true
+autoDefinitionTermID = true
+autoIDType = 'github-ascii'
+-- content/p1.md --
+---
+title: "Title"
+---
+
+## Title with id set {#title-with-id}
+
+## Title with id set duplicate {#title-with-id}
+
+## My Title
+
+Base Name
+: Base name of the file.
+
+Base Name
+: Duplicate term name.
+
+My Title
+: Term with same name as title.
+
+Foo@Bar
+: The foo bar.
+
+foo [something](/a/b/) bar
+: A foo bar.
+
+良善天父
+: The good father.
+
+Ā ā Ă ă Ą ą Ć ć Ĉ ĉ Ċ ċ Č č Ď
+: Testing accents.
+
+Multiline set text header
+Second line
+---------------
+
+## Example [hyperlink](https://example.com/) in a header
+
+-- layouts/_default/single.html --
+{{ .Content }}|Identifiers: {{ .Fragments.Identifiers }}|
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html",
+ `Base Name `,
+ `Base Name `,
+ `Foo@Bar `,
+ `My Title `,
+ `foo something bar `,
+ `Title with id set `,
+ `Title with id set duplicate `,
+ `My Title `,
+ `良善天父 `,
+ `Ā ā Ă ă Ą ą Ć ć Ĉ ĉ Ċ ċ Č č Ď `,
+ ``,
+ ` a `,
+ )
+}
diff --git a/markup/goldmark/internal/render/context.go b/markup/goldmark/internal/render/context.go
index 306d26748..fd8e788ed 100644
--- a/markup/goldmark/internal/render/context.go
+++ b/markup/goldmark/internal/render/context.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Hugo Authors. All rights reserved.
+// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -16,8 +16,20 @@ package render
import (
"bytes"
"math/bits"
+ "strings"
+ "sync"
+
+ "github.com/gohugoio/hugo-goldmark-extensions/passthrough"
+ bp "github.com/gohugoio/hugo/bufferpool"
+ east "github.com/yuin/goldmark-emoji/ast"
+
+ htext "github.com/gohugoio/hugo/common/text"
+ "github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/markup/converter"
+ "github.com/gohugoio/hugo/markup/converter/hooks"
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/util"
)
type BufWriter struct {
@@ -40,9 +52,20 @@ func (b *BufWriter) Flush() error {
type Context struct {
*BufWriter
+ ContextData
positions []int
pids []uint64
- ContextData
+ ordinals map[ast.NodeKind]int
+ values map[ast.NodeKind][]any
+}
+
+func (ctx *Context) GetAndIncrementOrdinal(kind ast.NodeKind) int {
+ if ctx.ordinals == nil {
+ ctx.ordinals = make(map[ast.NodeKind]int)
+ }
+ i := ctx.ordinals[kind]
+ ctx.ordinals[kind]++
+ return i
}
func (ctx *Context) PushPos(n int) {
@@ -56,6 +79,13 @@ func (ctx *Context) PopPos() int {
return p
}
+func (ctx *Context) PopRenderedString() string {
+ pos := ctx.PopPos()
+ text := string(ctx.Bytes()[pos:])
+ ctx.Truncate(pos)
+ return text
+}
+
// PushPid pushes a new page ID to the stack.
func (ctx *Context) PushPid(pid uint64) {
ctx.pids = append(ctx.pids, pid)
@@ -80,6 +110,38 @@ func (ctx *Context) PopPid() uint64 {
return p
}
+func (ctx *Context) PushValue(k ast.NodeKind, v any) {
+ if ctx.values == nil {
+ ctx.values = make(map[ast.NodeKind][]any)
+ }
+ ctx.values[k] = append(ctx.values[k], v)
+}
+
+func (ctx *Context) PopValue(k ast.NodeKind) any {
+ if ctx.values == nil {
+ return nil
+ }
+ v := ctx.values[k]
+ if len(v) == 0 {
+ return nil
+ }
+ i := len(v) - 1
+ r := v[i]
+ ctx.values[k] = v[:i]
+ return r
+}
+
+func (ctx *Context) PeekValue(k ast.NodeKind) any {
+ if ctx.values == nil {
+ return nil
+ }
+ v := ctx.values[k]
+ if len(v) == 0 {
+ return nil
+ }
+ return v[len(v)-1]
+}
+
type ContextData interface {
RenderContext() converter.RenderContext
DocumentContext() converter.DocumentContext
@@ -97,3 +159,169 @@ func (ctx *RenderContextDataHolder) RenderContext() converter.RenderContext {
func (ctx *RenderContextDataHolder) DocumentContext() converter.DocumentContext {
return ctx.Dctx
}
+
+// extractSourceSample returns a sample of the source for the given node.
+// Note that this is not a copy of the source, but a slice of it,
+// so it assumes that the source is not mutated.
+func extractSourceSample(n ast.Node, src []byte) []byte {
+ if n.Type() == ast.TypeInline {
+ switch n := n.(type) {
+ case *passthrough.PassthroughInline:
+ return n.Segment.Value(src)
+ }
+
+ return nil
+ }
+
+ var sample []byte
+
+ getStartStop := func(n ast.Node) (int, int) {
+ if n == nil {
+ return 0, 0
+ }
+
+ var start, stop int
+ for i := 0; i < n.Lines().Len() && i < 2; i++ {
+ line := n.Lines().At(i)
+ if i == 0 {
+ start = line.Start
+ }
+ stop = line.Stop
+ }
+ return start, stop
+ }
+
+ start, stop := getStartStop(n)
+ if stop == 0 {
+ // Try first child.
+ start, stop = getStartStop(n.FirstChild())
+ }
+
+ if stop > 0 {
+ // We do not mutate the source, so this is safe.
+ sample = src[start:stop]
+ }
+
+ return sample
+}
+
+// GetPageAndPageInner returns the current page and the inner page for the given context.
+func GetPageAndPageInner(rctx *Context) (any, any) {
+ p := rctx.DocumentContext().Document
+ pid := rctx.PeekPid()
+ if pid > 0 {
+ if lookup := rctx.DocumentContext().DocumentLookup; lookup != nil {
+ if v := rctx.DocumentContext().DocumentLookup(pid); v != nil {
+ return p, v
+ }
+ }
+ }
+ return p, p
+}
+
+// NewBaseContext creates a new BaseContext.
+func NewBaseContext(rctx *Context, renderer any, n ast.Node, src []byte, getSourceSample func() []byte, ordinal int) hooks.BaseContext {
+ if getSourceSample == nil {
+ getSourceSample = func() []byte {
+ return extractSourceSample(n, src)
+ }
+ }
+ page, pageInner := GetPageAndPageInner(rctx)
+ b := &hookBase{
+ page: page,
+ pageInner: pageInner,
+
+ getSourceSample: getSourceSample,
+ ordinal: ordinal,
+ }
+
+ b.createPos = func() htext.Position {
+ if resolver, ok := renderer.(hooks.ElementPositionResolver); ok {
+ return resolver.ResolvePosition(b)
+ }
+
+ return htext.Position{
+ Filename: rctx.DocumentContext().Filename,
+ LineNumber: 1,
+ ColumnNumber: 1,
+ }
+ }
+
+ return b
+}
+
+var _ hooks.PositionerSourceTargetProvider = (*hookBase)(nil)
+
+type hookBase struct {
+ page any
+ pageInner any
+ ordinal int
+
+ // This is only used in error situations and is expensive to create,
+ // so delay creation until needed.
+ pos htext.Position
+ posInit sync.Once
+ createPos func() htext.Position
+ getSourceSample func() []byte
+}
+
+func (c *hookBase) Page() any {
+ return c.page
+}
+
+func (c *hookBase) PageInner() any {
+ return c.pageInner
+}
+
+func (c *hookBase) Ordinal() int {
+ return c.ordinal
+}
+
+func (c *hookBase) Position() htext.Position {
+ c.posInit.Do(func() {
+ c.pos = c.createPos()
+ })
+ return c.pos
+}
+
+// For internal use.
+func (c *hookBase) PositionerSourceTarget() []byte {
+ return c.getSourceSample()
+}
+
+// TextPlain returns a plain text representation of the given node.
+// This will resolve any leftover HTML entities. This will typically be
+// entities inserted by e.g. the typographer extension.
+// Goldmark's Node.Text was deprecated in 1.7.8.
+func TextPlain(n ast.Node, source []byte) string {
+ buf := bp.GetBuffer()
+ defer bp.PutBuffer(buf)
+
+ for c := n.FirstChild(); c != nil; c = c.NextSibling() {
+ textPlainTo(c, source, buf)
+ }
+ return string(util.ResolveEntityNames(buf.Bytes()))
+}
+
+func textPlainTo(c ast.Node, source []byte, buf *bytes.Buffer) {
+ if c == nil {
+ return
+ }
+
+ switch c := c.(type) {
+ case *ast.RawHTML:
+ s := strings.TrimSpace(tpl.StripHTML(string(c.Segments.Value(source))))
+ buf.WriteString(s)
+ case *ast.String:
+ buf.Write(c.Value)
+ case *ast.Text:
+ buf.Write(c.Segment.Value(source))
+ if c.HardLineBreak() || c.SoftLineBreak() {
+ buf.WriteByte('\n')
+ }
+ case *east.Emoji:
+ buf.WriteString(string(c.ShortName))
+ default:
+ textPlainTo(c.FirstChild(), source, buf)
+ }
+}
diff --git a/markup/goldmark/passthrough/passthrough.go b/markup/goldmark/passthrough/passthrough.go
new file mode 100644
index 000000000..c56842f3d
--- /dev/null
+++ b/markup/goldmark/passthrough/passthrough.go
@@ -0,0 +1,166 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package passthrough
+
+import (
+ "bytes"
+
+ "github.com/gohugoio/hugo-goldmark-extensions/passthrough"
+ "github.com/gohugoio/hugo/markup/converter/hooks"
+ "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
+ "github.com/gohugoio/hugo/markup/goldmark/internal/render"
+ "github.com/gohugoio/hugo/markup/internal/attributes"
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/util"
+)
+
+func New(cfg goldmark_config.Passthrough) goldmark.Extender {
+ if !cfg.Enable {
+ return nil
+ }
+ return &passthroughExtension{cfg: cfg}
+}
+
+type (
+ passthroughExtension struct {
+ cfg goldmark_config.Passthrough
+ }
+ htmlRenderer struct{}
+)
+
+func (e *passthroughExtension) Extend(m goldmark.Markdown) {
+ configuredInlines := e.cfg.Delimiters.Inline
+ configuredBlocks := e.cfg.Delimiters.Block
+
+ inlineDelimiters := make([]passthrough.Delimiters, len(configuredInlines))
+ blockDelimiters := make([]passthrough.Delimiters, len(configuredBlocks))
+
+ for i, d := range configuredInlines {
+ inlineDelimiters[i] = passthrough.Delimiters{
+ Open: d[0],
+ Close: d[1],
+ }
+ }
+
+ for i, d := range configuredBlocks {
+ blockDelimiters[i] = passthrough.Delimiters{
+ Open: d[0],
+ Close: d[1],
+ }
+ }
+
+ pse := passthrough.New(
+ passthrough.Config{
+ InlineDelimiters: inlineDelimiters,
+ BlockDelimiters: blockDelimiters,
+ },
+ )
+
+ pse.Extend(m)
+
+ // Set up render hooks if configured.
+ // Upstream passthrough inline = 101
+ // Upstream passthrough block = 99
+ m.Renderer().AddOptions(renderer.WithNodeRenderers(
+ util.Prioritized(newHTMLRenderer(), 90),
+ ))
+}
+
+func newHTMLRenderer() renderer.NodeRenderer {
+ r := &htmlRenderer{}
+ return r
+}
+
+func (r *htmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+ reg.Register(passthrough.KindPassthroughBlock, r.renderPassthroughBlock)
+ reg.Register(passthrough.KindPassthroughInline, r.renderPassthroughBlock)
+}
+
+func (r *htmlRenderer) renderPassthroughBlock(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ ctx := w.(*render.Context)
+
+ if entering {
+ return ast.WalkContinue, nil
+ }
+
+ var (
+ s string
+ typ string
+ delims *passthrough.Delimiters
+ )
+
+ switch nn := node.(type) {
+ case *passthrough.PassthroughInline:
+ s = string(nn.Text(src))
+ typ = "inline"
+ delims = nn.Delimiters
+ case (*passthrough.PassthroughBlock):
+ l := nn.Lines().Len()
+ var buff bytes.Buffer
+ for i := range l {
+ line := nn.Lines().At(i)
+ buff.Write(line.Value(src))
+ }
+ s = buff.String()
+ typ = "block"
+ delims = nn.Delimiters
+ }
+
+ renderer := ctx.RenderContext().GetRenderer(hooks.PassthroughRendererType, typ)
+ if renderer == nil {
+ // Write the raw content if no renderer is found.
+ ctx.WriteString(s)
+ return ast.WalkContinue, nil
+ }
+
+ // Inline and block passthroughs share the same ordinal counter.
+ ordinal := ctx.GetAndIncrementOrdinal(passthrough.KindPassthroughBlock)
+
+ // Trim the delimiters.
+ s = s[len(delims.Open) : len(s)-len(delims.Close)]
+
+ pctx := &passthroughContext{
+ BaseContext: render.NewBaseContext(ctx, renderer, node, src, nil, ordinal),
+ inner: s,
+ typ: typ,
+ AttributesHolder: attributes.New(node.Attributes(), attributes.AttributesOwnerGeneral),
+ }
+
+ pr := renderer.(hooks.PassthroughRenderer)
+
+ if err := pr.RenderPassthrough(ctx.RenderContext().Ctx, w, pctx); err != nil {
+ return ast.WalkStop, err
+ }
+
+ return ast.WalkContinue, nil
+}
+
+type passthroughContext struct {
+ hooks.BaseContext
+
+ typ string // inner or block
+ inner string
+
+ *attributes.AttributesHolder
+}
+
+func (p *passthroughContext) Type() string {
+ return p.typ
+}
+
+func (p *passthroughContext) Inner() string {
+ return p.inner
+}
diff --git a/markup/goldmark/passthrough/passthrough_integration_test.go b/markup/goldmark/passthrough/passthrough_integration_test.go
new file mode 100644
index 000000000..2d51c5961
--- /dev/null
+++ b/markup/goldmark/passthrough/passthrough_integration_test.go
@@ -0,0 +1,62 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package passthrough_test
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/hugolib"
+)
+
+func TestPassthroughRenderHook(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- config.toml --
+[markup.goldmark.extensions.passthrough]
+enable = true
+[markup.goldmark.extensions.passthrough.delimiters]
+block = [['$$', '$$']]
+inline = [['$', '$']]
+-- content/p1.md --
+---
+title: "p1"
+---
+## LaTeX test
+
+Some inline LaTeX 1: $a^*=x-b^*$.
+
+Block equation that would be mangled by default parser:
+
+$$a^*=x-b^*$$
+
+Some inline LaTeX 2: $a^*=x-b^*$.
+
+-- layouts/_default/single.html --
+{{ .Content }}
+-- layouts/_default/_markup/render-passthrough-block.html --
+Passthrough block: {{ .Inner | safeHTML }}|{{ .Type }}|{{ .Ordinal }}:END
+-- layouts/_default/_markup/render-passthrough-inline.html --
+Passthrough inline: {{ .Inner | safeHTML }}|{{ .Type }}|{{ .Ordinal }}:END
+
+`
+
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/p1/index.html", `
+ Some inline LaTeX 1: Passthrough inline: a^*=x-b^*|inline|0:END
+ Passthrough block: a^*=x-b^*|block|1:END
+ Some inline LaTeX 2: Passthrough inline: a^*=x-b^*|inline|2:END
+
+ `)
+}
diff --git a/markup/goldmark/render_hooks.go b/markup/goldmark/render_hooks.go
index c127a2c0e..1e91f7ab1 100644
--- a/markup/goldmark/render_hooks.go
+++ b/markup/goldmark/render_hooks.go
@@ -52,7 +52,7 @@ type linkContext struct {
pageInner any
destination string
title string
- text hstring.RenderedString
+ text hstring.HTML
plainText string
*attributes.AttributesHolder
}
@@ -69,7 +69,7 @@ func (ctx linkContext) PageInner() any {
return ctx.pageInner
}
-func (ctx linkContext) Text() hstring.RenderedString {
+func (ctx linkContext) Text() hstring.HTML {
return ctx.text
}
@@ -100,7 +100,7 @@ type headingContext struct {
pageInner any
level int
anchor string
- text hstring.RenderedString
+ text hstring.HTML
plainText string
*attributes.AttributesHolder
}
@@ -121,7 +121,7 @@ func (ctx headingContext) Anchor() string {
return ctx.anchor
}
-func (ctx headingContext) Text() hstring.RenderedString {
+func (ctx headingContext) Text() hstring.HTML {
return ctx.text
}
@@ -169,9 +169,7 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
return ast.WalkContinue, nil
}
- pos := ctx.PopPos()
- text := ctx.Buffer.Bytes()[pos:]
- ctx.Buffer.Truncate(pos)
+ text := ctx.PopRenderedString()
var (
isBlock bool
@@ -190,17 +188,19 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
// internal attributes before rendering.
attrs := r.filterInternalAttributes(n.Attributes())
+ page, pageInner := render.GetPageAndPageInner(ctx)
+
err := lr.RenderLink(
ctx.RenderContext().Ctx,
w,
imageLinkContext{
linkContext: linkContext{
- page: ctx.DocumentContext().Document,
- pageInner: r.getPageInner(ctx),
+ page: page,
+ pageInner: pageInner,
destination: string(n.Destination),
title: string(n.Title),
- text: hstring.RenderedString(text),
- plainText: string(n.Text(source)),
+ text: hstring.HTML(text),
+ plainText: render.TextPlain(n, source),
AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerGeneral),
},
ordinal: ordinal,
@@ -211,18 +211,6 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
return ast.WalkContinue, err
}
-func (r *hookedRenderer) getPageInner(rctx *render.Context) any {
- pid := rctx.PeekPid()
- if pid > 0 {
- if lookup := rctx.DocumentContext().DocumentLookup; lookup != nil {
- if v := rctx.DocumentContext().DocumentLookup(pid); v != nil {
- return v
- }
- }
- }
- return rctx.DocumentContext().Document
-}
-
func (r *hookedRenderer) filterInternalAttributes(attrs []ast.Attribute) []ast.Attribute {
n := 0
for _, x := range attrs {
@@ -235,7 +223,7 @@ func (r *hookedRenderer) filterInternalAttributes(attrs []ast.Attribute) []ast.A
}
// Fall back to the default Goldmark render funcs. Method below borrowed from:
-// https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404
+// https://github.com/yuin/goldmark
func (r *hookedRenderer) renderImageDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
@@ -246,7 +234,7 @@ func (r *hookedRenderer) renderImageDefault(w util.BufWriter, source []byte, nod
_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
}
_, _ = w.WriteString(`" alt="`)
- _, _ = w.Write(nodeToHTMLText(n, source))
+ r.renderTexts(w, source, n)
_ = w.WriteByte('"')
if n.Title != nil {
_, _ = w.WriteString(` title="`)
@@ -254,8 +242,7 @@ func (r *hookedRenderer) renderImageDefault(w util.BufWriter, source []byte, nod
_ = w.WriteByte('"')
}
if n.Attributes() != nil {
- attrs := r.filterInternalAttributes(n.Attributes())
- attributes.RenderASTAttributes(w, attrs...)
+ html.RenderAttributes(w, n, html.ImageAttributeFilter)
}
if r.XHTML {
_, _ = w.WriteString(" />")
@@ -288,20 +275,20 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
return ast.WalkContinue, nil
}
- pos := ctx.PopPos()
- text := ctx.Buffer.Bytes()[pos:]
- ctx.Buffer.Truncate(pos)
+ text := ctx.PopRenderedString()
+
+ page, pageInner := render.GetPageAndPageInner(ctx)
err := lr.RenderLink(
ctx.RenderContext().Ctx,
w,
linkContext{
- page: ctx.DocumentContext().Document,
- pageInner: r.getPageInner(ctx),
+ page: page,
+ pageInner: pageInner,
destination: string(n.Destination),
title: string(n.Title),
- text: hstring.RenderedString(text),
- plainText: string(n.Text(source)),
+ text: hstring.HTML(text),
+ plainText: render.TextPlain(n, source),
AttributesHolder: attributes.Empty,
},
)
@@ -309,6 +296,79 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
return ast.WalkContinue, err
}
+// Borrowed from Goldmark's HTML renderer.
+func (r *hookedRenderer) renderTexts(w util.BufWriter, source []byte, n ast.Node) {
+ for c := n.FirstChild(); c != nil; c = c.NextSibling() {
+ if s, ok := c.(*ast.String); ok {
+ _, _ = r.renderString(w, source, s, true)
+ } else if t, ok := c.(*ast.Text); ok {
+ _, _ = r.renderText(w, source, t, true)
+ } else {
+ r.renderTexts(w, source, c)
+ }
+ }
+}
+
+// Borrowed from Goldmark's HTML renderer.
+func (r *hookedRenderer) renderString(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ n := node.(*ast.String)
+ if n.IsCode() {
+ _, _ = w.Write(n.Value)
+ } else {
+ if n.IsRaw() {
+ r.Writer.RawWrite(w, n.Value)
+ } else {
+ r.Writer.Write(w, n.Value)
+ }
+ }
+ return ast.WalkContinue, nil
+}
+
+// Borrowed from Goldmark's HTML renderer.
+func (r *hookedRenderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ n := node.(*ast.Text)
+ segment := n.Segment
+ if n.IsRaw() {
+ r.Writer.RawWrite(w, segment.Value(source))
+ } else {
+ value := segment.Value(source)
+ r.Writer.Write(w, value)
+ if n.HardLineBreak() || (n.SoftLineBreak() && r.HardWraps) {
+ if r.XHTML {
+ _, _ = w.WriteString(" \n")
+ } else {
+ _, _ = w.WriteString(" \n")
+ }
+ } else if n.SoftLineBreak() {
+ // TODO(bep) we use these methods a fallback to default rendering when no image/link hooks are defined.
+ // I don't think the below is relevant in these situations, but if so, we need to create a PR
+ // upstream to export softLineBreak.
+ /*if r.EastAsianLineBreaks != html.EastAsianLineBreaksNone && len(value) != 0 {
+ sibling := node.NextSibling()
+ if sibling != nil && sibling.Kind() == ast.KindText {
+ if siblingText := sibling.(*ast.Text).Value(source); len(siblingText) != 0 {
+ thisLastRune := util.ToRune(value, len(value)-1)
+ siblingFirstRune, _ := utf8.DecodeRune(siblingText)
+ if r.EastAsianLineBreaks.softLineBreak(thisLastRune, siblingFirstRune) {
+ _ = w.WriteByte('\n')
+ }
+ }
+ }
+ } else {
+ _ = w.WriteByte('\n')
+ }*/
+ _ = w.WriteByte('\n')
+ }
+ }
+ return ast.WalkContinue, nil
+}
+
// Fall back to the default Goldmark render funcs. Method below borrowed from:
// https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404
func (r *hookedRenderer) renderLinkDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
@@ -358,14 +418,16 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
url = "mailto:" + url
}
+ page, pageInner := render.GetPageAndPageInner(ctx)
+
err := lr.RenderLink(
ctx.RenderContext().Ctx,
w,
linkContext{
- page: ctx.DocumentContext().Document,
- pageInner: r.getPageInner(ctx),
+ page: page,
+ pageInner: pageInner,
destination: url,
- text: hstring.RenderedString(label),
+ text: hstring.HTML(label),
plainText: label,
AttributesHolder: attributes.Empty,
},
@@ -435,24 +497,25 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
return ast.WalkContinue, nil
}
- pos := ctx.PopPos()
- text := ctx.Buffer.Bytes()[pos:]
- ctx.Buffer.Truncate(pos)
- // All ast.Heading nodes are guaranteed to have an attribute called "id"
- // that is an array of bytes that encode a valid string.
- anchori, _ := n.AttributeString("id")
- anchor := anchori.([]byte)
+ text := ctx.PopRenderedString()
+
+ var anchor []byte
+ if anchori, ok := n.AttributeString("id"); ok {
+ anchor, _ = anchori.([]byte)
+ }
+
+ page, pageInner := render.GetPageAndPageInner(ctx)
err := hr.RenderHeading(
ctx.RenderContext().Ctx,
w,
headingContext{
- page: ctx.DocumentContext().Document,
- pageInner: r.getPageInner(ctx),
+ page: page,
+ pageInner: pageInner,
level: n.Level,
anchor: string(anchor),
- text: hstring.RenderedString(text),
- plainText: string(n.Text(source)),
+ text: hstring.HTML(text),
+ plainText: render.TextPlain(n, source),
AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
},
)
@@ -487,21 +550,3 @@ func (e *links) Extend(m goldmark.Markdown) {
util.Prioritized(newLinkRenderer(e.cfg), 100),
))
}
-
-// Borrowed from Goldmark.
-func nodeToHTMLText(n ast.Node, source []byte) []byte {
- var buf bytes.Buffer
- for c := n.FirstChild(); c != nil; c = c.NextSibling() {
- if s, ok := c.(*ast.String); ok && s.IsCode() {
- buf.Write(s.Text(source))
- } else if !c.HasChildren() {
- buf.Write(util.EscapeHTML(c.Text(source)))
- if t, ok := c.(*ast.Text); ok && t.SoftLineBreak() {
- buf.WriteByte('\n')
- }
- } else {
- buf.Write(nodeToHTMLText(c, source))
- }
- }
- return buf.Bytes()
-}
diff --git a/markup/goldmark/tables/tables.go b/markup/goldmark/tables/tables.go
new file mode 100644
index 000000000..a7dfb8ee6
--- /dev/null
+++ b/markup/goldmark/tables/tables.go
@@ -0,0 +1,175 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tables
+
+import (
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/types/hstring"
+ "github.com/gohugoio/hugo/markup/converter/hooks"
+ "github.com/gohugoio/hugo/markup/goldmark/internal/render"
+ "github.com/gohugoio/hugo/markup/internal/attributes"
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/ast"
+ gast "github.com/yuin/goldmark/extension/ast"
+ "github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/util"
+)
+
+type (
+ ext struct{}
+ htmlRenderer struct{}
+)
+
+func New() goldmark.Extender {
+ return &ext{}
+}
+
+func (e *ext) Extend(m goldmark.Markdown) {
+ m.Renderer().AddOptions(renderer.WithNodeRenderers(
+ util.Prioritized(newHTMLRenderer(), 100),
+ ))
+}
+
+func newHTMLRenderer() renderer.NodeRenderer {
+ r := &htmlRenderer{}
+ return r
+}
+
+func (r *htmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+ reg.Register(gast.KindTable, r.renderTable)
+ reg.Register(gast.KindTableHeader, r.renderHeaderOrRow)
+ reg.Register(gast.KindTableRow, r.renderHeaderOrRow)
+ reg.Register(gast.KindTableCell, r.renderCell)
+}
+
+func (r *htmlRenderer) renderTable(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
+ ctx := w.(*render.Context)
+ if entering {
+ // This will be modified below.
+ table := &hooks.Table{}
+ ctx.PushValue(gast.KindTable, table)
+ return ast.WalkContinue, nil
+ }
+
+ v := ctx.PopValue(gast.KindTable)
+ if v == nil {
+ panic("table not found")
+ }
+
+ table := v.(*hooks.Table)
+
+ renderer := ctx.RenderContext().GetRenderer(hooks.TableRendererType, nil)
+ if renderer == nil {
+ panic("table hook renderer not found")
+ }
+
+ ordinal := ctx.GetAndIncrementOrdinal(gast.KindTable)
+
+ tctx := &tableContext{
+ BaseContext: render.NewBaseContext(ctx, renderer, n, source, nil, ordinal),
+ AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
+ tHead: table.THead,
+ tBody: table.TBody,
+ }
+
+ cr := renderer.(hooks.TableRenderer)
+
+ err := cr.RenderTable(
+ ctx.RenderContext().Ctx,
+ w,
+ tctx,
+ )
+ if err != nil {
+ return ast.WalkContinue, herrors.NewFileErrorFromPos(err, tctx.Position())
+ }
+
+ return ast.WalkContinue, nil
+}
+
+func (r *htmlRenderer) peekTable(ctx *render.Context) *hooks.Table {
+ v := ctx.PeekValue(gast.KindTable)
+ if v == nil {
+ panic("table not found")
+ }
+ return v.(*hooks.Table)
+}
+
+func (r *htmlRenderer) renderCell(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ ctx := w.(*render.Context)
+
+ if entering {
+ // Store the current pos so we can capture the rendered text.
+ ctx.PushPos(ctx.Buffer.Len())
+ return ast.WalkContinue, nil
+ }
+
+ n := node.(*gast.TableCell)
+
+ text := ctx.PopRenderedString()
+
+ table := r.peekTable(ctx)
+
+ var alignment string
+ switch n.Alignment {
+ case gast.AlignLeft:
+ alignment = "left"
+ case gast.AlignRight:
+ alignment = "right"
+ case gast.AlignCenter:
+ alignment = "center"
+ default:
+ alignment = ""
+ }
+
+ cell := hooks.TableCell{Text: hstring.HTML(text), Alignment: alignment}
+
+ if node.Parent().Kind() == gast.KindTableHeader {
+ table.THead[len(table.THead)-1] = append(table.THead[len(table.THead)-1], cell)
+ } else {
+ table.TBody[len(table.TBody)-1] = append(table.TBody[len(table.TBody)-1], cell)
+ }
+
+ return ast.WalkContinue, nil
+}
+
+func (r *htmlRenderer) renderHeaderOrRow(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
+ ctx := w.(*render.Context)
+ table := r.peekTable(ctx)
+ if entering {
+ if n.Kind() == gast.KindTableHeader {
+ table.THead = append(table.THead, hooks.TableRow{})
+ } else {
+ table.TBody = append(table.TBody, hooks.TableRow{})
+ }
+ return ast.WalkContinue, nil
+ }
+
+ return ast.WalkContinue, nil
+}
+
+type tableContext struct {
+ hooks.BaseContext
+ *attributes.AttributesHolder
+
+ tHead []hooks.TableRow
+ tBody []hooks.TableRow
+}
+
+func (c *tableContext) THead() []hooks.TableRow {
+ return c.tHead
+}
+
+func (c *tableContext) TBody() []hooks.TableRow {
+ return c.tBody
+}
diff --git a/markup/goldmark/tables/tables_integration_test.go b/markup/goldmark/tables/tables_integration_test.go
new file mode 100644
index 000000000..36cf953ae
--- /dev/null
+++ b/markup/goldmark/tables/tables_integration_test.go
@@ -0,0 +1,182 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tables_test
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/hugolib"
+)
+
+func TestTableHook(t *testing.T) {
+ t.Parallel()
+ files := `
+-- hugo.toml --
+[markup.goldmark.parser.attribute]
+block = true
+title = true
+-- content/p1.md --
+## Table 1
+
+| Item | In Stock | Price |
+| :---------------- | :------: | ----: |
+| Python Hat | True | 23.99 |
+| SQL **Hat** | True | 23.99 |
+| Codecademy Tee | False | 19.99 |
+| Codecademy Hoodie | False | 42.99 |
+{.foo foo="bar"}
+
+## Table 2
+
+| Month | Savings |
+| -------- | ------- |
+| January | $250 |
+| February | $80 |
+| March | $420 |
+
+-- layouts/_default/single.html --
+{{ .Content }}
+-- layouts/_default/_markup/render-table.html --
+Attributes: {{ .Attributes }}|
+{{ template "print" (dict "what" (printf "table-%d-thead" $.Ordinal) "rows" .THead) }}
+{{ template "print" (dict "what" (printf "table-%d-tbody" $.Ordinal) "rows" .TBody) }}
+{{ define "print" }}
+ {{ .what }}:{{ range $i, $a := .rows }} {{ $i }}:{{ range $j, $b := . }} {{ $j }}: {{ .Alignment }}: {{ .Text }}|{{ end }}{{ end }}$
+{{ end }}
+
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html",
+ "Attributes: map[class:foo foo:bar]|",
+ "table-0-thead: 0: 0: left: Item| 1: center: In Stock| 2: right: Price|$",
+ "table-0-tbody: 0: 0: left: Python Hat| 1: center: True| 2: right: 23.99| 1: 0: left: SQL Hat | 1: center: True| 2: right: 23.99| 2: 0: left: Codecademy Tee| 1: center: False| 2: right: 19.99| 3: 0: left: Codecademy Hoodie| 1: center: False| 2: right: 42.99|$",
+ )
+
+ b.AssertFileContent("public/p1/index.html",
+ "table-1-thead: 0: 0: : Month| 1: : Savings|$",
+ "table-1-tbody: 0: 0: : January| 1: : $250| 1: 0: : February| 1: : $80| 2: 0: : March| 1: : $420|$",
+ )
+}
+
+func TestTableDefault(t *testing.T) {
+ t.Parallel()
+ files := `
+-- hugo.toml --
+[markup.goldmark.parser.attribute]
+block = true
+title = true
+-- content/p1.md --
+
+## Table 1
+
+| Item | In Stock | Price |
+| :---------------- | :------: | ----: |
+| Python Hat | True | 23.99 |
+| SQL Hat | True | 23.99 |
+| Codecademy Tee | False | 19.99 |
+| Codecademy Hoodie | False | 42.99 |
+{.foo}
+
+## Table 2
+
+a|b
+---|---
+1|2
+{id="\">"}
+
+-- layouts/_default/single.html --
+Summary: {{ .Summary }}
+Content: {{ .Content }}
+
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html", ``)
+ b.AssertFileContent("public/p1/index.html", ``)
+}
+
+// Issue 12811.
+func TestTableDefaultRSSAndHTML(t *testing.T) {
+ t.Parallel()
+ files := `
+-- hugo.toml --
+[outputFormats]
+ [outputFormats.rss]
+ weight = 30
+ [outputFormats.html]
+ weight = 20
+-- content/_index.md --
+---
+title: "Home"
+output: ["rss", "html"]
+---
+
+| Item | In Stock | Price |
+| :---------------- | :------: | ----: |
+| Python Hat | True | 23.99 |
+| SQL Hat | True | 23.99 |
+| Codecademy Tee | False | 19.99 |
+| Codecademy Hoodie | False | 42.99 |
+
+{{< foo >}}
+
+-- layouts/index.html --
+Content: {{ .Content }}
+-- layouts/index.xml --
+Content: {{ .Content }}
+-- layouts/shortcodes/foo.xml --
+foo xml
+-- layouts/shortcodes/foo.html --
+foo html
+
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.xml", "")
+ b.AssertFileContent("public/index.html", "")
+}
+
+func TestTableDefaultRSSOnly(t *testing.T) {
+ t.Parallel()
+ files := `
+-- hugo.toml --
+[outputs]
+ home = ['rss']
+ section = ['rss']
+ taxonomy = ['rss']
+ term = ['rss']
+ page = ['rss']
+disableKinds = ["taxonomy", "term", "page", "section"]
+-- content/_index.md --
+---
+title: "Home"
+---
+
+## Table 1
+
+| Item | In Stock | Price |
+| :---------------- | :------: | ----: |
+| Python Hat | True | 23.99 |
+| SQL Hat | True | 23.99 |
+| Codecademy Tee | False | 19.99 |
+| Codecademy Hoodie | False | 42.99 |
+
+-- layouts/index.xml --
+Content: {{ .Content }}
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.xml", "")
+}
diff --git a/markup/goldmark/toc.go b/markup/goldmark/toc.go
index b0f7e703f..538f65df4 100644
--- a/markup/goldmark/toc.go
+++ b/markup/goldmark/toc.go
@@ -53,6 +53,10 @@ func (t *tocTransformer) Transform(n *ast.Document, reader text.Reader, pc parse
headingText bytes.Buffer
)
+ if ids := pc.IDs().(stringValuesProvider).StringValues(); len(ids) > 0 {
+ toc.SetIdentifiers(ids)
+ }
+
ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
s := ast.WalkStatus(ast.WalkContinue)
if n.Kind() == ast.KindHeading {
@@ -131,5 +135,7 @@ func (e *tocExtension) Extend(m goldmark.Markdown) {
r.AddOptions(e.options...)
m.Parser().AddOptions(parser.WithASTTransformers(util.Prioritized(&tocTransformer{
r: r,
- }, 10)))
+ },
+ // This must run after the ID generation (priority 100).
+ 110)))
}
diff --git a/markup/goldmark/toc_integration_test.go b/markup/goldmark/toc_integration_test.go
index 3b48dac6c..814ae199b 100644
--- a/markup/goldmark/toc_integration_test.go
+++ b/markup/goldmark/toc_integration_test.go
@@ -239,12 +239,12 @@ title: p7 (emoji)
// image
b.AssertFileContent("public/p3/index.html", `
-An image
+An image
`)
// raw html
b.AssertFileContent("public/p4/index.html", `
-Some raw HTML
+Some raw HTML
`)
// typographer
@@ -258,7 +258,29 @@ title: p7 (emoji)
`)
// emoji
+
b.AssertFileContent("public/p7/index.html", `
A 🐍 emoji
`)
}
+
+func TestIssue13416(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- layouts/index.html --
+Content:{{ .Content }}|
+-- layouts/_default/_markup/render-heading.html --
+-- content/_index.md --
+---
+title: home
+---
+#
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileExists("public/index.html", true)
+}
diff --git a/markup/highlight/config.go b/markup/highlight/config.go
index 62db6b237..b19e9ec1b 100644
--- a/markup/highlight/config.go
+++ b/markup/highlight/config.go
@@ -33,7 +33,6 @@ const (
lineNosKey = "linenos"
hlLinesKey = "hl_lines"
linosStartKey = "linenostart"
- noHlKey = "nohl"
)
var DefaultConfig = Config{
@@ -45,19 +44,21 @@ var DefaultConfig = Config{
NoClasses: true,
LineNumbersInTable: true,
TabWidth: 4,
+ WrapperClass: "highlight",
}
type Config struct {
Style string
+ // Enable syntax highlighting of fenced code blocks.
CodeFences bool
+ // The class or classes to use for the outermost element of the highlighted code.
+ WrapperClass string
+
// Use inline CSS styles.
NoClasses bool
- // No highlighting.
- NoHl bool
-
// When set, line numbers will be printed.
LineNos bool
LineNumbersInTable bool
@@ -229,8 +230,6 @@ func normalizeHighlightOptions(m map[string]any) {
for k, v := range m {
switch k {
- case noHlKey:
- m[noHlKey] = cast.ToBool(v)
case lineNosKey:
if v == "table" || v == "inline" {
m["lineNumbersInTable"] = v == "table"
diff --git a/markup/highlight/highlight.go b/markup/highlight/highlight.go
index a284b5981..ee07fb9ad 100644
--- a/markup/highlight/highlight.go
+++ b/markup/highlight/highlight.go
@@ -168,7 +168,7 @@ func highlight(fw hugio.FlexiWriter, code, lang string, attributes []attributes.
lexer = chromalexers.Get(lang)
}
- if lexer == nil && (cfg.GuessSyntax && !cfg.NoHl) {
+ if lexer == nil && cfg.GuessSyntax {
lexer = lexers.Analyse(code)
if lexer == nil {
lexer = lexers.Fallback
@@ -202,7 +202,7 @@ func highlight(fw hugio.FlexiWriter, code, lang string, attributes []attributes.
}
if !cfg.Hl_inline {
- writeDivStart(w, attributes)
+ writeDivStart(w, attributes, cfg.WrapperClass)
}
options := cfg.toHTMLOptions()
@@ -303,8 +303,9 @@ func (s startEnd) End(code bool) string {
return s.end(code)
}
-func writeDivStart(w hugio.FlexiWriter, attrs []attributes.Attribute) {
- w.WriteString(`xəx
`)
}
+
+func TestHighlightClass(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- config.toml --
+[markup.highlight]
+noClasses = false
+wrapperClass = "highlight no-prose"
+-- content/_index.md --
+---
+title: home
+---
+§§§go
+xəx := 0
+§§§
+-- layouts/index.html --
+{{ .Content }}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", `
+
"))
if bodyEnd < 0 || bodyEnd >= len(result) {
- bodyEnd = len(result) - 1
- if bodyEnd < 0 {
- bodyEnd = 0
- }
+ bodyEnd = max(len(result)-1, 0)
}
return result[bodyStart+7 : bodyEnd], err
diff --git a/markup/rst/convert_test.go b/markup/rst/convert_test.go
index 1897e650f..730e00acf 100644
--- a/markup/rst/convert_test.go
+++ b/markup/rst/convert_test.go
@@ -36,7 +36,7 @@ func TestConvert(t *testing.T) {
p, err := Provider.New(
converter.ProviderConfig{
Logger: loggers.NewDefault(),
- Exec: hexec.New(sc),
+ Exec: hexec.New(sc, "", loggers.NewDefault()),
})
c.Assert(err, qt.IsNil)
conv, err := p.New(converter.DocumentContext{})
diff --git a/markup/tableofcontents/tableofcontents.go b/markup/tableofcontents/tableofcontents.go
index 49a9cdeb7..6c40c9a59 100644
--- a/markup/tableofcontents/tableofcontents.go
+++ b/markup/tableofcontents/tableofcontents.go
@@ -14,11 +14,13 @@
package tableofcontents
import (
+ "fmt"
"html/template"
"sort"
"strings"
"github.com/gohugoio/hugo/common/collections"
+ "github.com/spf13/cast"
)
// Empty is an empty ToC.
@@ -29,7 +31,8 @@ var Empty = &Fragments{
// Builder is used to build the ToC data structure.
type Builder struct {
- toc *Fragments
+ identifiersSet bool
+ toc *Fragments
}
// AddAt adds the heading to the ToC.
@@ -40,6 +43,16 @@ func (b *Builder) AddAt(h *Heading, row, level int) {
b.toc.addAt(h, row, level)
}
+// SetIdentifiers sets the identifiers in the ToC.
+func (b *Builder) SetIdentifiers(ids []string) {
+ if b.toc == nil {
+ b.toc = &Fragments{}
+ }
+ b.identifiersSet = true
+ sort.Strings(ids)
+ b.toc.Identifiers = ids
+}
+
// Build returns the ToC.
func (b Builder) Build() *Fragments {
if b.toc == nil {
@@ -49,7 +62,9 @@ func (b Builder) Build() *Fragments {
b.toc.walk(func(h *Heading) {
if h.ID != "" {
b.toc.HeadingsMap[h.ID] = h
- b.toc.Identifiers = append(b.toc.Identifiers, h.ID)
+ if !b.identifiersSet {
+ b.toc.Identifiers = append(b.toc.Identifiers, h.ID)
+ }
}
})
sort.Strings(b.toc.Identifiers)
@@ -133,19 +148,30 @@ func (toc *Fragments) addAt(h *Heading, row, level int) {
}
// ToHTML renders the ToC as HTML.
-func (toc *Fragments) ToHTML(startLevel, stopLevel int, ordered bool) template.HTML {
+func (toc *Fragments) ToHTML(startLevel, stopLevel any, ordered bool) (template.HTML, error) {
if toc == nil {
- return ""
+ return "", nil
}
+
+ iStartLevel, err := cast.ToIntE(startLevel)
+ if err != nil {
+ return "", fmt.Errorf("startLevel: %w", err)
+ }
+
+ iStopLevel, err := cast.ToIntE(stopLevel)
+ if err != nil {
+ return "", fmt.Errorf("stopLevel: %w", err)
+ }
+
b := &tocBuilder{
s: strings.Builder{},
h: toc.Headings,
- startLevel: startLevel,
- stopLevel: stopLevel,
+ startLevel: iStartLevel,
+ stopLevel: iStopLevel,
ordered: ordered,
}
b.Build()
- return template.HTML(b.s.String())
+ return template.HTML(b.s.String()), nil
}
func (toc Fragments) walk(fn func(*Heading)) {
@@ -224,7 +250,7 @@ func (b *tocBuilder) writeHeading(level, indent int, h *Heading) {
}
func (b *tocBuilder) indent(n int) {
- for i := 0; i < n; i++ {
+ for range n {
b.s.WriteString(" ")
}
}
diff --git a/markup/tableofcontents/tableofcontents_integration_test.go b/markup/tableofcontents/tableofcontents_integration_test.go
index 87a7c0108..e6ae03ce2 100644
--- a/markup/tableofcontents/tableofcontents_integration_test.go
+++ b/markup/tableofcontents/tableofcontents_integration_test.go
@@ -14,6 +14,7 @@
package tableofcontents_test
import (
+ "strings"
"testing"
"github.com/gohugoio/hugo/hugolib"
@@ -43,3 +44,80 @@ disableKinds = ['page','rss','section','sitemap','taxonomy','term']
"heading-l5|5|Heading L5",
)
}
+
+// Issue #13107
+func TestToHTMLArgTypes(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['home','section','rss','sitemap','taxonomy','term']
+-- layouts/_default/single.html --
+{{ .Fragments.ToHTML .Params.toc.startLevel .Params.toc.endLevel false }}
+-- content/json.md --
+{
+ "title": "json",
+ "params": {
+ "toc": {
+ "startLevel": 2,
+ "endLevel": 4
+ }
+ }
+}
+CONTENT
+-- content/toml.md --
++++
+title = 'toml'
+[params.toc]
+startLevel = 2
+endLevel = 4
++++
+CONTENT
+-- content/yaml.md --
+---
+title: yaml
+params:
+ toc:
+ startLevel: 2
+ endLevel: 4
+---
+CONTENT
+`
+
+ content := `
+# Level One
+## Level Two
+### Level Three
+#### Level Four
+##### Level Five
+###### Level Six
+ `
+
+ want := `
+
+
+
+`
+
+ files = strings.ReplaceAll(files, "CONTENT", content)
+
+ b := hugolib.Test(t, files)
+ b.AssertFileContentEquals("public/json/index.html", strings.TrimSpace(want))
+ b.AssertFileContentEquals("public/toml/index.html", strings.TrimSpace(want))
+ b.AssertFileContentEquals("public/yaml/index.html", strings.TrimSpace(want))
+
+ files = strings.ReplaceAll(files, `2`, `"x"`)
+
+ b, _ = hugolib.TestE(t, files)
+ b.AssertLogMatches(`error calling ToHTML: startLevel: unable to cast "x" of type string`)
+}
diff --git a/markup/tableofcontents/tableofcontents_test.go b/markup/tableofcontents/tableofcontents_test.go
index 3af9c4eb6..b07d9e3ad 100644
--- a/markup/tableofcontents/tableofcontents_test.go
+++ b/markup/tableofcontents/tableofcontents_test.go
@@ -45,7 +45,8 @@ func TestToc(t *testing.T) {
toc.addAt(&Heading{Title: "1-H3-1", ID: "1-h2-2"}, 0, 2)
toc.addAt(&Heading{Title: "Heading 2", ID: "h1-2"}, 1, 0)
- got := string(toc.ToHTML(1, -1, false))
+ tocHTML, _ := toc.ToHTML(1, -1, false)
+ got := string(tocHTML)
c.Assert(got, qt.Equals, `
Heading 1
@@ -62,7 +63,8 @@ func TestToc(t *testing.T) {
`, qt.Commentf(got))
- got = string(toc.ToHTML(1, 1, false))
+ tocHTML, _ = toc.ToHTML(1, 1, false)
+ got = string(tocHTML)
c.Assert(got, qt.Equals, `
Heading 1
@@ -70,7 +72,8 @@ func TestToc(t *testing.T) {
`, qt.Commentf(got))
- got = string(toc.ToHTML(1, 2, false))
+ tocHTML, _ = toc.ToHTML(1, 2, false)
+ got = string(tocHTML)
c.Assert(got, qt.Equals, `
Heading 1
@@ -83,7 +86,8 @@ func TestToc(t *testing.T) {
`, qt.Commentf(got))
- got = string(toc.ToHTML(2, 2, false))
+ tocHTML, _ = toc.ToHTML(2, 2, false)
+ got = string(tocHTML)
c.Assert(got, qt.Equals, `
1-H2-1
@@ -91,7 +95,8 @@ func TestToc(t *testing.T) {
`, qt.Commentf(got))
- got = string(toc.ToHTML(1, -1, true))
+ tocHTML, _ = toc.ToHTML(1, -1, true)
+ got = string(tocHTML)
c.Assert(got, qt.Equals, `
Heading 1
@@ -118,7 +123,8 @@ func TestTocMissingParent(t *testing.T) {
toc.addAt(&Heading{Title: "H3", ID: "h3"}, 1, 2)
toc.addAt(&Heading{Title: "H3", ID: "h3"}, 1, 2)
- got := string(toc.ToHTML(1, -1, false))
+ tocHTML, _ := toc.ToHTML(1, -1, false)
+ got := string(tocHTML)
c.Assert(got, qt.Equals, `
@@ -139,7 +145,8 @@ func TestTocMissingParent(t *testing.T) {
`, qt.Commentf(got))
- got = string(toc.ToHTML(3, 3, false))
+ tocHTML, _ = toc.ToHTML(3, 3, false)
+ got = string(tocHTML)
c.Assert(got, qt.Equals, `
H3
@@ -147,7 +154,8 @@ func TestTocMissingParent(t *testing.T) {
`, qt.Commentf(got))
- got = string(toc.ToHTML(1, -1, true))
+ tocHTML, _ = toc.ToHTML(1, -1, true)
+ got = string(tocHTML)
c.Assert(got, qt.Equals, `
@@ -188,7 +196,7 @@ func TestTocMisc(t *testing.T) {
func BenchmarkToc(b *testing.B) {
newTocs := func(n int) []*Fragments {
var tocs []*Fragments
- for i := 0; i < n; i++ {
+ for range n {
tocs = append(tocs, newTestToc())
}
return tocs
diff --git a/media/builtin.go b/media/builtin.go
index aafe245c9..41d1ed655 100644
--- a/media/builtin.go
+++ b/media/builtin.go
@@ -5,6 +5,7 @@ type BuiltinTypes struct {
CSSType Type
SCSSType Type
SASSType Type
+ GotmplType Type
CSVType Type
HTMLType Type
JavascriptType Type
@@ -34,8 +35,12 @@ type BuiltinTypes struct {
OpenTypeFontType Type
// Common document types
- PDFType Type
- MarkdownType Type
+ PDFType Type
+ MarkdownType Type
+ EmacsOrgModeType Type
+ AsciiDocType Type
+ PandocType Type
+ ReStructuredTextType Type
// Common video types
AVIType Type
@@ -56,6 +61,7 @@ var Builtin = BuiltinTypes{
CSSType: Type{Type: "text/css"},
SCSSType: Type{Type: "text/x-scss"},
SASSType: Type{Type: "text/x-sass"},
+ GotmplType: Type{Type: "text/x-gotmpl"},
CSVType: Type{Type: "text/csv"},
HTMLType: Type{Type: "text/html"},
JavascriptType: Type{Type: "text/javascript"},
@@ -85,8 +91,12 @@ var Builtin = BuiltinTypes{
OpenTypeFontType: Type{Type: "font/otf"},
// Common document types
- PDFType: Type{Type: "application/pdf"},
- MarkdownType: Type{Type: "text/markdown"},
+ PDFType: Type{Type: "application/pdf"},
+ MarkdownType: Type{Type: "text/markdown"},
+ AsciiDocType: Type{Type: "text/asciidoc"}, // https://github.com/asciidoctor/asciidoctor/issues/2502
+ PandocType: Type{Type: "text/pandoc"},
+ ReStructuredTextType: Type{Type: "text/rst"}, // https://docutils.sourceforge.io/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data
+ EmacsOrgModeType: Type{Type: "text/org"},
// Common video types
AVIType: Type{Type: "video/x-msvideo"},
@@ -108,11 +118,12 @@ var defaultMediaTypesConfig = map[string]any{
"text/x-scss": map[string]any{"suffixes": []string{"scss"}},
"text/x-sass": map[string]any{"suffixes": []string{"sass"}},
"text/csv": map[string]any{"suffixes": []string{"csv"}},
- "text/html": map[string]any{"suffixes": []string{"html"}},
+ "text/html": map[string]any{"suffixes": []string{"html", "htm"}},
"text/javascript": map[string]any{"suffixes": []string{"js", "jsm", "mjs"}},
"text/typescript": map[string]any{"suffixes": []string{"ts"}},
"text/tsx": map[string]any{"suffixes": []string{"tsx"}},
"text/jsx": map[string]any{"suffixes": []string{"jsx"}},
+ "text/x-gotmpl": map[string]any{"suffixes": []string{"gotmpl"}},
"application/json": map[string]any{"suffixes": []string{"json"}},
"application/manifest+json": map[string]any{"suffixes": []string{"webmanifest"}},
@@ -137,7 +148,11 @@ var defaultMediaTypesConfig = map[string]any{
// Common document types
"application/pdf": map[string]any{"suffixes": []string{"pdf"}},
- "text/markdown": map[string]any{"suffixes": []string{"md", "markdown"}},
+ "text/markdown": map[string]any{"suffixes": []string{"md", "mdown", "markdown"}},
+ "text/asciidoc": map[string]any{"suffixes": []string{"adoc", "asciidoc", "ad"}},
+ "text/pandoc": map[string]any{"suffixes": []string{"pandoc", "pdc"}},
+ "text/rst": map[string]any{"suffixes": []string{"rst"}},
+ "text/org": map[string]any{"suffixes": []string{"org"}},
// Common video types
"video/x-msvideo": map[string]any{"suffixes": []string{"avi"}},
@@ -152,10 +167,3 @@ var defaultMediaTypesConfig = map[string]any{
"application/octet-stream": map[string]any{},
}
-
-func init() {
- // Apply delimiter to all.
- for _, m := range defaultMediaTypesConfig {
- m.(map[string]any)["delimiter"] = "."
- }
-}
diff --git a/media/config.go b/media/config.go
index cdec2e438..6d3687a4f 100644
--- a/media/config.go
+++ b/media/config.go
@@ -14,13 +14,15 @@
package media
import (
- "errors"
"fmt"
+ "path/filepath"
"reflect"
+ "slices"
"sort"
"strings"
"github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/config"
"github.com/mitchellh/mapstructure"
@@ -31,6 +33,11 @@ import (
var DefaultTypes Types
func init() {
+ // Apply delimiter to all.
+ for _, m := range defaultMediaTypesConfig {
+ m.(map[string]any)["delimiter"] = "."
+ }
+
ns, err := DecodeTypes(nil)
if err != nil {
panic(err)
@@ -39,17 +46,118 @@ func init() {
// Initialize the Builtin types with values from DefaultTypes.
v := reflect.ValueOf(&Builtin).Elem()
- for i := 0; i < v.NumField(); i++ {
+
+ for i := range v.NumField() {
f := v.Field(i)
+ fieldName := v.Type().Field(i).Name
builtinType := f.Interface().(Type)
+ if builtinType.Type == "" {
+ panic(fmt.Errorf("builtin type %q is empty", fieldName))
+ }
defaultType, found := DefaultTypes.GetByType(builtinType.Type)
if !found {
- panic(errors.New("missing default type for builtin type: " + builtinType.Type))
+ panic(fmt.Errorf("missing default type for field builtin type: %q", fieldName))
}
f.Set(reflect.ValueOf(defaultType))
}
}
+func init() {
+ DefaultContentTypes = ContentTypes{
+ HTML: Builtin.HTMLType,
+ Markdown: Builtin.MarkdownType,
+ AsciiDoc: Builtin.AsciiDocType,
+ Pandoc: Builtin.PandocType,
+ ReStructuredText: Builtin.ReStructuredTextType,
+ EmacsOrgMode: Builtin.EmacsOrgModeType,
+ }
+
+ DefaultContentTypes.init(nil)
+}
+
+var DefaultContentTypes ContentTypes
+
+type ContentTypeConfig struct {
+ // Empty for now.
+}
+
+// ContentTypes holds the media types that are considered content in Hugo.
+type ContentTypes struct {
+ HTML Type
+ Markdown Type
+ AsciiDoc Type
+ Pandoc Type
+ ReStructuredText Type
+ EmacsOrgMode Type
+
+ types Types
+
+ // Created in init().
+ extensionSet map[string]bool
+}
+
+func (t *ContentTypes) init(types Types) {
+ sort.Slice(t.types, func(i, j int) bool {
+ return t.types[i].Type < t.types[j].Type
+ })
+
+ if tt, ok := types.GetByType(t.HTML.Type); ok {
+ t.HTML = tt
+ }
+ if tt, ok := types.GetByType(t.Markdown.Type); ok {
+ t.Markdown = tt
+ }
+ if tt, ok := types.GetByType(t.AsciiDoc.Type); ok {
+ t.AsciiDoc = tt
+ }
+ if tt, ok := types.GetByType(t.Pandoc.Type); ok {
+ t.Pandoc = tt
+ }
+ if tt, ok := types.GetByType(t.ReStructuredText.Type); ok {
+ t.ReStructuredText = tt
+ }
+ if tt, ok := types.GetByType(t.EmacsOrgMode.Type); ok {
+ t.EmacsOrgMode = tt
+ }
+
+ t.extensionSet = make(map[string]bool)
+ for _, mt := range t.types {
+ for _, suffix := range mt.Suffixes() {
+ t.extensionSet[suffix] = true
+ }
+ }
+}
+
+func (t ContentTypes) IsContentSuffix(suffix string) bool {
+ return t.extensionSet[suffix]
+}
+
+// IsContentFile returns whether the given filename is a content file.
+func (t ContentTypes) IsContentFile(filename string) bool {
+ return t.IsContentSuffix(strings.TrimPrefix(filepath.Ext(filename), "."))
+}
+
+// IsIndexContentFile returns whether the given filename is an index content file.
+func (t ContentTypes) IsIndexContentFile(filename string) bool {
+ if !t.IsContentFile(filename) {
+ return false
+ }
+
+ base := filepath.Base(filename)
+
+ return strings.HasPrefix(base, "index.") || strings.HasPrefix(base, "_index.")
+}
+
+// IsHTMLSuffix returns whether the given suffix is a HTML media type.
+func (t ContentTypes) IsHTMLSuffix(suffix string) bool {
+ return slices.Contains(t.HTML.Suffixes(), suffix)
+}
+
+// Types is a slice of media types.
+func (t ContentTypes) Types() Types {
+ return t.types
+}
+
// Hold the configuration for a given media type.
type MediaTypeConfig struct {
// The file suffixes used for this media type.
@@ -58,6 +166,58 @@ type MediaTypeConfig struct {
Delimiter string
}
+var defaultContentTypesConfig = map[string]ContentTypeConfig{
+ Builtin.HTMLType.Type: {},
+ Builtin.MarkdownType.Type: {},
+ Builtin.AsciiDocType.Type: {},
+ Builtin.PandocType.Type: {},
+ Builtin.ReStructuredTextType.Type: {},
+ Builtin.EmacsOrgModeType.Type: {},
+}
+
+// DecodeContentTypes decodes the given map of content types.
+func DecodeContentTypes(in map[string]any, types Types) (*config.ConfigNamespace[map[string]ContentTypeConfig, ContentTypes], error) {
+ buildConfig := func(v any) (ContentTypes, any, error) {
+ var s map[string]ContentTypeConfig
+ c := DefaultContentTypes
+ m, err := maps.ToStringMapE(v)
+ if err != nil {
+ return c, nil, err
+ }
+ if len(m) == 0 {
+ s = defaultContentTypesConfig
+ } else {
+ s = make(map[string]ContentTypeConfig)
+ m = maps.CleanConfigStringMap(m)
+ for k, v := range m {
+ var ctc ContentTypeConfig
+ if err := mapstructure.WeakDecode(v, &ctc); err != nil {
+ return c, nil, err
+ }
+ s[k] = ctc
+ }
+ }
+
+ for k := range s {
+ mediaType, found := types.GetByType(k)
+ if !found {
+ return c, nil, fmt.Errorf("unknown media type %q", k)
+ }
+ c.types = append(c.types, mediaType)
+ }
+
+ c.init(types)
+
+ return c, s, nil
+ }
+
+ ns, err := config.DecodeNamespace[map[string]ContentTypeConfig](in, buildConfig)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode media types: %w", err)
+ }
+ return ns, nil
+}
+
// DecodeTypes decodes the given map of media types.
func DecodeTypes(in map[string]any) (*config.ConfigNamespace[map[string]MediaTypeConfig, Types], error) {
buildConfig := func(v any) (Types, any, error) {
@@ -83,7 +243,7 @@ func DecodeTypes(in map[string]any) (*config.ConfigNamespace[map[string]MediaTyp
return nil, nil, err
}
mm := maps.ToStringMap(v)
- suffixes, found := maps.LookupEqualFold(mm, "suffixes")
+ suffixes, _, found := maps.LookupEqualFold(mm, "suffixes")
if found {
mediaType.SuffixesCSV = strings.TrimSpace(strings.ToLower(strings.Join(cast.ToStringSlice(suffixes), ",")))
}
@@ -105,3 +265,13 @@ func DecodeTypes(in map[string]any) (*config.ConfigNamespace[map[string]MediaTyp
}
return ns, nil
}
+
+// TODO(bep) get rid of this.
+var DefaultPathParser = &paths.PathParser{
+ IsContentExt: func(ext string) bool {
+ panic("not supported")
+ },
+ IsOutputFormat: func(name, ext string) bool {
+ panic("DefaultPathParser: not supported")
+ },
+}
diff --git a/media/config_test.go b/media/config_test.go
index 4803eb42a..5abbcac2f 100644
--- a/media/config_test.go
+++ b/media/config_test.go
@@ -114,7 +114,7 @@ func TestDefaultTypes(t *testing.T) {
tp Type
expectedMainType string
expectedSubType string
- expectedSuffix string
+ expectedSuffixes string
expectedType string
expectedString string
}{
@@ -122,29 +122,34 @@ func TestDefaultTypes(t *testing.T) {
{Builtin.CSSType, "text", "css", "css", "text/css", "text/css"},
{Builtin.SCSSType, "text", "x-scss", "scss", "text/x-scss", "text/x-scss"},
{Builtin.CSVType, "text", "csv", "csv", "text/csv", "text/csv"},
- {Builtin.HTMLType, "text", "html", "html", "text/html", "text/html"},
- {Builtin.JavascriptType, "text", "javascript", "js", "text/javascript", "text/javascript"},
+ {Builtin.HTMLType, "text", "html", "html,htm", "text/html", "text/html"},
+ {Builtin.MarkdownType, "text", "markdown", "md,mdown,markdown", "text/markdown", "text/markdown"},
+ {Builtin.EmacsOrgModeType, "text", "org", "org", "text/org", "text/org"},
+ {Builtin.PandocType, "text", "pandoc", "pandoc,pdc", "text/pandoc", "text/pandoc"},
+ {Builtin.ReStructuredTextType, "text", "rst", "rst", "text/rst", "text/rst"},
+ {Builtin.AsciiDocType, "text", "asciidoc", "adoc,asciidoc,ad", "text/asciidoc", "text/asciidoc"},
+ {Builtin.JavascriptType, "text", "javascript", "js,jsm,mjs", "text/javascript", "text/javascript"},
{Builtin.TypeScriptType, "text", "typescript", "ts", "text/typescript", "text/typescript"},
{Builtin.TSXType, "text", "tsx", "tsx", "text/tsx", "text/tsx"},
{Builtin.JSXType, "text", "jsx", "jsx", "text/jsx", "text/jsx"},
{Builtin.JSONType, "application", "json", "json", "application/json", "application/json"},
- {Builtin.RSSType, "application", "rss", "xml", "application/rss+xml", "application/rss+xml"},
+ {Builtin.RSSType, "application", "rss", "xml,rss", "application/rss+xml", "application/rss+xml"},
{Builtin.SVGType, "image", "svg", "svg", "image/svg+xml", "image/svg+xml"},
{Builtin.TextType, "text", "plain", "txt", "text/plain", "text/plain"},
{Builtin.XMLType, "application", "xml", "xml", "application/xml", "application/xml"},
{Builtin.TOMLType, "application", "toml", "toml", "application/toml", "application/toml"},
- {Builtin.YAMLType, "application", "yaml", "yaml", "application/yaml", "application/yaml"},
+ {Builtin.YAMLType, "application", "yaml", "yaml,yml", "application/yaml", "application/yaml"},
{Builtin.PDFType, "application", "pdf", "pdf", "application/pdf", "application/pdf"},
{Builtin.TrueTypeFontType, "font", "ttf", "ttf", "font/ttf", "font/ttf"},
{Builtin.OpenTypeFontType, "font", "otf", "otf", "font/otf", "font/otf"},
} {
c.Assert(test.tp.MainType, qt.Equals, test.expectedMainType)
c.Assert(test.tp.SubType, qt.Equals, test.expectedSubType)
-
+ c.Assert(test.tp.SuffixesCSV, qt.Equals, test.expectedSuffixes)
c.Assert(test.tp.Type, qt.Equals, test.expectedType)
c.Assert(test.tp.String(), qt.Equals, test.expectedString)
}
- c.Assert(len(DefaultTypes), qt.Equals, 36)
+ c.Assert(len(DefaultTypes), qt.Equals, 41)
}
diff --git a/media/mediaType.go b/media/mediaType.go
index 367c8ecc9..b3b615444 100644
--- a/media/mediaType.go
+++ b/media/mediaType.go
@@ -117,13 +117,16 @@ func FromContent(types Types, extensionHints []string, content []byte) Type {
return m
}
-// FromStringAndExt creates a Type from a MIME string and a given extension.
-func FromStringAndExt(t, ext string) (Type, error) {
+// FromStringAndExt creates a Type from a MIME string and a given extensions
+func FromStringAndExt(t string, ext ...string) (Type, error) {
tp, err := FromString(t)
if err != nil {
return tp, err
}
- tp.SuffixesCSV = strings.TrimPrefix(ext, ".")
+ for i, e := range ext {
+ ext[i] = strings.TrimPrefix(e, ".")
+ }
+ tp.SuffixesCSV = strings.Join(ext, ",")
tp.Delimiter = DefaultDelimiter
tp.init()
return tp, nil
@@ -187,6 +190,16 @@ func (m Type) IsText() bool {
return false
}
+// For internal use.
+func (m Type) IsHTML() bool {
+ return m.SubType == Builtin.HTMLType.SubType
+}
+
+// For internal use.
+func (m Type) IsMarkdown() bool {
+ return m.SubType == Builtin.MarkdownType.SubType
+}
+
func InitMediaType(m *Type) {
m.init()
}
@@ -221,6 +234,26 @@ func (t Types) Len() int { return len(t) }
func (t Types) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t Types) Less(i, j int) bool { return t[i].Type < t[j].Type }
+// GetBestMatch returns the best match for the given media type string.
+func (t Types) GetBestMatch(s string) (Type, bool) {
+ // First try an exact match.
+ if mt, found := t.GetByType(s); found {
+ return mt, true
+ }
+
+ // Try main type.
+ if mt, found := t.GetBySubType(s); found {
+ return mt, true
+ }
+
+ // Try extension.
+ if mt, _, found := t.GetFirstBySuffix(s); found {
+ return mt, true
+ }
+
+ return Type{}, false
+}
+
// GetByType returns a media type for tp.
func (t Types) GetByType(tp string) (Type, bool) {
for _, tt := range t {
@@ -240,12 +273,16 @@ func (t Types) GetByType(tp string) (Type, bool) {
return Type{}, false
}
+func (t Types) normalizeSuffix(s string) string {
+ return strings.ToLower(strings.TrimPrefix(s, "."))
+}
+
// BySuffix will return all media types matching a suffix.
func (t Types) BySuffix(suffix string) []Type {
- suffix = strings.ToLower(suffix)
+ suffix = t.normalizeSuffix(suffix)
var types []Type
for _, tt := range t {
- if tt.hasSuffix(suffix) {
+ if tt.HasSuffix(suffix) {
types = append(types, tt)
}
}
@@ -254,9 +291,9 @@ func (t Types) BySuffix(suffix string) []Type {
// GetFirstBySuffix will return the first type matching the given suffix.
func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) {
- suffix = strings.ToLower(suffix)
+ suffix = t.normalizeSuffix(suffix)
for _, tt := range t {
- if tt.hasSuffix(suffix) {
+ if tt.HasSuffix(suffix) {
return tt, SuffixInfo{
FullSuffix: tt.Delimiter + suffix,
Suffix: suffix,
@@ -271,9 +308,9 @@ func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) {
// is ambiguous.
// The lookup is case insensitive.
func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) {
- suffix = strings.ToLower(suffix)
+ suffix = t.normalizeSuffix(suffix)
for _, tt := range t {
- if tt.hasSuffix(suffix) {
+ if tt.HasSuffix(suffix) {
if found {
// ambiguous
found = false
@@ -291,16 +328,16 @@ func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) {
}
func (t Types) IsTextSuffix(suffix string) bool {
- suffix = strings.ToLower(suffix)
+ suffix = t.normalizeSuffix(suffix)
for _, tt := range t {
- if tt.hasSuffix(suffix) {
+ if tt.HasSuffix(suffix) {
return tt.IsText()
}
}
return false
}
-func (m Type) hasSuffix(suffix string) bool {
+func (m Type) HasSuffix(suffix string) bool {
return strings.Contains(","+m.SuffixesCSV+",", ","+suffix+",")
}
@@ -324,6 +361,22 @@ func (t Types) GetByMainSubType(mainType, subType string) (tp Type, found bool)
return
}
+// GetBySubType gets a media type given a sub type e.g. "plain".
+func (t Types) GetBySubType(subType string) (tp Type, found bool) {
+ for _, tt := range t {
+ if strings.EqualFold(subType, tt.SubType) {
+ if found {
+ // ambiguous
+ found = false
+ return
+ }
+ tp = tt
+ found = true
+ }
+ }
+ return
+}
+
// IsZero reports whether this Type represents a zero value.
// For internal use.
func (m Type) IsZero() bool {
diff --git a/media/mediaType_test.go b/media/mediaType_test.go
index 2e3a4a914..3b8e099b8 100644
--- a/media/mediaType_test.go
+++ b/media/mediaType_test.go
@@ -115,10 +115,10 @@ func TestFromTypeString(t *testing.T) {
func TestFromStringAndExt(t *testing.T) {
c := qt.New(t)
- f, err := FromStringAndExt("text/html", "html")
+ f, err := FromStringAndExt("text/html", "html", "htm")
c.Assert(err, qt.IsNil)
c.Assert(f, qt.Equals, Builtin.HTMLType)
- f, err = FromStringAndExt("text/html", ".html")
+ f, err = FromStringAndExt("text/html", ".html", ".htm")
c.Assert(err, qt.IsNil)
c.Assert(f, qt.Equals, Builtin.HTMLType)
}
diff --git a/metrics/metrics.go b/metrics/metrics.go
index 9715a3747..08d9322ab 100644
--- a/metrics/metrics.go
+++ b/metrics/metrics.go
@@ -25,9 +25,9 @@ import (
"sync"
"time"
+ "github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/compare"
- "github.com/gohugoio/hugo/identity"
)
// The Provider interface defines an interface for measuring metrics.
@@ -168,22 +168,21 @@ func (s *Store) WriteMetrics(w io.Writer) {
s.mu.Unlock()
if s.calculateHints {
- fmt.Fprintf(w, " %13s %12s %12s %9s %7s %6s %5s %s\n", "cumulative", "average", "maximum", "cache", "percent", "cached", "total", "")
- fmt.Fprintf(w, " %13s %12s %12s %9s %7s %6s %5s %s\n", "duration", "duration", "duration", "potential", "cached", "count", "count", "template")
- fmt.Fprintf(w, " %13s %12s %12s %9s %7s %6s %5s %s\n", "----------", "--------", "--------", "---------", "-------", "------", "-----", "--------")
+ fmt.Fprintf(w, " %15s %12s %12s %9s %7s %6s %5s %s\n", "cumulative", "average", "maximum", "cache", "percent", "cached", "total", "")
+ fmt.Fprintf(w, " %15s %12s %12s %9s %7s %6s %5s %s\n", "duration", "duration", "duration", "potential", "cached", "count", "count", "template")
+ fmt.Fprintf(w, " %15s %12s %12s %9s %7s %6s %5s %s\n", "----------", "--------", "--------", "---------", "-------", "------", "-----", "--------")
} else {
- fmt.Fprintf(w, " %13s %12s %12s %5s %s\n", "cumulative", "average", "maximum", "", "")
- fmt.Fprintf(w, " %13s %12s %12s %5s %s\n", "duration", "duration", "duration", "count", "template")
- fmt.Fprintf(w, " %13s %12s %12s %5s %s\n", "----------", "--------", "--------", "-----", "--------")
-
+ fmt.Fprintf(w, " %15s %12s %12s %5s %s\n", "cumulative", "average", "maximum", "", "")
+ fmt.Fprintf(w, " %15s %12s %12s %5s %s\n", "duration", "duration", "duration", "count", "template")
+ fmt.Fprintf(w, " %15s %12s %12s %5s %s\n", "----------", "--------", "--------", "-----", "--------")
}
sort.Sort(bySum(results))
for _, v := range results {
if s.calculateHints {
- fmt.Fprintf(w, " %13s %12s %12s %9d %7.f %6d %5d %s\n", v.sum, v.avg, v.max, v.cacheFactor, float64(v.cacheCount)/float64(v.count)*100, v.cacheCount, v.count, v.key)
+ fmt.Fprintf(w, " %15s %12s %12s %9d %7.f %6d %5d %s\n", v.sum, v.avg, v.max, v.cacheFactor, float64(v.cacheCount)/float64(v.count)*100, v.cacheCount, v.count, v.key)
} else {
- fmt.Fprintf(w, " %13s %12s %12s %5d %s\n", v.sum, v.avg, v.max, v.count, v.key)
+ fmt.Fprintf(w, " %15s %12s %12s %5d %s\n", v.sum, v.avg, v.max, v.count, v.key)
}
}
}
@@ -242,7 +241,7 @@ func howSimilar(a, b any) int {
return 90
}
- h1, h2 := identity.HashString(a), identity.HashString(b)
+ h1, h2 := hashing.HashString(a), hashing.HashString(b)
if h1 == h2 {
return 100
}
diff --git a/minifiers/minifiers_test.go b/minifiers/minifiers_test.go
index bfd743fae..b81c632fe 100644
--- a/minifiers/minifiers_test.go
+++ b/minifiers/minifiers_test.go
@@ -153,10 +153,12 @@ func TestBugs(t *testing.T) {
rawString string
expectedMinString string
}{
- // https://github.com/gohugoio/hugo/issues/5506
+ // Issue 5506
{media.Builtin.CSSType, " body { color: rgba(000, 000, 000, 0.7); }", "body{color:rgba(0,0,0,.7)}"},
- // https://github.com/gohugoio/hugo/issues/8332
+ // Issue 8332
{media.Builtin.HTMLType, " Tags", ` Tags`},
+ // Issue #13082
+ {media.Builtin.HTMLType, " ", ` `},
} {
var b bytes.Buffer
diff --git a/modules/client.go b/modules/client.go
index f358f3f75..a8998bb8d 100644
--- a/modules/client.go
+++ b/modules/client.go
@@ -32,6 +32,7 @@ import (
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/config"
hglob "github.com/gohugoio/hugo/hugofs/glob"
@@ -41,8 +42,6 @@ import (
"github.com/gohugoio/hugo/hugofs/files"
- "github.com/gohugoio/hugo/config"
-
"golang.org/x/mod/module"
"github.com/gohugoio/hugo/common/hugio"
@@ -79,21 +78,6 @@ func NewClient(cfg ClientConfig) *Client {
goModFilename = n
}
- var env []string
- mcfg := cfg.ModuleConfig
-
- config.SetEnvVars(&env,
- "PWD", cfg.WorkingDir,
- "GO111MODULE", "on",
- "GOPROXY", mcfg.Proxy,
- "GOPRIVATE", mcfg.Private,
- "GONOPROXY", mcfg.NoProxy,
- "GOPATH", cfg.CacheDir,
- "GOWORK", mcfg.Workspace, // Requires Go 1.18, see https://tip.golang.org/doc/go1.18
- // GOCACHE was introduced in Go 1.15. This matches the location derived from GOPATH above.
- "GOCACHE", filepath.Join(cfg.CacheDir, "pkg", "mod"),
- )
-
logger := cfg.Logger
if logger == nil {
logger = loggers.NewDefault()
@@ -109,8 +93,8 @@ func NewClient(cfg ClientConfig) *Client {
ccfg: cfg,
logger: logger,
noVendor: noVendor,
- moduleConfig: mcfg,
- environ: env,
+ moduleConfig: cfg.ModuleConfig,
+ environ: cfg.toEnv(),
GoModulesFilename: goModFilename,
}
}
@@ -365,19 +349,7 @@ func (c *Client) Get(args ...string) error {
}
func (c *Client) get(args ...string) error {
- var hasD bool
- for _, arg := range args {
- if arg == "-d" {
- hasD = true
- break
- }
- }
- if !hasD {
- // go get without the -d flag does not make sense to us, as
- // it will try to build and install go packages.
- args = append([]string{"-d"}, args...)
- }
- if err := c.runGo(context.Background(), c.logger.Out(), append([]string{"get"}, args...)...); err != nil {
+ if err := c.runGo(context.Background(), c.logger.StdOut(), append([]string{"get"}, args...)...); err != nil {
return fmt.Errorf("failed to get %q: %w", args, err)
}
return nil
@@ -387,7 +359,7 @@ func (c *Client) get(args ...string) error {
// If path is empty, Go will try to guess.
// If this succeeds, this project will be marked as Go Module.
func (c *Client) Init(path string) error {
- err := c.runGo(context.Background(), c.logger.Out(), "mod", "init", path)
+ err := c.runGo(context.Background(), c.logger.StdOut(), "mod", "init", path)
if err != nil {
return fmt.Errorf("failed to init modules: %w", err)
}
@@ -408,14 +380,12 @@ func (c *Client) Verify(clean bool) error {
if err != nil {
if clean {
m := verifyErrorDirRe.FindAllStringSubmatch(err.Error(), -1)
- if m != nil {
- for i := 0; i < len(m); i++ {
- c, err := hugofs.MakeReadableAndRemoveAllModulePkgDir(c.fs, m[i][1])
- if err != nil {
- return err
- }
- fmt.Println("Cleaned", c)
+ for i := range m {
+ c, err := hugofs.MakeReadableAndRemoveAllModulePkgDir(c.fs, m[i][1])
+ if err != nil {
+ return err
}
+ fmt.Println("Cleaned", c)
}
// Try to verify it again.
err = c.runVerify()
@@ -645,7 +615,7 @@ func (c *Client) runGo(
argsv := collections.StringSliceToInterfaceSlice(args)
argsv = append(argsv, hexec.WithEnviron(c.environ))
- argsv = append(argsv, hexec.WithStderr(io.MultiWriter(stderr, os.Stderr)))
+ argsv = append(argsv, hexec.WithStderr(goOutputReplacerWriter{w: io.MultiWriter(stderr, os.Stderr)}))
argsv = append(argsv, hexec.WithStdout(stdout))
argsv = append(argsv, hexec.WithDir(c.ccfg.WorkingDir))
argsv = append(argsv, hexec.WithContext(ctx))
@@ -691,6 +661,24 @@ If you then run 'hugo mod graph' it should resolve itself to the most recent ver
return nil
}
+var goOutputReplacer = strings.NewReplacer(
+ "go: to add module requirements and sums:", "hugo: to add module requirements and sums:",
+ "go mod tidy", "hugo mod tidy",
+)
+
+type goOutputReplacerWriter struct {
+ w io.Writer
+}
+
+func (w goOutputReplacerWriter) Write(p []byte) (n int, err error) {
+ s := goOutputReplacer.Replace(string(p))
+ _, err = w.w.Write([]byte(s))
+ if err != nil {
+ return 0, err
+ }
+ return len(p), nil
+}
+
func (c *Client) tidy(mods Modules, goModOnly bool) error {
isGoMod := make(map[string]bool)
for _, m := range mods {
@@ -754,12 +742,18 @@ type ClientConfig struct {
// This can be nil.
IgnoreVendor glob.Glob
+ // Ignore any module not found errors.
+ IgnoreModuleDoesNotExist bool
+
// Absolute path to the project dir.
WorkingDir string
// Absolute path to the project's themes dir.
ThemesDir string
+ // The publish dir.
+ PublishDir string
+
// Eg. "production"
Environment string
@@ -773,6 +767,37 @@ func (c ClientConfig) shouldIgnoreVendor(path string) bool {
return c.IgnoreVendor != nil && c.IgnoreVendor.Match(path)
}
+func (cfg ClientConfig) toEnv() []string {
+ mcfg := cfg.ModuleConfig
+ var env []string
+ keyVals := []string{
+ "PWD", cfg.WorkingDir,
+ "GO111MODULE", "on",
+ "GOPATH", cfg.CacheDir,
+ "GOWORK", mcfg.Workspace, // Requires Go 1.18, see https://tip.golang.org/doc/go1.18
+ // GOCACHE was introduced in Go 1.15. This matches the location derived from GOPATH above.
+ "GOCACHE", filepath.Join(cfg.CacheDir, "pkg", "mod"),
+ }
+
+ if mcfg.Proxy != "" {
+ keyVals = append(keyVals, "GOPROXY", mcfg.Proxy)
+ }
+ if mcfg.Private != "" {
+ keyVals = append(keyVals, "GOPRIVATE", mcfg.Private)
+ }
+ if mcfg.NoProxy != "" {
+ keyVals = append(keyVals, "GONOPROXY", mcfg.NoProxy)
+ }
+ if mcfg.Auth != "" {
+ // GOAUTH was introduced in Go 1.24, see https://tip.golang.org/doc/go1.24.
+ keyVals = append(keyVals, "GOAUTH", mcfg.Auth)
+ }
+
+ config.SetEnvVars(&env, keyVals...)
+
+ return env
+}
+
type goBinaryStatus int
type goModule struct {
diff --git a/modules/client_test.go b/modules/client_test.go
index ea910580f..1b4b1161a 100644
--- a/modules/client_test.go
+++ b/modules/client_test.go
@@ -22,6 +22,7 @@ import (
"testing"
"github.com/gohugoio/hugo/common/hexec"
+ "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config/security"
"github.com/gohugoio/hugo/hugofs/glob"
@@ -51,13 +52,17 @@ github.com/gohugoio/hugoTestModules1_darwin/modh2_2@v1.4.0 github.com/gohugoio/h
themesDir := filepath.Join(workingDir, "themes")
err = os.Mkdir(themesDir, 0o777)
c.Assert(err, qt.IsNil)
+ publishDir := filepath.Join(workingDir, "public")
+ err = os.Mkdir(publishDir, 0o777)
+ c.Assert(err, qt.IsNil)
ccfg := ClientConfig{
Fs: hugofs.Os,
- WorkingDir: workingDir,
CacheDir: filepath.Join(workingDir, "modcache"),
+ WorkingDir: workingDir,
ThemesDir: themesDir,
- Exec: hexec.New(security.DefaultConfig),
+ PublishDir: publishDir,
+ Exec: hexec.New(security.DefaultConfig, "", loggers.NewDefault()),
}
withConfig(&ccfg)
@@ -211,3 +216,42 @@ func TestGetModlineSplitter(t *testing.T) {
gosumSplitter := getModlineSplitter(false)
c.Assert(gosumSplitter("github.com/BurntSushi/toml v0.3.1"), qt.DeepEquals, []string{"github.com/BurntSushi/toml", "v0.3.1"})
}
+
+func TestClientConfigToEnv(t *testing.T) {
+ c := qt.New(t)
+
+ ccfg := ClientConfig{
+ WorkingDir: "/mywork",
+ CacheDir: "/mycache",
+ }
+
+ env := ccfg.toEnv()
+
+ c.Assert(env, qt.DeepEquals, []string{"PWD=/mywork", "GO111MODULE=on", "GOPATH=/mycache", "GOWORK=", filepath.FromSlash("GOCACHE=/mycache/pkg/mod")})
+
+ ccfg = ClientConfig{
+ WorkingDir: "/mywork",
+ CacheDir: "/mycache",
+ ModuleConfig: Config{
+ Proxy: "https://proxy.example.org",
+ Private: "myprivate",
+ NoProxy: "mynoproxy",
+ Workspace: "myworkspace",
+ Auth: "myauth",
+ },
+ }
+
+ env = ccfg.toEnv()
+
+ c.Assert(env, qt.DeepEquals, []string{
+ "PWD=/mywork",
+ "GO111MODULE=on",
+ "GOPATH=/mycache",
+ "GOWORK=myworkspace",
+ filepath.FromSlash("GOCACHE=/mycache/pkg/mod"),
+ "GOPROXY=https://proxy.example.org",
+ "GOPRIVATE=myprivate",
+ "GONOPROXY=mynoproxy",
+ "GOAUTH=myauth",
+ })
+}
diff --git a/modules/collect.go b/modules/collect.go
index dff71924b..7034a6b16 100644
--- a/modules/collect.go
+++ b/modules/collect.go
@@ -27,6 +27,7 @@ import (
"github.com/bep/debounce"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/common/paths"
"github.com/spf13/cast"
@@ -555,6 +556,9 @@ func (c *collector) collectModulesTXT(owner Module) error {
line := scanner.Text()
line = strings.Trim(line, "# ")
line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
parts := strings.Fields(line)
if len(parts) != 2 {
return fmt.Errorf("invalid modules list: %q", filename)
@@ -657,7 +661,13 @@ func (c *collector) normalizeMounts(owner *moduleAdapter, mounts []Mount) ([]Mou
// Verify that Source exists
_, err := c.fs.Stat(sourceDir)
if err != nil {
- if strings.HasSuffix(sourceDir, files.FilenameHugoStatsJSON) {
+ if paths.IsSameFilePath(sourceDir, c.ccfg.PublishDir) {
+ // This is a little exotic, but there are use cases for mounting the public folder.
+ // This will typically also be in .gitingore, so create it.
+ if err := c.fs.MkdirAll(sourceDir, 0o755); err != nil {
+ return nil, fmt.Errorf("%s: %q", errMsg, err)
+ }
+ } else if strings.HasSuffix(sourceDir, files.FilenameHugoStatsJSON) {
// A common pattern for Tailwind 3 is to mount that file to get it on the server watch list.
// A common pattern is also to add hugo_stats.json to .gitignore.
@@ -669,6 +679,8 @@ func (c *collector) normalizeMounts(owner *moduleAdapter, mounts []Mount) ([]Mou
}
f.Close()
} else {
+ // TODO(bep) commenting out for now, as this will create to much noise.
+ // c.logger.Warnf("module %q: mount source %q does not exist", owner.Path(), sourceDir)
continue
}
}
@@ -690,6 +702,9 @@ func (c *collector) normalizeMounts(owner *moduleAdapter, mounts []Mount) ([]Mou
}
func (c *collector) wrapModuleNotFound(err error) error {
+ if c.Client.ccfg.IgnoreModuleDoesNotExist {
+ return nil
+ }
err = fmt.Errorf(err.Error()+": %w", ErrNotExist)
if c.GoModulesFilename == "" {
return err
diff --git a/modules/config.go b/modules/config.go
index 2f1168d3a..1a833b301 100644
--- a/modules/config.go
+++ b/modules/config.go
@@ -295,6 +295,12 @@ type Config struct {
// Configures GOPRIVATE when running the Go command for module operations.
Private string
+ // Configures GOAUTH when running the Go command for module operations.
+ // This is a semicolon-separated list of authentication commands for go-import and HTTPS module mirror interactions.
+ // This is useful for private repositories.
+ // See `go help goauth` for more information.
+ Auth string
+
// Defaults to "off".
// Set to a work file, e.g. hugo.work, to enable Go "Workspace" mode.
// Can be relative to the working directory or absolute.
@@ -402,6 +408,9 @@ type Mount struct {
// Exclude all files matching the given Glob patterns (string or slice).
ExcludeFiles any
+
+ // Disable watching in watch mode for this mount.
+ DisableWatch bool
}
// Used as key to remove duplicates.
diff --git a/navigation/menu.go b/navigation/menu.go
index 3802014b1..a971f2e74 100644
--- a/navigation/menu.go
+++ b/navigation/menu.go
@@ -25,6 +25,7 @@ import (
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
+ "slices"
)
var smc = newMenuCache()
@@ -267,7 +268,7 @@ func (m Menu) Reverse() Menu {
// Clone clones the menu entries.
// This is for internal use only.
func (m Menu) Clone() Menu {
- return append(Menu(nil), m...)
+ return slices.Clone(m)
}
func DecodeConfig(in any) (*config.ConfigNamespace[map[string]MenuConfig, Menus], error) {
diff --git a/navigation/menu_cache.go b/navigation/menu_cache.go
index b2c46f7ac..065781780 100644
--- a/navigation/menu_cache.go
+++ b/navigation/menu_cache.go
@@ -14,6 +14,7 @@
package navigation
import (
+ "slices"
"sync"
)
@@ -84,7 +85,7 @@ func (c *menuCache) getP(key string, apply func(m *Menu), menuLists ...Menu) (Me
}
m := menuLists[0]
- menuCopy := append(Menu(nil), m...)
+ menuCopy := slices.Clone(m)
if apply != nil {
apply(&menuCopy)
diff --git a/navigation/menu_cache_test.go b/navigation/menu_cache_test.go
index 9943db517..8fa17ffc3 100644
--- a/navigation/menu_cache_test.go
+++ b/navigation/menu_cache_test.go
@@ -23,7 +23,7 @@ import (
func createSortTestMenu(num int) Menu {
menu := make(Menu, num)
- for i := 0; i < num; i++ {
+ for i := range num {
m := &MenuEntry{}
menu[i] = m
}
@@ -49,11 +49,11 @@ func TestMenuCache(t *testing.T) {
var testMenuSets []Menu
- for i := 0; i < 50; i++ {
+ for i := range 50 {
testMenuSets = append(testMenuSets, createSortTestMenu(i+1))
}
- for j := 0; j < 100; j++ {
+ for range 100 {
wg.Add(1)
go func() {
defer wg.Done()
diff --git a/navigation/pagemenus.go b/navigation/pagemenus.go
index 3ff49cf81..0919d93bb 100644
--- a/navigation/pagemenus.go
+++ b/navigation/pagemenus.go
@@ -41,20 +41,11 @@ type MenuQueryProvider interface {
IsMenuCurrent(menuID string, inme *MenuEntry) bool
}
-func PageMenusFromPage(p Page) (PageMenus, error) {
- params := p.Params()
-
- ms, ok := params["menus"]
- if !ok {
- ms, ok = params["menu"]
- }
-
- pm := PageMenus{}
-
- if !ok {
+func PageMenusFromPage(ms any, p Page) (PageMenus, error) {
+ if ms == nil {
return nil, nil
}
-
+ pm := PageMenus{}
me := MenuEntry{}
SetPageValues(&me, p)
diff --git a/output/docshelper.go b/output/docshelper.go
index f7f3b0c4a..387c011ad 100644
--- a/output/docshelper.go
+++ b/output/docshelper.go
@@ -1,12 +1,10 @@
package output
import (
- "strings"
// "fmt"
"github.com/gohugoio/hugo/docshelper"
- "github.com/gohugoio/hugo/output/layouts"
)
// This is is just some helpers used to create some JSON used in the Hugo docs.
@@ -14,90 +12,12 @@ func init() {
docsProvider := func() docshelper.DocProvider {
return docshelper.DocProvider{
"output": map[string]any{
- "layouts": createLayoutExamples(),
+ // TODO(bep), maybe revisit this later, but I hope this isn't needed.
+ // "layouts": createLayoutExamples(),
+ "layouts": map[string]any{},
},
}
}
docshelper.AddDocProviderFunc(docsProvider)
}
-
-func createLayoutExamples() any {
- type Example struct {
- Example string
- Kind string
- OutputFormat string
- Suffix string
- Layouts []string `json:"Template Lookup Order"`
- }
-
- var (
- basicExamples []Example
- demoLayout = "demolayout"
- demoType = "demotype"
- )
-
- for _, example := range []struct {
- name string
- d layouts.LayoutDescriptor
- }{
- // Taxonomy layouts.LayoutDescriptor={categories category taxonomy en false Type Section
- {"Single page in \"posts\" section", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "html", Suffix: "html"}},
- {"Base template for single page in \"posts\" section", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", OutputFormatName: "html", Suffix: "html"}},
- {"Single page in \"posts\" section with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
- {"Base template for single page in \"posts\" section with layout set to \"demolayout\"", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
- {"AMP single page", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "amp", Suffix: "html"}},
- {"AMP single page, French language", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Lang: "fr", OutputFormatName: "html", Suffix: "html"}},
- // Typeless pages get "page" as type
- {"Home page", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}},
- {"Base template for home page", layouts.LayoutDescriptor{Baseof: true, Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}},
- {"Home page with type set to \"demotype\"", layouts.LayoutDescriptor{Kind: "home", Type: demoType, OutputFormatName: "html", Suffix: "html"}},
- {"Base template for home page with type set to \"demotype\"", layouts.LayoutDescriptor{Baseof: true, Kind: "home", Type: demoType, OutputFormatName: "html", Suffix: "html"}},
- {"Home page with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "home", Type: "page", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
- {"AMP home, French language", layouts.LayoutDescriptor{Kind: "home", Type: "page", Lang: "fr", OutputFormatName: "amp", Suffix: "html"}},
- {"JSON home", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "json", Suffix: "json"}},
- {"RSS home", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "rss", Suffix: "xml"}},
-
- {"Section list for \"posts\"", layouts.LayoutDescriptor{Kind: "section", Type: "posts", Section: "posts", OutputFormatName: "html", Suffix: "html"}},
- {"Section list for \"posts\" with type set to \"blog\"", layouts.LayoutDescriptor{Kind: "section", Type: "blog", Section: "posts", OutputFormatName: "html", Suffix: "html"}},
- {"Section list for \"posts\" with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "section", Layout: demoLayout, Section: "posts", OutputFormatName: "html", Suffix: "html"}},
- {"Section list for \"posts\"", layouts.LayoutDescriptor{Kind: "section", Type: "posts", OutputFormatName: "rss", Suffix: "xml"}},
-
- {"Taxonomy list for \"categories\"", layouts.LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category", OutputFormatName: "html", Suffix: "html"}},
- {"Taxonomy list for \"categories\"", layouts.LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category", OutputFormatName: "rss", Suffix: "xml"}},
-
- {"Term list for \"categories\"", layouts.LayoutDescriptor{Kind: "term", Type: "categories", Section: "category", OutputFormatName: "html", Suffix: "html"}},
- {"Term list for \"categories\"", layouts.LayoutDescriptor{Kind: "term", Type: "categories", Section: "category", OutputFormatName: "rss", Suffix: "xml"}},
- } {
-
- l := layouts.NewLayoutHandler()
- layouts, _ := l.For(example.d)
-
- basicExamples = append(basicExamples, Example{
- Example: example.name,
- Kind: example.d.Kind,
- OutputFormat: example.d.OutputFormatName,
- Suffix: example.d.Suffix,
- Layouts: makeLayoutsPresentable(layouts),
- })
- }
-
- return basicExamples
-}
-
-func makeLayoutsPresentable(l []string) []string {
- var filtered []string
- for _, ll := range l {
- if strings.Contains(ll, "page/") {
- // This is a valid lookup, but it's more confusing than useful.
- continue
- }
- ll = "layouts/" + strings.TrimPrefix(ll, "_text/")
-
- if !strings.Contains(ll, "indexes") {
- filtered = append(filtered, ll)
- }
- }
-
- return filtered
-}
diff --git a/output/layouts/layout.go b/output/layouts/layout.go
deleted file mode 100644
index 09606dba1..000000000
--- a/output/layouts/layout.go
+++ /dev/null
@@ -1,336 +0,0 @@
-// Copyright 2024 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package layouts
-
-import (
- "strings"
- "sync"
-)
-
-// These may be used as content sections with potential conflicts. Avoid that.
-var reservedSections = map[string]bool{
- "shortcodes": true,
- "partials": true,
-}
-
-// LayoutDescriptor describes how a layout should be chosen. This is
-// typically built from a Page.
-type LayoutDescriptor struct {
- Type string
- Section string
-
- // E.g. "page", but also used for the _markup render kinds, e.g. "render-image".
- Kind string
-
- // Comma-separated list of kind variants, e.g. "go,json" as variants which would find "render-codeblock-go.html"
- KindVariants string
-
- Lang string
- Layout string
- // LayoutOverride indicates what we should only look for the above layout.
- LayoutOverride bool
-
- // From OutputFormat and MediaType.
- OutputFormatName string
- Suffix string
-
- RenderingHook bool
- Baseof bool
-}
-
-func (d LayoutDescriptor) isList() bool {
- return !d.RenderingHook && d.Kind != "page" && d.Kind != "404" && d.Kind != "sitemap" && d.Kind != "sitemapindex"
-}
-
-// LayoutHandler calculates the layout template to use to render a given output type.
-type LayoutHandler struct {
- mu sync.RWMutex
- cache map[LayoutDescriptor][]string
-}
-
-// NewLayoutHandler creates a new LayoutHandler.
-func NewLayoutHandler() *LayoutHandler {
- return &LayoutHandler{cache: make(map[LayoutDescriptor][]string)}
-}
-
-// For returns a layout for the given LayoutDescriptor and options.
-// Layouts are rendered and cached internally.
-func (l *LayoutHandler) For(d LayoutDescriptor) ([]string, error) {
- // We will get lots of requests for the same layouts, so avoid recalculations.
- l.mu.RLock()
- if cacheVal, found := l.cache[d]; found {
- l.mu.RUnlock()
- return cacheVal, nil
- }
- l.mu.RUnlock()
-
- layouts := resolvePageTemplate(d)
-
- layouts = uniqueStringsReuse(layouts)
-
- l.mu.Lock()
- l.cache[d] = layouts
- l.mu.Unlock()
-
- return layouts, nil
-}
-
-type layoutBuilder struct {
- layoutVariations []string
- typeVariations []string
- d LayoutDescriptor
- // f Format
-}
-
-func (l *layoutBuilder) addLayoutVariations(vars ...string) {
- for _, layoutVar := range vars {
- if l.d.Baseof && layoutVar != "baseof" {
- l.layoutVariations = append(l.layoutVariations, layoutVar+"-baseof")
- continue
- }
- if !l.d.RenderingHook && !l.d.Baseof && l.d.LayoutOverride && layoutVar != l.d.Layout {
- continue
- }
- l.layoutVariations = append(l.layoutVariations, layoutVar)
- }
-}
-
-func (l *layoutBuilder) addTypeVariations(vars ...string) {
- for _, typeVar := range vars {
- if !reservedSections[typeVar] {
- if l.d.RenderingHook {
- typeVar = typeVar + renderingHookRoot
- }
- l.typeVariations = append(l.typeVariations, typeVar)
- }
- }
-}
-
-func (l *layoutBuilder) addSectionType() {
- if l.d.Section != "" {
- l.addTypeVariations(l.d.Section)
- }
-}
-
-func (l *layoutBuilder) addKind() {
- l.addLayoutVariations(l.d.Kind)
- l.addTypeVariations(l.d.Kind)
-}
-
-const renderingHookRoot = "/_markup"
-
-func resolvePageTemplate(d LayoutDescriptor) []string {
- b := &layoutBuilder{d: d}
-
- if !d.RenderingHook && d.Layout != "" {
- b.addLayoutVariations(d.Layout)
- }
- if d.Type != "" {
- b.addTypeVariations(d.Type)
- }
-
- if d.RenderingHook {
- if d.KindVariants != "" {
- // Add the more specific variants first.
- for _, variant := range strings.Split(d.KindVariants, ",") {
- b.addLayoutVariations(d.Kind + "-" + variant)
- }
- }
- b.addLayoutVariations(d.Kind)
- b.addSectionType()
- }
-
- switch d.Kind {
- case "page":
- b.addLayoutVariations("single")
- b.addSectionType()
- case "home":
- b.addLayoutVariations("index", "home")
- // Also look in the root
- b.addTypeVariations("")
- case "section":
- if d.Section != "" {
- b.addLayoutVariations(d.Section)
- }
- b.addSectionType()
- b.addKind()
- case "term":
- b.addKind()
- if d.Section != "" {
- b.addLayoutVariations(d.Section)
- }
- b.addLayoutVariations("taxonomy")
- b.addTypeVariations("taxonomy")
- b.addSectionType()
- case "taxonomy":
- if d.Section != "" {
- b.addLayoutVariations(d.Section + ".terms")
- }
- b.addSectionType()
- b.addLayoutVariations("terms")
- // For legacy reasons this is deliberately put last.
- b.addKind()
- case "404":
- b.addLayoutVariations("404")
- b.addTypeVariations("")
- case "robotstxt":
- b.addLayoutVariations("robots")
- b.addTypeVariations("")
- case "sitemap":
- b.addLayoutVariations("sitemap")
- b.addTypeVariations("")
- case "sitemapindex":
- b.addLayoutVariations("sitemapindex")
- b.addTypeVariations("")
- }
-
- isRSS := d.OutputFormatName == "rss"
- if !d.RenderingHook && !d.Baseof && isRSS {
- // The historic and common rss.xml case
- b.addLayoutVariations("")
- }
-
- if d.Baseof || d.Kind != "404" {
- // Most have _default in their lookup path
- b.addTypeVariations("_default")
- }
-
- if d.isList() {
- // Add the common list type
- b.addLayoutVariations("list")
- }
-
- if d.Baseof {
- b.addLayoutVariations("baseof")
- }
-
- layouts := b.resolveVariations()
-
- if !d.RenderingHook && !d.Baseof && isRSS {
- layouts = append(layouts, "_internal/_default/rss.xml")
- }
-
- switch d.Kind {
- case "robotstxt":
- layouts = append(layouts, "_internal/_default/robots.txt")
- case "sitemap":
- layouts = append(layouts, "_internal/_default/sitemap.xml")
- case "sitemapindex":
- layouts = append(layouts, "_internal/_default/sitemapindex.xml")
- }
-
- return layouts
-}
-
-func (l *layoutBuilder) resolveVariations() []string {
- var layouts []string
-
- var variations []string
- name := strings.ToLower(l.d.OutputFormatName)
-
- if l.d.Lang != "" {
- // We prefer the most specific type before language.
- variations = append(variations, []string{l.d.Lang + "." + name, name, l.d.Lang}...)
- } else {
- variations = append(variations, name)
- }
-
- variations = append(variations, "")
-
- for _, typeVar := range l.typeVariations {
- for _, variation := range variations {
- for _, layoutVar := range l.layoutVariations {
- if variation == "" && layoutVar == "" {
- continue
- }
-
- s := constructLayoutPath(typeVar, layoutVar, variation, l.d.Suffix)
- if s != "" {
- layouts = append(layouts, s)
- }
- }
- }
- }
-
- return layouts
-}
-
-// constructLayoutPath constructs a layout path given a type, layout,
-// variations, and extension. The path constructed follows the pattern of
-// type/layout.variations.extension. If any value is empty, it will be left out
-// of the path construction.
-//
-// Path construction requires at least 2 of 3 out of layout, variations, and extension.
-// If more than one of those is empty, an empty string is returned.
-func constructLayoutPath(typ, layout, variations, extension string) string {
- // we already know that layout and variations are not both empty because of
- // checks in resolveVariants().
- if extension == "" && (layout == "" || variations == "") {
- return ""
- }
-
- // Commence valid path construction...
-
- var (
- p strings.Builder
- needDot bool
- )
-
- if typ != "" {
- p.WriteString(typ)
- p.WriteString("/")
- }
-
- if layout != "" {
- p.WriteString(layout)
- needDot = true
- }
-
- if variations != "" {
- if needDot {
- p.WriteString(".")
- }
- p.WriteString(variations)
- needDot = true
- }
-
- if extension != "" {
- if needDot {
- p.WriteString(".")
- }
- p.WriteString(extension)
- }
-
- return p.String()
-}
-
-// Inline this here so we can use tinygo to compile a wasm binary of this package.
-func uniqueStringsReuse(s []string) []string {
- result := s[:0]
- for i, val := range s {
- var seen bool
-
- for j := 0; j < i; j++ {
- if s[j] == val {
- seen = true
- break
- }
- }
-
- if !seen {
- result = append(result, val)
- }
- }
- return result
-}
diff --git a/output/layouts/layout_test.go b/output/layouts/layout_test.go
deleted file mode 100644
index b6033247c..000000000
--- a/output/layouts/layout_test.go
+++ /dev/null
@@ -1,982 +0,0 @@
-// Copyright 2017-present The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package layouts
-
-import (
- "fmt"
- "reflect"
- "strings"
- "testing"
-
- qt "github.com/frankban/quicktest"
- "github.com/kylelemons/godebug/diff"
-)
-
-func TestLayout(t *testing.T) {
- c := qt.New(t)
-
- for _, this := range []struct {
- name string
- layoutDescriptor LayoutDescriptor
- layoutOverride string
- expect []string
- }{
- {
- "Home",
- LayoutDescriptor{Kind: "home", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "index.amp.html",
- "home.amp.html",
- "list.amp.html",
- "index.html",
- "home.html",
- "list.html",
- "_default/index.amp.html",
- "_default/home.amp.html",
- "_default/list.amp.html",
- "_default/index.html",
- "_default/home.html",
- "_default/list.html",
- },
- },
- {
- "Home baseof",
- LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "index-baseof.amp.html",
- "home-baseof.amp.html",
- "list-baseof.amp.html",
- "baseof.amp.html",
- "index-baseof.html",
- "home-baseof.html",
- "list-baseof.html",
- "baseof.html",
- "_default/index-baseof.amp.html",
- "_default/home-baseof.amp.html",
- "_default/list-baseof.amp.html",
- "_default/baseof.amp.html",
- "_default/index-baseof.html",
- "_default/home-baseof.html",
- "_default/list-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Home, HTML",
- LayoutDescriptor{Kind: "home", OutputFormatName: "html", Suffix: "html"},
- "",
- // We will eventually get to index.html. This looks stuttery, but makes the lookup logic easy to understand.
- []string{
- "index.html.html",
- "home.html.html",
- "list.html.html",
- "index.html",
- "home.html",
- "list.html",
- "_default/index.html.html",
- "_default/home.html.html",
- "_default/list.html.html",
- "_default/index.html",
- "_default/home.html",
- "_default/list.html",
- },
- },
- {
- "Home, HTML, baseof",
- LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "html", Suffix: "html"},
- "",
- []string{
- "index-baseof.html.html",
- "home-baseof.html.html",
- "list-baseof.html.html",
- "baseof.html.html",
- "index-baseof.html",
- "home-baseof.html",
- "list-baseof.html",
- "baseof.html",
- "_default/index-baseof.html.html",
- "_default/home-baseof.html.html",
- "_default/list-baseof.html.html",
- "_default/baseof.html.html",
- "_default/index-baseof.html",
- "_default/home-baseof.html",
- "_default/list-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Home, french language",
- LayoutDescriptor{Kind: "home", Lang: "fr", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "index.fr.amp.html",
- "home.fr.amp.html",
- "list.fr.amp.html",
- "index.amp.html",
- "home.amp.html",
- "list.amp.html",
- "index.fr.html",
- "home.fr.html",
- "list.fr.html",
- "index.html",
- "home.html",
- "list.html",
- "_default/index.fr.amp.html",
- "_default/home.fr.amp.html",
- "_default/list.fr.amp.html",
- "_default/index.amp.html",
- "_default/home.amp.html",
- "_default/list.amp.html",
- "_default/index.fr.html",
- "_default/home.fr.html",
- "_default/list.fr.html",
- "_default/index.html",
- "_default/home.html",
- "_default/list.html",
- },
- },
- {
- "Home, no ext or delim",
- LayoutDescriptor{Kind: "home", OutputFormatName: "nem", Suffix: ""},
- "",
- []string{
- "index.nem",
- "home.nem",
- "list.nem",
- "_default/index.nem",
- "_default/home.nem",
- "_default/list.nem",
- },
- },
- {
- "Home, no ext",
- LayoutDescriptor{Kind: "home", OutputFormatName: "nex", Suffix: ""},
- "",
- []string{
- "index.nex",
- "home.nex",
- "list.nex",
- "_default/index.nex",
- "_default/home.nex",
- "_default/list.nex",
- },
- },
- {
- "Page, no ext or delim",
- LayoutDescriptor{Kind: "page", OutputFormatName: "nem", Suffix: ""},
- "",
- []string{"_default/single.nem"},
- },
- {
- "Section",
- LayoutDescriptor{Kind: "section", Section: "sect1", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "sect1/sect1.amp.html",
- "sect1/section.amp.html",
- "sect1/list.amp.html",
- "sect1/sect1.html",
- "sect1/section.html",
- "sect1/list.html",
- "section/sect1.amp.html",
- "section/section.amp.html",
- "section/list.amp.html",
- "section/sect1.html",
- "section/section.html",
- "section/list.html",
- "_default/sect1.amp.html",
- "_default/section.amp.html",
- "_default/list.amp.html",
- "_default/sect1.html",
- "_default/section.html",
- "_default/list.html",
- },
- },
- {
- "Section, baseof",
- LayoutDescriptor{Kind: "section", Section: "sect1", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "sect1/sect1-baseof.amp.html",
- "sect1/section-baseof.amp.html",
- "sect1/list-baseof.amp.html",
- "sect1/baseof.amp.html",
- "sect1/sect1-baseof.html",
- "sect1/section-baseof.html",
- "sect1/list-baseof.html",
- "sect1/baseof.html",
- "section/sect1-baseof.amp.html",
- "section/section-baseof.amp.html",
- "section/list-baseof.amp.html",
- "section/baseof.amp.html",
- "section/sect1-baseof.html",
- "section/section-baseof.html",
- "section/list-baseof.html",
- "section/baseof.html",
- "_default/sect1-baseof.amp.html",
- "_default/section-baseof.amp.html",
- "_default/list-baseof.amp.html",
- "_default/baseof.amp.html",
- "_default/sect1-baseof.html",
- "_default/section-baseof.html",
- "_default/list-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Section, baseof, French, AMP",
- LayoutDescriptor{Kind: "section", Section: "sect1", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "sect1/sect1-baseof.fr.amp.html",
- "sect1/section-baseof.fr.amp.html",
- "sect1/list-baseof.fr.amp.html",
- "sect1/baseof.fr.amp.html",
- "sect1/sect1-baseof.amp.html",
- "sect1/section-baseof.amp.html",
- "sect1/list-baseof.amp.html",
- "sect1/baseof.amp.html",
- "sect1/sect1-baseof.fr.html",
- "sect1/section-baseof.fr.html",
- "sect1/list-baseof.fr.html",
- "sect1/baseof.fr.html",
- "sect1/sect1-baseof.html",
- "sect1/section-baseof.html",
- "sect1/list-baseof.html",
- "sect1/baseof.html",
- "section/sect1-baseof.fr.amp.html",
- "section/section-baseof.fr.amp.html",
- "section/list-baseof.fr.amp.html",
- "section/baseof.fr.amp.html",
- "section/sect1-baseof.amp.html",
- "section/section-baseof.amp.html",
- "section/list-baseof.amp.html",
- "section/baseof.amp.html",
- "section/sect1-baseof.fr.html",
- "section/section-baseof.fr.html",
- "section/list-baseof.fr.html",
- "section/baseof.fr.html",
- "section/sect1-baseof.html",
- "section/section-baseof.html",
- "section/list-baseof.html",
- "section/baseof.html",
- "_default/sect1-baseof.fr.amp.html",
- "_default/section-baseof.fr.amp.html",
- "_default/list-baseof.fr.amp.html",
- "_default/baseof.fr.amp.html",
- "_default/sect1-baseof.amp.html",
- "_default/section-baseof.amp.html",
- "_default/list-baseof.amp.html",
- "_default/baseof.amp.html",
- "_default/sect1-baseof.fr.html",
- "_default/section-baseof.fr.html",
- "_default/list-baseof.fr.html",
- "_default/baseof.fr.html",
- "_default/sect1-baseof.html",
- "_default/section-baseof.html",
- "_default/list-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Section with layout",
- LayoutDescriptor{Kind: "section", Section: "sect1", Layout: "mylayout", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "sect1/mylayout.amp.html",
- "sect1/sect1.amp.html",
- "sect1/section.amp.html",
- "sect1/list.amp.html",
- "sect1/mylayout.html",
- "sect1/sect1.html",
- "sect1/section.html",
- "sect1/list.html",
- "section/mylayout.amp.html",
- "section/sect1.amp.html",
- "section/section.amp.html",
- "section/list.amp.html",
- "section/mylayout.html",
- "section/sect1.html",
- "section/section.html",
- "section/list.html",
- "_default/mylayout.amp.html",
- "_default/sect1.amp.html",
- "_default/section.amp.html",
- "_default/list.amp.html",
- "_default/mylayout.html",
- "_default/sect1.html",
- "_default/section.html",
- "_default/list.html",
- },
- },
- {
- "Term, French, AMP",
- LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "term/term.fr.amp.html",
- "term/tags.fr.amp.html",
- "term/taxonomy.fr.amp.html",
- "term/list.fr.amp.html",
- "term/term.amp.html",
- "term/tags.amp.html",
- "term/taxonomy.amp.html",
- "term/list.amp.html",
- "term/term.fr.html",
- "term/tags.fr.html",
- "term/taxonomy.fr.html",
- "term/list.fr.html",
- "term/term.html",
- "term/tags.html",
- "term/taxonomy.html",
- "term/list.html",
- "taxonomy/term.fr.amp.html",
- "taxonomy/tags.fr.amp.html",
- "taxonomy/taxonomy.fr.amp.html",
- "taxonomy/list.fr.amp.html",
- "taxonomy/term.amp.html",
- "taxonomy/tags.amp.html",
- "taxonomy/taxonomy.amp.html",
- "taxonomy/list.amp.html",
- "taxonomy/term.fr.html",
- "taxonomy/tags.fr.html",
- "taxonomy/taxonomy.fr.html",
- "taxonomy/list.fr.html",
- "taxonomy/term.html",
- "taxonomy/tags.html",
- "taxonomy/taxonomy.html",
- "taxonomy/list.html",
- "tags/term.fr.amp.html",
- "tags/tags.fr.amp.html",
- "tags/taxonomy.fr.amp.html",
- "tags/list.fr.amp.html",
- "tags/term.amp.html",
- "tags/tags.amp.html",
- "tags/taxonomy.amp.html",
- "tags/list.amp.html",
- "tags/term.fr.html",
- "tags/tags.fr.html",
- "tags/taxonomy.fr.html",
- "tags/list.fr.html",
- "tags/term.html",
- "tags/tags.html",
- "tags/taxonomy.html",
- "tags/list.html",
- "_default/term.fr.amp.html",
- "_default/tags.fr.amp.html",
- "_default/taxonomy.fr.amp.html",
- "_default/list.fr.amp.html",
- "_default/term.amp.html",
- "_default/tags.amp.html",
- "_default/taxonomy.amp.html",
- "_default/list.amp.html",
- "_default/term.fr.html",
- "_default/tags.fr.html",
- "_default/taxonomy.fr.html",
- "_default/list.fr.html",
- "_default/term.html",
- "_default/tags.html",
- "_default/taxonomy.html",
- "_default/list.html",
- },
- },
- {
- "Term, baseof, French, AMP",
- LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "term/term-baseof.fr.amp.html",
- "term/tags-baseof.fr.amp.html",
- "term/taxonomy-baseof.fr.amp.html",
- "term/list-baseof.fr.amp.html",
- "term/baseof.fr.amp.html",
- "term/term-baseof.amp.html",
- "term/tags-baseof.amp.html",
- "term/taxonomy-baseof.amp.html",
- "term/list-baseof.amp.html",
- "term/baseof.amp.html",
- "term/term-baseof.fr.html",
- "term/tags-baseof.fr.html",
- "term/taxonomy-baseof.fr.html",
- "term/list-baseof.fr.html",
- "term/baseof.fr.html",
- "term/term-baseof.html",
- "term/tags-baseof.html",
- "term/taxonomy-baseof.html",
- "term/list-baseof.html",
- "term/baseof.html",
- "taxonomy/term-baseof.fr.amp.html",
- "taxonomy/tags-baseof.fr.amp.html",
- "taxonomy/taxonomy-baseof.fr.amp.html",
- "taxonomy/list-baseof.fr.amp.html",
- "taxonomy/baseof.fr.amp.html",
- "taxonomy/term-baseof.amp.html",
- "taxonomy/tags-baseof.amp.html",
- "taxonomy/taxonomy-baseof.amp.html",
- "taxonomy/list-baseof.amp.html",
- "taxonomy/baseof.amp.html",
- "taxonomy/term-baseof.fr.html",
- "taxonomy/tags-baseof.fr.html",
- "taxonomy/taxonomy-baseof.fr.html",
- "taxonomy/list-baseof.fr.html",
- "taxonomy/baseof.fr.html",
- "taxonomy/term-baseof.html",
- "taxonomy/tags-baseof.html",
- "taxonomy/taxonomy-baseof.html",
- "taxonomy/list-baseof.html",
- "taxonomy/baseof.html",
- "tags/term-baseof.fr.amp.html",
- "tags/tags-baseof.fr.amp.html",
- "tags/taxonomy-baseof.fr.amp.html",
- "tags/list-baseof.fr.amp.html",
- "tags/baseof.fr.amp.html",
- "tags/term-baseof.amp.html",
- "tags/tags-baseof.amp.html",
- "tags/taxonomy-baseof.amp.html",
- "tags/list-baseof.amp.html",
- "tags/baseof.amp.html",
- "tags/term-baseof.fr.html",
- "tags/tags-baseof.fr.html",
- "tags/taxonomy-baseof.fr.html",
- "tags/list-baseof.fr.html",
- "tags/baseof.fr.html",
- "tags/term-baseof.html",
- "tags/tags-baseof.html",
- "tags/taxonomy-baseof.html",
- "tags/list-baseof.html",
- "tags/baseof.html",
- "_default/term-baseof.fr.amp.html",
- "_default/tags-baseof.fr.amp.html",
- "_default/taxonomy-baseof.fr.amp.html",
- "_default/list-baseof.fr.amp.html",
- "_default/baseof.fr.amp.html",
- "_default/term-baseof.amp.html",
- "_default/tags-baseof.amp.html",
- "_default/taxonomy-baseof.amp.html",
- "_default/list-baseof.amp.html",
- "_default/baseof.amp.html",
- "_default/term-baseof.fr.html",
- "_default/tags-baseof.fr.html",
- "_default/taxonomy-baseof.fr.html",
- "_default/list-baseof.fr.html",
- "_default/baseof.fr.html",
- "_default/term-baseof.html",
- "_default/tags-baseof.html",
- "_default/taxonomy-baseof.html",
- "_default/list-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Term",
- LayoutDescriptor{Kind: "term", Section: "tags", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "term/term.amp.html",
- "term/tags.amp.html",
- "term/taxonomy.amp.html",
- "term/list.amp.html",
- "term/term.html",
- "term/tags.html",
- "term/taxonomy.html",
- "term/list.html",
- "taxonomy/term.amp.html",
- "taxonomy/tags.amp.html",
- "taxonomy/taxonomy.amp.html",
- "taxonomy/list.amp.html",
- "taxonomy/term.html",
- "taxonomy/tags.html",
- "taxonomy/taxonomy.html",
- "taxonomy/list.html",
- "tags/term.amp.html",
- "tags/tags.amp.html",
- "tags/taxonomy.amp.html",
- "tags/list.amp.html",
- "tags/term.html",
- "tags/tags.html",
- "tags/taxonomy.html",
- "tags/list.html",
- "_default/term.amp.html",
- "_default/tags.amp.html",
- "_default/taxonomy.amp.html",
- "_default/list.amp.html",
- "_default/term.html",
- "_default/tags.html",
- "_default/taxonomy.html",
- "_default/list.html",
- },
- },
- {
- "Taxonomy",
- LayoutDescriptor{Kind: "taxonomy", Section: "categories", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "categories/categories.terms.amp.html",
- "categories/terms.amp.html",
- "categories/taxonomy.amp.html",
- "categories/list.amp.html",
- "categories/categories.terms.html",
- "categories/terms.html",
- "categories/taxonomy.html",
- "categories/list.html",
- "taxonomy/categories.terms.amp.html",
- "taxonomy/terms.amp.html",
- "taxonomy/taxonomy.amp.html",
- "taxonomy/list.amp.html",
- "taxonomy/categories.terms.html",
- "taxonomy/terms.html",
- "taxonomy/taxonomy.html",
- "taxonomy/list.html",
- "_default/categories.terms.amp.html",
- "_default/terms.amp.html",
- "_default/taxonomy.amp.html",
- "_default/list.amp.html",
- "_default/categories.terms.html",
- "_default/terms.html",
- "_default/taxonomy.html",
- "_default/list.html",
- },
- },
- {
- "Page",
- LayoutDescriptor{Kind: "page", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "_default/single.amp.html",
- "_default/single.html",
- },
- },
- {
- "Page, baseof",
- LayoutDescriptor{Kind: "page", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "_default/single-baseof.amp.html",
- "_default/baseof.amp.html",
- "_default/single-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Page with layout",
- LayoutDescriptor{Kind: "page", Layout: "mylayout", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "_default/mylayout.amp.html",
- "_default/single.amp.html",
- "_default/mylayout.html",
- "_default/single.html",
- },
- },
- {
- "Page with layout, baseof",
- LayoutDescriptor{Kind: "page", Layout: "mylayout", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "_default/mylayout-baseof.amp.html",
- "_default/single-baseof.amp.html",
- "_default/baseof.amp.html",
- "_default/mylayout-baseof.html",
- "_default/single-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Page with layout and type",
- LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "myttype/mylayout.amp.html",
- "myttype/single.amp.html",
- "myttype/mylayout.html",
- "myttype/single.html",
- "_default/mylayout.amp.html",
- "_default/single.amp.html",
- "_default/mylayout.html",
- "_default/single.html",
- },
- },
- {
- "Page baseof with layout and type",
- LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "myttype/mylayout-baseof.amp.html",
- "myttype/single-baseof.amp.html",
- "myttype/baseof.amp.html",
- "myttype/mylayout-baseof.html",
- "myttype/single-baseof.html",
- "myttype/baseof.html",
- "_default/mylayout-baseof.amp.html",
- "_default/single-baseof.amp.html",
- "_default/baseof.amp.html",
- "_default/mylayout-baseof.html",
- "_default/single-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Page baseof with layout and type in French",
- LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "myttype/mylayout-baseof.fr.amp.html",
- "myttype/single-baseof.fr.amp.html",
- "myttype/baseof.fr.amp.html",
- "myttype/mylayout-baseof.amp.html",
- "myttype/single-baseof.amp.html",
- "myttype/baseof.amp.html",
- "myttype/mylayout-baseof.fr.html",
- "myttype/single-baseof.fr.html",
- "myttype/baseof.fr.html",
- "myttype/mylayout-baseof.html",
- "myttype/single-baseof.html",
- "myttype/baseof.html",
- "_default/mylayout-baseof.fr.amp.html",
- "_default/single-baseof.fr.amp.html",
- "_default/baseof.fr.amp.html",
- "_default/mylayout-baseof.amp.html",
- "_default/single-baseof.amp.html",
- "_default/baseof.amp.html",
- "_default/mylayout-baseof.fr.html",
- "_default/single-baseof.fr.html",
- "_default/baseof.fr.html",
- "_default/mylayout-baseof.html",
- "_default/single-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Page with layout and type with subtype",
- LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype/mysubtype", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "myttype/mysubtype/mylayout.amp.html",
- "myttype/mysubtype/single.amp.html",
- "myttype/mysubtype/mylayout.html",
- "myttype/mysubtype/single.html",
- "_default/mylayout.amp.html",
- "_default/single.amp.html",
- "_default/mylayout.html",
- "_default/single.html",
- },
- },
- // RSS
- {
- "RSS Home",
- LayoutDescriptor{Kind: "home", OutputFormatName: "rss", Suffix: "xml"},
- "",
- []string{
- "index.rss.xml",
- "home.rss.xml",
- "rss.xml",
- "list.rss.xml",
- "index.xml",
- "home.xml",
- "list.xml",
- "_default/index.rss.xml",
- "_default/home.rss.xml",
- "_default/rss.xml",
- "_default/list.rss.xml",
- "_default/index.xml",
- "_default/home.xml",
- "_default/list.xml",
- "_internal/_default/rss.xml",
- },
- },
- {
- "RSS Home, baseof",
- LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "rss", Suffix: "xml"},
- "",
- []string{
- "index-baseof.rss.xml",
- "home-baseof.rss.xml",
- "list-baseof.rss.xml",
- "baseof.rss.xml",
- "index-baseof.xml",
- "home-baseof.xml",
- "list-baseof.xml",
- "baseof.xml",
- "_default/index-baseof.rss.xml",
- "_default/home-baseof.rss.xml",
- "_default/list-baseof.rss.xml",
- "_default/baseof.rss.xml",
- "_default/index-baseof.xml",
- "_default/home-baseof.xml",
- "_default/list-baseof.xml",
- "_default/baseof.xml",
- },
- },
- {
- "RSS Section",
- LayoutDescriptor{Kind: "section", Section: "sect1", OutputFormatName: "rss", Suffix: "xml"},
- "",
- []string{
- "sect1/sect1.rss.xml",
- "sect1/section.rss.xml",
- "sect1/rss.xml",
- "sect1/list.rss.xml",
- "sect1/sect1.xml",
- "sect1/section.xml",
- "sect1/list.xml",
- "section/sect1.rss.xml",
- "section/section.rss.xml",
- "section/rss.xml",
- "section/list.rss.xml",
- "section/sect1.xml",
- "section/section.xml",
- "section/list.xml",
- "_default/sect1.rss.xml",
- "_default/section.rss.xml",
- "_default/rss.xml",
- "_default/list.rss.xml",
- "_default/sect1.xml",
- "_default/section.xml",
- "_default/list.xml",
- "_internal/_default/rss.xml",
- },
- },
- {
- "RSS Term",
- LayoutDescriptor{Kind: "term", Section: "tag", OutputFormatName: "rss", Suffix: "xml"},
- "",
- []string{
- "term/term.rss.xml",
- "term/tag.rss.xml",
- "term/taxonomy.rss.xml",
- "term/rss.xml",
- "term/list.rss.xml",
- "term/term.xml",
- "term/tag.xml",
- "term/taxonomy.xml",
- "term/list.xml",
- "taxonomy/term.rss.xml",
- "taxonomy/tag.rss.xml",
- "taxonomy/taxonomy.rss.xml",
- "taxonomy/rss.xml",
- "taxonomy/list.rss.xml",
- "taxonomy/term.xml",
- "taxonomy/tag.xml",
- "taxonomy/taxonomy.xml",
- "taxonomy/list.xml",
- "tag/term.rss.xml",
- "tag/tag.rss.xml",
- "tag/taxonomy.rss.xml",
- "tag/rss.xml",
- "tag/list.rss.xml",
- "tag/term.xml",
- "tag/tag.xml",
- "tag/taxonomy.xml",
- "tag/list.xml",
- "_default/term.rss.xml",
- "_default/tag.rss.xml",
- "_default/taxonomy.rss.xml",
- "_default/rss.xml",
- "_default/list.rss.xml",
- "_default/term.xml",
- "_default/tag.xml",
- "_default/taxonomy.xml",
- "_default/list.xml",
- "_internal/_default/rss.xml",
- },
- },
- {
- "RSS Taxonomy",
- LayoutDescriptor{Kind: "taxonomy", Section: "tag", OutputFormatName: "rss", Suffix: "xml"},
- "",
- []string{
- "tag/tag.terms.rss.xml",
- "tag/terms.rss.xml",
- "tag/taxonomy.rss.xml",
- "tag/rss.xml",
- "tag/list.rss.xml",
- "tag/tag.terms.xml",
- "tag/terms.xml",
- "tag/taxonomy.xml",
- "tag/list.xml",
- "taxonomy/tag.terms.rss.xml",
- "taxonomy/terms.rss.xml",
- "taxonomy/taxonomy.rss.xml",
- "taxonomy/rss.xml",
- "taxonomy/list.rss.xml",
- "taxonomy/tag.terms.xml",
- "taxonomy/terms.xml",
- "taxonomy/taxonomy.xml",
- "taxonomy/list.xml",
- "_default/tag.terms.rss.xml",
- "_default/terms.rss.xml",
- "_default/taxonomy.rss.xml",
- "_default/rss.xml",
- "_default/list.rss.xml",
- "_default/tag.terms.xml",
- "_default/terms.xml",
- "_default/taxonomy.xml",
- "_default/list.xml",
- "_internal/_default/rss.xml",
- },
- },
- {
- "Home plain text",
- LayoutDescriptor{Kind: "home", OutputFormatName: "json", Suffix: "json"},
- "",
- []string{
- "index.json.json",
- "home.json.json",
- "list.json.json",
- "index.json",
- "home.json",
- "list.json",
- "_default/index.json.json",
- "_default/home.json.json",
- "_default/list.json.json",
- "_default/index.json",
- "_default/home.json",
- "_default/list.json",
- },
- },
- {
- "Page plain text",
- LayoutDescriptor{Kind: "page", OutputFormatName: "json", Suffix: "json"},
- "",
- []string{
- "_default/single.json.json",
- "_default/single.json",
- },
- },
- {
- "Reserved section, shortcodes",
- LayoutDescriptor{Kind: "section", Section: "shortcodes", Type: "shortcodes", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "section/shortcodes.amp.html",
- "section/section.amp.html",
- "section/list.amp.html",
- "section/shortcodes.html",
- "section/section.html",
- "section/list.html",
- "_default/shortcodes.amp.html",
- "_default/section.amp.html",
- "_default/list.amp.html",
- "_default/shortcodes.html",
- "_default/section.html",
- "_default/list.html",
- },
- },
- {
- "Reserved section, partials",
- LayoutDescriptor{Kind: "section", Section: "partials", Type: "partials", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "section/partials.amp.html",
- "section/section.amp.html",
- "section/list.amp.html",
- "section/partials.html",
- "section/section.html",
- "section/list.html",
- "_default/partials.amp.html",
- "_default/section.amp.html",
- "_default/list.amp.html",
- "_default/partials.html",
- "_default/section.html",
- "_default/list.html",
- },
- },
- // This is currently always HTML only
- {
- "404, HTML",
- LayoutDescriptor{Kind: "404", OutputFormatName: "html", Suffix: "html"},
- "",
- []string{
- "404.html.html",
- "404.html",
- },
- },
- {
- "404, HTML baseof",
- LayoutDescriptor{Kind: "404", Baseof: true, OutputFormatName: "html", Suffix: "html"},
- "",
- []string{
- "404-baseof.html.html",
- "baseof.html.html",
- "404-baseof.html",
- "baseof.html",
- "_default/404-baseof.html.html",
- "_default/baseof.html.html",
- "_default/404-baseof.html",
- "_default/baseof.html",
- },
- },
- {
- "Content hook",
- LayoutDescriptor{Kind: "render-link", RenderingHook: true, Layout: "mylayout", Section: "blog", OutputFormatName: "amp", Suffix: "html"},
- "",
- []string{
- "blog/_markup/render-link.amp.html",
- "blog/_markup/render-link.html",
- "_default/_markup/render-link.amp.html",
- "_default/_markup/render-link.html",
- },
- },
- } {
- c.Run(this.name, func(c *qt.C) {
- l := NewLayoutHandler()
-
- layouts, err := l.For(this.layoutDescriptor)
-
- c.Assert(err, qt.IsNil)
- c.Assert(layouts, qt.Not(qt.IsNil), qt.Commentf(this.layoutDescriptor.Kind))
-
- if !reflect.DeepEqual(layouts, this.expect) {
- r := strings.NewReplacer(
- "[", "\t\"",
- "]", "\",",
- " ", "\",\n\t\"",
- )
- fmtGot := r.Replace(fmt.Sprintf("%v", layouts))
- fmtExp := r.Replace(fmt.Sprintf("%v", this.expect))
-
- c.Fatalf("got %d items, expected %d:\nGot:\n\t%v\nExpected:\n\t%v\nDiff:\n%s", len(layouts), len(this.expect), layouts, this.expect, diff.Diff(fmtExp, fmtGot))
-
- }
- })
- }
-}
-
-/*
-func BenchmarkLayout(b *testing.B) {
- descriptor := LayoutDescriptor{Kind: "taxonomy", Section: "categories"}
- l := NewLayoutHandler()
-
- for i := 0; i < b.N; i++ {
- _, err := l.For(descriptor, HTMLFormat)
- if err != nil {
- panic(err)
- }
- }
-}
-
-func BenchmarkLayoutUncached(b *testing.B) {
- for i := 0; i < b.N; i++ {
- descriptor := LayoutDescriptor{Kind: "taxonomy", Section: "categories"}
- l := NewLayoutHandler()
-
- _, err := l.For(descriptor, HTMLFormat)
- if err != nil {
- panic(err)
- }
- }
-}
-*/
diff --git a/output/outputFormat.go b/output/outputFormat.go
index d249c72b9..8f3716e3e 100644
--- a/output/outputFormat.go
+++ b/output/outputFormat.go
@@ -133,6 +133,15 @@ var (
Weight: 10,
}
+ // Alias is the output format used for alias redirects.
+ AliasHTMLFormat = Format{
+ Name: "alias",
+ MediaType: media.Builtin.HTMLType,
+ IsHTML: true,
+ Ugly: true,
+ Permalinkable: false,
+ }
+
MarkdownFormat = Format{
Name: "markdown",
MediaType: media.Builtin.MarkdownType,
@@ -192,8 +201,17 @@ var (
Rel: "sitemap",
}
- HTTPStatusHTMLFormat = Format{
- Name: "httpstatus",
+ GotmplFormat = Format{
+ Name: "gotmpl",
+ MediaType: media.Builtin.GotmplType,
+ IsPlainText: true,
+ NotAlternative: true,
+ }
+
+ // I'm not sure having a 404 format is a good idea,
+ // for one, we would want to have multiple formats for this.
+ HTTPStatus404HTMLFormat = Format{
+ Name: "404",
MediaType: media.Builtin.HTMLType,
NotAlternative: true,
Ugly: true,
@@ -209,12 +227,16 @@ var DefaultFormats = Formats{
CSSFormat,
CSVFormat,
HTMLFormat,
+ GotmplFormat,
+ HTTPStatus404HTMLFormat,
+ AliasHTMLFormat,
JSONFormat,
MarkdownFormat,
WebAppManifestFormat,
RobotsTxtFormat,
RSSFormat,
SitemapFormat,
+ SitemapIndexFormat,
}
func init() {
diff --git a/output/outputFormat_test.go b/output/outputFormat_test.go
index c86bcd634..5950c590c 100644
--- a/output/outputFormat_test.go
+++ b/output/outputFormat_test.go
@@ -68,7 +68,7 @@ func TestDefaultTypes(t *testing.T) {
c.Assert(RSSFormat.NoUgly, qt.Equals, true)
c.Assert(CalendarFormat.IsHTML, qt.Equals, false)
- c.Assert(len(DefaultFormats), qt.Equals, 11)
+ c.Assert(len(DefaultFormats), qt.Equals, 15)
}
func TestGetFormatByName(t *testing.T) {
@@ -140,7 +140,7 @@ func TestGetFormatByFilename(t *testing.T) {
func TestSort(t *testing.T) {
c := qt.New(t)
c.Assert(DefaultFormats[0].Name, qt.Equals, "html")
- c.Assert(DefaultFormats[1].Name, qt.Equals, "amp")
+ c.Assert(DefaultFormats[1].Name, qt.Equals, "404")
json := JSONFormat
json.Weight = 1
diff --git a/parser/frontmatter.go b/parser/frontmatter.go
index ced8b84fc..18e55f9ad 100644
--- a/parser/frontmatter.go
+++ b/parser/frontmatter.go
@@ -104,7 +104,6 @@ func InterfaceToFrontMatter(in any, format metadecoders.Format, w io.Writer) err
}
err = InterfaceToConfig(in, format, w)
-
if err != nil {
return err
}
diff --git a/parser/lowercase_camel_json.go b/parser/lowercase_camel_json.go
index af0891de6..9d89ff020 100644
--- a/parser/lowercase_camel_json.go
+++ b/parser/lowercase_camel_json.go
@@ -46,6 +46,12 @@ type LowerCaseCamelJSONMarshaller struct {
Value any
}
+var preserveUpperCaseKeyRe = regexp.MustCompile(`^"HTTP`)
+
+func preserveUpperCaseKey(match []byte) bool {
+ return preserveUpperCaseKeyRe.Match(match)
+}
+
func (c LowerCaseCamelJSONMarshaller) MarshalJSON() ([]byte, error) {
marshalled, err := json.Marshal(c.Value)
@@ -59,7 +65,7 @@ func (c LowerCaseCamelJSONMarshaller) MarshalJSON() ([]byte, error) {
// Empty keys are valid JSON, only lowercase if we do not have an
// empty key.
- if len(match) > 2 {
+ if len(match) > 2 && !preserveUpperCaseKey(match) {
// Decode first rune after the double quotes
r, width := utf8.DecodeRune(match[1:])
r = unicode.ToLower(r)
@@ -93,7 +99,7 @@ func (c ReplacingJSONMarshaller) MarshalJSON() ([]byte, error) {
if c.OmitEmpty {
// It's tricky to do this with a regexp, so convert it to a map, remove zero values and convert back.
- var m map[string]interface{}
+ var m map[string]any
err = json.Unmarshal(converted, &m)
if err != nil {
return nil, err
@@ -101,13 +107,13 @@ func (c ReplacingJSONMarshaller) MarshalJSON() ([]byte, error) {
var removeZeroVAlues func(m map[string]any)
removeZeroVAlues = func(m map[string]any) {
for k, v := range m {
- if !hreflect.IsTruthful(v) {
+ if !hreflect.IsMap(v) && !hreflect.IsTruthful(v) {
delete(m, k)
} else {
switch vv := v.(type) {
- case map[string]interface{}:
+ case map[string]any:
removeZeroVAlues(vv)
- case []interface{}:
+ case []any:
for _, vvv := range vv {
if m, ok := vvv.(map[string]any); ok {
removeZeroVAlues(m)
diff --git a/parser/metadecoders/decoder.go b/parser/metadecoders/decoder.go
index 5dac23f03..419fbf4d2 100644
--- a/parser/metadecoders/decoder.go
+++ b/parser/metadecoders/decoder.go
@@ -36,16 +36,22 @@ import (
// Decoder provides some configuration options for the decoders.
type Decoder struct {
- // Delimiter is the field delimiter used in the CSV decoder. It defaults to ','.
+ // Delimiter is the field delimiter. Used in the CSV decoder. Default is
+ // ','.
Delimiter rune
- // Comment, if not 0, is the comment character used in the CSV decoder. Lines beginning with the
- // Comment character without preceding whitespace are ignored.
+ // Comment, if not 0, is the comment character. Lines beginning with the
+ // Comment character without preceding whitespace are ignored. Used in the
+ // CSV decoder.
Comment rune
// If true, a quote may appear in an unquoted field and a non-doubled quote
- // may appear in a quoted field. It defaults to false.
+ // may appear in a quoted field. Used in the CSV decoder. Default is false.
LazyQuotes bool
+
+ // The target data type, either slice or map. Used in the CSV decoder.
+ // Default is slice.
+ TargetType string
}
// OptionsKey is used in cache keys.
@@ -54,12 +60,14 @@ func (d Decoder) OptionsKey() string {
sb.WriteRune(d.Delimiter)
sb.WriteRune(d.Comment)
sb.WriteString(strconv.FormatBool(d.LazyQuotes))
+ sb.WriteString(d.TargetType)
return sb.String()
}
// Default is a Decoder in its default configuration.
var Default = Decoder{
- Delimiter: ',',
+ Delimiter: ',',
+ TargetType: "slice",
}
// UnmarshalToMap will unmarshall data in format f into a new map. This is
@@ -122,7 +130,14 @@ func (d Decoder) Unmarshal(data []byte, f Format) (any, error) {
if len(data) == 0 {
switch f {
case CSV:
- return make([][]string, 0), nil
+ switch d.TargetType {
+ case "map":
+ return make(map[string]any), nil
+ case "slice":
+ return make([][]string, 0), nil
+ default:
+ return nil, fmt.Errorf("invalid targetType: expected either slice or map, received %s", d.TargetType)
+ }
default:
return make(map[string]any), nil
}
@@ -152,7 +167,19 @@ func (d Decoder) UnmarshalTo(data []byte, f Format, v any) error {
if err != nil {
return toFileError(f, data, fmt.Errorf("failed to unmarshal XML: %w", err))
}
- xmlValue = xmlRoot[xmlRootName].(map[string]any)
+
+ // Get the root value and verify it's a map
+ rootValue := xmlRoot[xmlRootName]
+ if rootValue == nil {
+ return toFileError(f, data, fmt.Errorf("XML root element '%s' has no value", xmlRootName))
+ }
+
+ // Type check before conversion
+ mapValue, ok := rootValue.(map[string]any)
+ if !ok {
+ return toFileError(f, data, fmt.Errorf("XML root element '%s' must be a map/object, got %T", xmlRootName, rootValue))
+ }
+ xmlValue = mapValue
}
switch v := v.(type) {
@@ -220,10 +247,36 @@ func (d Decoder) unmarshalCSV(data []byte, v any) error {
switch vv := v.(type) {
case *any:
- *vv = records
- default:
- return fmt.Errorf("CSV cannot be unmarshaled into %T", v)
+ switch d.TargetType {
+ case "map":
+ if len(records) < 2 {
+ return fmt.Errorf("cannot unmarshal CSV into %T: expected at least a header row and one data row", v)
+ }
+ seen := make(map[string]bool, len(records[0]))
+ for _, fieldName := range records[0] {
+ if seen[fieldName] {
+ return fmt.Errorf("cannot unmarshal CSV into %T: header row contains duplicate field names", v)
+ }
+ seen[fieldName] = true
+ }
+
+ sm := make([]map[string]string, len(records)-1)
+ for i, record := range records[1:] {
+ m := make(map[string]string, len(records[0]))
+ for j, col := range record {
+ m[records[0][j]] = col
+ }
+ sm[i] = m
+ }
+ *vv = sm
+ case "slice":
+ *vv = records
+ default:
+ return fmt.Errorf("cannot unmarshal CSV into %T: invalid targetType: expected either slice or map, received %s", v, d.TargetType)
+ }
+ default:
+ return fmt.Errorf("cannot unmarshal CSV into %T", v)
}
return nil
@@ -251,6 +304,10 @@ func (d Decoder) unmarshalORG(data []byte, v any) error {
frontMatter[k[:len(k)-2]] = strings.Fields(v)
} else if strings.Contains(v, "\n") {
frontMatter[k] = strings.Split(v, "\n")
+ } else if k == "filetags" {
+ trimmed := strings.TrimPrefix(v, ":")
+ trimmed = strings.TrimSuffix(trimmed, ":")
+ frontMatter[k] = strings.Split(trimmed, ":")
} else if k == "date" || k == "lastmod" || k == "publishdate" || k == "expirydate" {
frontMatter[k] = parseORGDate(v)
} else {
diff --git a/parser/metadecoders/decoder_test.go b/parser/metadecoders/decoder_test.go
index 734713c2e..d78293402 100644
--- a/parser/metadecoders/decoder_test.go
+++ b/parser/metadecoders/decoder_test.go
@@ -99,6 +99,7 @@ func TestUnmarshalToMap(t *testing.T) {
// errors
{`a = b`, TOML, false},
{`a,b,c`, CSV, false}, // Use Unmarshal for CSV
+ {`just a string `, XML, false},
} {
msg := qt.Commentf("%d: %s", i, test.format)
m, err := d.UnmarshalToMap([]byte(test.data), test.format)
@@ -131,6 +132,8 @@ func TestUnmarshalToInterface(t *testing.T) {
{[]byte("#+a: foo bar\n#+a: baz"), ORG, map[string]any{"a": []string{string("foo bar"), string("baz")}}},
{[]byte(`#+DATE: <2020-06-26 Fri>`), ORG, map[string]any{"date": "2020-06-26"}},
{[]byte(`#+LASTMOD: <2020-06-26 Fri>`), ORG, map[string]any{"lastmod": "2020-06-26"}},
+ {[]byte(`#+FILETAGS: :work:`), ORG, map[string]any{"filetags": []string{"work"}}},
+ {[]byte(`#+FILETAGS: :work:fun:`), ORG, map[string]any{"filetags": []string{"work", "fun"}}},
{[]byte(`#+PUBLISHDATE: <2020-06-26 Fri>`), ORG, map[string]any{"publishdate": "2020-06-26"}},
{[]byte(`#+EXPIRYDATE: <2020-06-26 Fri>`), ORG, map[string]any{"expirydate": "2020-06-26"}},
{[]byte(`a = "b"`), TOML, expect},
@@ -306,3 +309,26 @@ func BenchmarkStringifyMapKeysIntegers(b *testing.B) {
stringifyMapKeys(maps[i])
}
}
+
+func BenchmarkDecodeYAMLToMap(b *testing.B) {
+ d := Default
+
+ data := []byte(`
+a:
+ v1: 32
+ v2: 43
+ v3: "foo"
+b:
+ - a
+ - b
+c: "d"
+
+`)
+
+ for i := 0; i < b.N; i++ {
+ _, err := d.UnmarshalToMap(data, YAML)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
diff --git a/parser/pageparser/item.go b/parser/pageparser/item.go
index 47ec6d64d..7d63be0ad 100644
--- a/parser/pageparser/item.go
+++ b/parser/pageparser/item.go
@@ -104,7 +104,7 @@ func (i Item) ValTyped(source []byte) any {
}
func (i Item) IsText() bool {
- return i.Type == tText || i.Type == tIndentation
+ return i.Type == tText || i.IsIndentation()
}
func (i Item) IsIndentation() bool {
@@ -152,7 +152,7 @@ func (i Item) IsFrontMatter() bool {
}
func (i Item) IsDone() bool {
- return i.Type == tError || i.Type == tEOF
+ return i.IsError() || i.IsEOF()
}
func (i Item) IsEOF() bool {
@@ -166,18 +166,19 @@ func (i Item) IsError() bool {
func (i Item) ToString(source []byte) string {
val := i.Val(source)
switch {
- case i.Type == tEOF:
+ case i.IsEOF():
return "EOF"
- case i.Type == tError:
+ case i.IsError():
return string(val)
- case i.Type == tIndentation:
+ case i.IsIndentation():
return fmt.Sprintf("%s:[%s]", i.Type, util.VisualizeSpaces(val))
case i.Type > tKeywordMarker:
return fmt.Sprintf("<%s>", val)
case len(val) > 50:
return fmt.Sprintf("%v:%.20q...", i.Type, val)
+ default:
+ return fmt.Sprintf("%v:[%s]", i.Type, val)
}
- return fmt.Sprintf("%v:[%s]", i.Type, val)
}
type ItemType int
diff --git a/parser/pageparser/item_test.go b/parser/pageparser/item_test.go
index 36b95e93a..10dbfe895 100644
--- a/parser/pageparser/item_test.go
+++ b/parser/pageparser/item_test.go
@@ -47,3 +47,217 @@ func TestItemValTyped(t *testing.T) {
source = []byte("xtrue")
c.Assert(Item{low: 0, high: len(source)}.ValTyped(source), qt.Equals, "xtrue")
}
+
+func TestItemBoolMethods(t *testing.T) {
+ c := qt.New(t)
+
+ source := []byte(" shortcode ")
+ tests := []struct {
+ name string
+ item Item
+ source []byte
+ want bool
+ call func(Item, []byte) bool
+ }{
+ {
+ name: "IsText true",
+ item: Item{Type: tText},
+ call: func(i Item, _ []byte) bool { return i.IsText() },
+ want: true,
+ },
+ {
+ name: "IsIndentation false",
+ item: Item{Type: tText},
+ call: func(i Item, _ []byte) bool { return i.IsIndentation() },
+ want: false,
+ },
+ {
+ name: "IsShortcodeName",
+ item: Item{Type: tScName},
+ call: func(i Item, _ []byte) bool { return i.IsShortcodeName() },
+ want: true,
+ },
+ {
+ name: "IsNonWhitespace true",
+ item: Item{
+ Type: tText,
+ low: 2,
+ high: 11,
+ },
+ source: source,
+ call: func(i Item, src []byte) bool { return i.IsNonWhitespace(src) },
+ want: true,
+ },
+ {
+ name: "IsShortcodeParam false",
+ item: Item{Type: tScParamVal},
+ call: func(i Item, _ []byte) bool { return i.IsShortcodeParam() },
+ want: false,
+ },
+ {
+ name: "IsInlineShortcodeName",
+ item: Item{Type: tScNameInline},
+ call: func(i Item, _ []byte) bool { return i.IsInlineShortcodeName() },
+ want: true,
+ },
+ {
+ name: "IsLeftShortcodeDelim tLeftDelimScWithMarkup",
+ item: Item{Type: tLeftDelimScWithMarkup},
+ call: func(i Item, _ []byte) bool { return i.IsLeftShortcodeDelim() },
+ want: true,
+ },
+ {
+ name: "IsLeftShortcodeDelim tLeftDelimScNoMarkup",
+ item: Item{Type: tLeftDelimScNoMarkup},
+ call: func(i Item, _ []byte) bool { return i.IsLeftShortcodeDelim() },
+ want: true,
+ },
+ {
+ name: "IsRightShortcodeDelim tRightDelimScWithMarkup",
+ item: Item{Type: tRightDelimScWithMarkup},
+ call: func(i Item, _ []byte) bool { return i.IsRightShortcodeDelim() },
+ want: true,
+ },
+ {
+ name: "IsRightShortcodeDelim tRightDelimScNoMarkup",
+ item: Item{Type: tRightDelimScNoMarkup},
+ call: func(i Item, _ []byte) bool { return i.IsRightShortcodeDelim() },
+ want: true,
+ },
+ {
+ name: "IsShortcodeClose",
+ item: Item{Type: tScClose},
+ call: func(i Item, _ []byte) bool { return i.IsShortcodeClose() },
+ want: true,
+ },
+ {
+ name: "IsShortcodeParamVal",
+ item: Item{Type: tScParamVal},
+ call: func(i Item, _ []byte) bool { return i.IsShortcodeParamVal() },
+ want: true,
+ },
+ {
+ name: "IsShortcodeMarkupDelimiter tLeftDelimScWithMarkup",
+ item: Item{Type: tLeftDelimScWithMarkup},
+ call: func(i Item, _ []byte) bool { return i.IsShortcodeMarkupDelimiter() },
+ want: true,
+ },
+ {
+ name: "IsShortcodeMarkupDelimiter tRightDelimScWithMarkup",
+ item: Item{Type: tRightDelimScWithMarkup},
+ call: func(i Item, _ []byte) bool { return i.IsShortcodeMarkupDelimiter() },
+ want: true,
+ },
+ {
+ name: "IsFrontMatter TypeFrontMatterYAML",
+ item: Item{Type: TypeFrontMatterYAML},
+ call: func(i Item, _ []byte) bool { return i.IsFrontMatter() },
+ want: true,
+ },
+ {
+ name: "IsFrontMatter TypeFrontMatterTOML",
+ item: Item{Type: TypeFrontMatterTOML},
+ call: func(i Item, _ []byte) bool { return i.IsFrontMatter() },
+ want: true,
+ },
+ {
+ name: "IsFrontMatter TypeFrontMatterJSON",
+ item: Item{Type: TypeFrontMatterJSON},
+ call: func(i Item, _ []byte) bool { return i.IsFrontMatter() },
+ want: true,
+ },
+ {
+ name: "IsFrontMatter TypeFrontMatterORG",
+ item: Item{Type: TypeFrontMatterORG},
+ call: func(i Item, _ []byte) bool { return i.IsFrontMatter() },
+ want: true,
+ },
+ {
+ name: "IsDone tError",
+ item: Item{Type: tError},
+ call: func(i Item, _ []byte) bool { return i.IsDone() },
+ want: true,
+ },
+ {
+ name: "IsDone tEOF",
+ item: Item{Type: tEOF},
+ call: func(i Item, _ []byte) bool { return i.IsDone() },
+ want: true,
+ },
+ {
+ name: "IsEOF",
+ item: Item{Type: tEOF},
+ call: func(i Item, _ []byte) bool { return i.IsEOF() },
+ want: true,
+ },
+ {
+ name: "IsError",
+ item: Item{Type: tError},
+ call: func(i Item, _ []byte) bool { return i.IsError() },
+ want: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := tt.call(tt.item, tt.source)
+ c.Assert(got, qt.Equals, tt.want)
+ })
+ }
+}
+
+func TestItem_ToString(t *testing.T) {
+ c := qt.New(t)
+
+ source := []byte("src")
+ long := make([]byte, 100)
+ for i := range long {
+ long[i] = byte(i)
+ }
+
+ tests := []struct {
+ name string
+ item Item
+ source []byte
+ want string
+ call func(Item, []byte) string
+ }{
+ {
+ name: "EOF",
+ item: Item{Type: tEOF},
+ call: func(i Item, _ []byte) string { return i.ToString(source) },
+ want: "EOF",
+ },
+ {
+ name: "Error",
+ item: Item{Type: tError},
+ call: func(i Item, _ []byte) string { return i.ToString(source) },
+ want: "",
+ },
+ {
+ name: "Indentation",
+ item: Item{Type: tIndentation},
+ call: func(i Item, _ []byte) string { return i.ToString(source) },
+ want: "tIndentation:[]",
+ },
+ {
+ name: "Long",
+ item: Item{Type: tKeywordMarker + 1, low: 0, high: 100},
+ call: func(i Item, _ []byte) string { return i.ToString(long) },
+ want: "<" + string(long) + ">",
+ },
+ {
+ name: "Empty",
+ item: Item{Type: tKeywordMarker + 1},
+ call: func(i Item, _ []byte) string { return i.ToString([]byte("")) },
+ want: "<>",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := tt.call(tt.item, tt.source)
+ c.Assert(got, qt.Equals, tt.want)
+ })
+ }
+}
diff --git a/parser/pageparser/pagelexer.go b/parser/pageparser/pagelexer.go
index 5f90e3687..a5f64b037 100644
--- a/parser/pageparser/pagelexer.go
+++ b/parser/pageparser/pagelexer.go
@@ -62,15 +62,19 @@ func (l *pageLexer) Input() []byte {
return l.input
}
-type Config struct{}
+type Config struct {
+ NoFrontMatter bool
+ NoSummaryDivider bool
+}
// note: the input position here is normally 0 (start), but
// can be set if position of first shortcode is known
func newPageLexer(input []byte, stateStart stateFunc, cfg Config) *pageLexer {
lexer := &pageLexer{
- input: input,
- stateStart: stateStart,
- cfg: cfg,
+ input: input,
+ stateStart: stateStart,
+ summaryDivider: summaryDivider,
+ cfg: cfg,
lexerShortcodeState: lexerShortcodeState{
currLeftDelimItem: tLeftDelimScNoMarkup,
currRightDelimItem: tRightDelimScNoMarkup,
@@ -295,6 +299,8 @@ func (s *sectionHandlers) skip() int {
}
func createSectionHandlers(l *pageLexer) *sectionHandlers {
+ handlers := make([]*sectionHandler, 0, 2)
+
shortCodeHandler := §ionHandler{
l: l,
skipFunc: func(l *pageLexer) int {
@@ -330,31 +336,36 @@ func createSectionHandlers(l *pageLexer) *sectionHandlers {
},
}
- summaryDividerHandler := §ionHandler{
- l: l,
- skipFunc: func(l *pageLexer) int {
- if l.summaryDividerChecked || l.summaryDivider == nil {
- return -1
- }
- return l.index(l.summaryDivider)
- },
- lexFunc: func(origin stateFunc, l *pageLexer) (stateFunc, bool) {
- if !l.hasPrefix(l.summaryDivider) {
- return origin, false
- }
+ handlers = append(handlers, shortCodeHandler)
- l.summaryDividerChecked = true
- l.pos += len(l.summaryDivider)
- // This makes it a little easier to reason about later.
- l.consumeSpace()
- l.emit(TypeLeadSummaryDivider)
+ if !l.cfg.NoSummaryDivider {
+ summaryDividerHandler := §ionHandler{
+ l: l,
+ skipFunc: func(l *pageLexer) int {
+ if l.summaryDividerChecked {
+ return -1
+ }
+ return l.index(l.summaryDivider)
+ },
+ lexFunc: func(origin stateFunc, l *pageLexer) (stateFunc, bool) {
+ if !l.hasPrefix(l.summaryDivider) {
+ return origin, false
+ }
+
+ l.summaryDividerChecked = true
+ l.pos += len(l.summaryDivider)
+ // This makes it a little easier to reason about later.
+ l.consumeSpace()
+ l.emit(TypeLeadSummaryDivider)
+
+ return origin, true
+ },
+ }
+
+ handlers = append(handlers, summaryDividerHandler)
- return origin, true
- },
}
- handlers := []*sectionHandler{shortCodeHandler, summaryDividerHandler}
-
return §ionHandlers{
l: l,
handlers: handlers,
diff --git a/parser/pageparser/pagelexer_intro.go b/parser/pageparser/pagelexer_intro.go
index 0ff0958fe..a68a9e03a 100644
--- a/parser/pageparser/pagelexer_intro.go
+++ b/parser/pageparser/pagelexer_intro.go
@@ -14,8 +14,6 @@
package pageparser
func lexIntroSection(l *pageLexer) stateFunc {
- l.summaryDivider = summaryDivider
-
LOOP:
for {
r := l.next()
@@ -93,14 +91,14 @@ func lexFrontMatterOrgMode(l *pageLexer) stateFunc {
#+DESCRIPTION: Just another golang parser for org content!
*/
- l.summaryDivider = summaryDividerOrg
-
l.backup()
if !l.hasPrefix(delimOrg) {
return lexMainSection
}
+ l.summaryDivider = summaryDividerOrg
+
// Read lines until we no longer see a #+ prefix
LOOP:
for {
@@ -125,7 +123,7 @@ LOOP:
// Handle YAML or TOML front matter.
func (l *pageLexer) lexFrontMatterSection(tp ItemType, delimr rune, name string, delim []byte) stateFunc {
- for i := 0; i < 2; i++ {
+ for range 2 {
if r := l.next(); r != delimr {
return l.errorf("invalid %s delimiter", name)
}
diff --git a/parser/pageparser/pagelexer_intro_test.go b/parser/pageparser/pagelexer_intro_test.go
new file mode 100644
index 000000000..c074e6f50
--- /dev/null
+++ b/parser/pageparser/pagelexer_intro_test.go
@@ -0,0 +1,46 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pageparser
+
+import (
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func Test_lexIntroSection(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+ for i, tt := range []struct {
+ input string
+ expectItemType ItemType
+ expectSummaryDivider []byte
+ }{
+ {"{\"title\": \"JSON\"}\n", TypeFrontMatterJSON, summaryDivider},
+ {"#+TITLE: ORG\n", TypeFrontMatterORG, summaryDividerOrg},
+ {"+++\ntitle = \"TOML\"\n+++\n", TypeFrontMatterTOML, summaryDivider},
+ {"---\ntitle: YAML\n---\n", TypeFrontMatterYAML, summaryDivider},
+ // Issue 13152
+ {"# ATX Header Level 1\n", tText, summaryDivider},
+ } {
+ errMsg := qt.Commentf("[%d] %v", i, tt.input)
+
+ l := newPageLexer([]byte(tt.input), lexIntroSection, Config{})
+ l.run()
+
+ c.Assert(l.items[0].Type, qt.Equals, tt.expectItemType, errMsg)
+ c.Assert(l.summaryDivider, qt.DeepEquals, tt.expectSummaryDivider, errMsg)
+
+ }
+}
diff --git a/parser/pageparser/pagelexer_shortcode.go b/parser/pageparser/pagelexer_shortcode.go
index def2f82c7..535d8192c 100644
--- a/parser/pageparser/pagelexer_shortcode.go
+++ b/parser/pageparser/pagelexer_shortcode.go
@@ -322,6 +322,7 @@ func lexInsideShortcode(l *pageLexer) stateFunc {
}
l.closingState++
l.isInline = false
+ l.elementStepNum = 0
l.emit(tScClose)
case r == '\\':
l.ignore()
diff --git a/parser/pageparser/pageparser.go b/parser/pageparser/pageparser.go
index 9e8b6d803..5c6f4b2ff 100644
--- a/parser/pageparser/pageparser.go
+++ b/parser/pageparser/pageparser.go
@@ -36,16 +36,11 @@ var _ Result = (*pageLexer)(nil)
// ParseBytes parses the page in b according to the given Config.
func ParseBytes(b []byte, cfg Config) (Items, error) {
- l, err := parseBytes(b, cfg, lexIntroSection)
- if err != nil {
- return nil, err
+ startLexer := lexIntroSection
+ if cfg.NoFrontMatter {
+ startLexer = lexMainSection
}
- return l.items, l.err
-}
-
-// ParseBytesMain parses b starting with the main section.
-func ParseBytesMain(b []byte, cfg Config) (Items, error) {
- l, err := parseBytes(b, cfg, lexMainSection)
+ l, err := parseBytes(b, cfg, startLexer)
if err != nil {
return nil, err
}
@@ -197,7 +192,7 @@ func (t *Iterator) PeekWalk(walkFn func(item Item) bool) {
// Consume is a convenience method to consume the next n tokens,
// but back off Errors and EOF.
func (t *Iterator) Consume(cnt int) {
- for i := 0; i < cnt; i++ {
+ for range cnt {
token := t.Next()
if token.Type == tError || token.Type == tEOF {
t.Backup()
diff --git a/parser/pageparser/pageparser_shortcode_test.go b/parser/pageparser/pageparser_shortcode_test.go
index 327da30ee..29626b6ad 100644
--- a/parser/pageparser/pageparser_shortcode_test.go
+++ b/parser/pageparser/pageparser_shortcode_test.go
@@ -126,6 +126,9 @@ var shortCodeLexerTests = []lexerTest{
{"self-closing with param", `{{< sc1 param1 />}}`, []typeText{
tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD, tstEOF,
}, nil},
+ {"self-closing with extra keyword", `{{< sc1 / keyword>}}`, []typeText{
+ tstLeftNoMD, tstSC1, tstSCClose, nti(tError, "closing tag for shortcode 'keyword' does not match start tag"),
+ }, nil},
{"multiple self-closing with param", `{{< sc1 param1 />}}{{< sc1 param1 />}}`, []typeText{
tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD,
tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD, tstEOF,
diff --git a/parser/pageparser/pageparser_test.go b/parser/pageparser/pageparser_test.go
index a50ab46e9..c6bedbd6f 100644
--- a/parser/pageparser/pageparser_test.go
+++ b/parser/pageparser/pageparser_test.go
@@ -101,3 +101,14 @@ func BenchmarkHasShortcode(b *testing.B) {
}
})
}
+
+func TestSummaryDividerStartingFromMain(t *testing.T) {
+ c := qt.New(t)
+
+ input := `aaa bbb`
+ items, err := collectStringMain(input)
+ c.Assert(err, qt.IsNil)
+
+ c.Assert(items, qt.HasLen, 4)
+ c.Assert(items[1].Type, qt.Equals, TypeLeadSummaryDivider)
+}
diff --git a/related/inverted_index.go b/related/inverted_index.go
index 7e171cf53..b8f1ad3e2 100644
--- a/related/inverted_index.go
+++ b/related/inverted_index.go
@@ -292,7 +292,7 @@ func (r *rank) addWeight(w int) {
}
var rankPool = sync.Pool{
- New: func() interface{} {
+ New: func() any {
return &rank{}
},
}
@@ -433,7 +433,7 @@ func (cfg IndexConfig) ToKeywords(v any) ([]Keyword, error) {
keywords = append(keywords, cfg.stringToKeyword(vv))
case []string:
vvv := make([]Keyword, len(vv))
- for i := 0; i < len(vvv); i++ {
+ for i := range vvv {
vvv[i] = cfg.stringToKeyword(vv[i])
}
keywords = append(keywords, vvv...)
@@ -582,6 +582,9 @@ func DecodeConfig(m maps.Params) (Config, error) {
}
}
for i := range c.Indices {
+ // Lower case name.
+ c.Indices[i].Name = strings.ToLower(c.Indices[i].Name)
+
icfg := c.Indices[i]
if icfg.Type == "" {
c.Indices[i].Type = TypeBasic
@@ -620,7 +623,7 @@ type Keyword interface {
func (cfg IndexConfig) StringsToKeywords(s ...string) []Keyword {
kw := make([]Keyword, len(s))
- for i := 0; i < len(s); i++ {
+ for i := range s {
kw[i] = cfg.stringToKeyword(s[i])
}
diff --git a/related/inverted_index_test.go b/related/inverted_index_test.go
index f1d8e11a1..d57237e11 100644
--- a/related/inverted_index_test.go
+++ b/related/inverted_index_test.go
@@ -21,6 +21,7 @@ import (
"time"
qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/config"
)
type testDoc struct {
@@ -64,7 +65,7 @@ func (d *testDoc) addKeywords(name string, keywords ...string) *testDoc {
for k, v := range keywordm {
keywords := make([]Keyword, len(v))
- for i := 0; i < len(v); i++ {
+ for i := range v {
keywords[i] = StringKeyword(v[i])
}
d.keywords[k] = keywords
@@ -220,7 +221,7 @@ func TestSearch(t *testing.T) {
doc := newTestDocWithDate("keywords", date, "a", "b")
doc.name = "thedoc"
- for i := 0; i < 10; i++ {
+ for i := range 10 {
docc := *doc
docc.name = fmt.Sprintf("doc%d", i)
idx.Add(context.Background(), &docc)
@@ -229,7 +230,7 @@ func TestSearch(t *testing.T) {
m, err := idx.Search(context.Background(), SearchOpts{Document: doc, Indices: []string{"keywords"}})
c.Assert(err, qt.IsNil)
c.Assert(len(m), qt.Equals, 10)
- for i := 0; i < 10; i++ {
+ for i := range 10 {
c.Assert(m[i].Name(), qt.Equals, fmt.Sprintf("doc%d", i))
}
})
@@ -249,6 +250,50 @@ func TestToKeywordsToLower(t *testing.T) {
})
}
+func TestDecodeConfig(t *testing.T) {
+ c := qt.New(t)
+
+ configToml := `
+[related]
+ includeNewer = true
+ threshold = 32
+ toLower = false
+ [[related.indices]]
+ applyFilter = false
+ cardinalityThreshold = 0
+ name = 'KeyworDs'
+ pattern = ''
+ toLower = false
+ type = 'basic'
+ weight = 100
+ [[related.indices]]
+ applyFilter = true
+ cardinalityThreshold = 32
+ name = 'date'
+ pattern = ''
+ toLower = false
+ type = 'basic'
+ weight = 10
+ [[related.indices]]
+ applyFilter = false
+ cardinalityThreshold = 0
+ name = 'tags'
+ pattern = ''
+ toLower = false
+ type = 'fragments'
+ weight = 80
+`
+
+ m, err := config.FromConfigString(configToml, "toml")
+ c.Assert(err, qt.IsNil)
+ conf, err := DecodeConfig(m.GetParams("related"))
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(conf.IncludeNewer, qt.IsTrue)
+ first := conf.Indices[0]
+ c.Assert(first.Name, qt.Equals, "keywords")
+}
+
func TestToKeywordsAnySlice(t *testing.T) {
c := qt.New(t)
var config IndexConfig
@@ -266,11 +311,11 @@ func BenchmarkRelatedNewIndex(b *testing.B) {
pages := make([]*testDoc, 100)
numkeywords := 30
allKeywords := make([]string, numkeywords)
- for i := 0; i < numkeywords; i++ {
+ for i := range numkeywords {
allKeywords[i] = fmt.Sprintf("keyword%d", i+1)
}
- for i := 0; i < len(pages); i++ {
+ for i := range pages {
start := rand.Intn(len(allKeywords))
end := start + 3
if end >= len(allKeywords) {
@@ -311,7 +356,7 @@ func BenchmarkRelatedNewIndex(b *testing.B) {
for i := 0; i < b.N; i++ {
idx := NewInvertedIndex(cfg)
docs := make([]Document, len(pages))
- for i := 0; i < len(pages); i++ {
+ for i := range pages {
docs[i] = pages[i]
}
idx.Add(context.Background(), docs...)
@@ -327,7 +372,7 @@ func BenchmarkRelatedMatchesIn(b *testing.B) {
docs := make([]*testDoc, 1000)
numkeywords := 20
allKeywords := make([]string, numkeywords)
- for i := 0; i < numkeywords; i++ {
+ for i := range numkeywords {
allKeywords[i] = fmt.Sprintf("keyword%d", i+1)
}
@@ -341,7 +386,7 @@ func BenchmarkRelatedMatchesIn(b *testing.B) {
idx := NewInvertedIndex(cfg)
- for i := 0; i < len(docs); i++ {
+ for i := range docs {
start := rand.Intn(len(allKeywords))
end := start + 3
if end >= len(allKeywords) {
diff --git a/related/related_integration_test.go b/related/related_integration_test.go
index 291bfdbf7..6d3c6d6de 100644
--- a/related/related_integration_test.go
+++ b/related/related_integration_test.go
@@ -160,7 +160,7 @@ keywords: ['k%d']
---
`
- for i := 0; i < 32; i++ {
+ for range 32 {
base += fmt.Sprintf("\n## Title %d", rand.Intn(100))
}
diff --git a/release-hook-post-linux.sh b/release-hook-post-linux.sh
deleted file mode 100755
index e97e274a8..000000000
--- a/release-hook-post-linux.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-
-# Se https://github.com/gohugoio/hugo/issues/8955
-objdump -T dist/hugo_extended_linux_linux_amd64/hugo | grep -E -q 'GLIBC_2.2[0-9]'
-RESULT=$?
-if [ $RESULT -eq 0 ]; then
- echo "Found GLIBC_2.2x in Linux binary, this will not work in older Vercel/Netlify images.";
- exit -1;
-fi
diff --git a/releaser/releaser.go b/releaser/releaser.go
index 254bda5b3..46cee1b13 100644
--- a/releaser/releaser.go
+++ b/releaser/releaser.go
@@ -1,4 +1,4 @@
-// Copyright 2017-present The Hugo Authors. All rights reserved.
+// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -19,11 +19,11 @@ import (
"fmt"
"log"
"os"
+ "os/exec"
"path/filepath"
"regexp"
"strings"
- "github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/hugo"
)
@@ -92,6 +92,8 @@ func (r *ReleaseHandler) Run() error {
mainVersion := newVersion
mainVersion.PatchLevel = 0
+ r.gitPull()
+
defer r.gitPush()
if r.step == 1 {
@@ -178,6 +180,12 @@ func (r ReleaseHandler) calculateVersions() (hugo.Version, hugo.Version) {
return newVersion, finalVersion
}
+func (r *ReleaseHandler) gitPull() {
+ if _, err := r.git("pull", "origin", "HEAD"); err != nil {
+ log.Fatal("pull failed:", err)
+ }
+}
+
func (r *ReleaseHandler) gitPush() {
if r.skipPush {
return
@@ -214,7 +222,7 @@ func (r *ReleaseHandler) replaceInFile(filename string, oldNew ...string) error
}
func git(args ...string) (string, error) {
- cmd, _ := hexec.SafeCommand("git", args...)
+ cmd := exec.Command("git", args...)
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("git failed: %q: %q (%q)", err, out, args)
@@ -222,10 +230,10 @@ func git(args ...string) (string, error) {
return string(out), nil
}
-func logf(format string, args ...interface{}) {
+func logf(format string, args ...any) {
fmt.Fprintf(os.Stderr, format, args...)
}
-func logln(args ...interface{}) {
+func logln(args ...any) {
fmt.Fprintln(os.Stderr, args...)
}
diff --git a/resources/errorResource.go b/resources/errorResource.go
deleted file mode 100644
index 582c54f6d..000000000
--- a/resources/errorResource.go
+++ /dev/null
@@ -1,145 +0,0 @@
-// Copyright 2021 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package resources
-
-import (
- "context"
- "image"
-
- "github.com/gohugoio/hugo/common/hugio"
- "github.com/gohugoio/hugo/common/maps"
- "github.com/gohugoio/hugo/media"
- "github.com/gohugoio/hugo/resources/images"
- "github.com/gohugoio/hugo/resources/images/exif"
- "github.com/gohugoio/hugo/resources/resource"
-)
-
-var (
- _ error = (*errorResource)(nil)
- // Image covers all current Resource implementations.
- _ images.ImageResource = (*errorResource)(nil)
- // The list of user facing and exported interfaces in resource.go
- // Note that if we're missing some interface here, the user will still
- // get an error, but not as pretty.
- _ resource.ContentResource = (*errorResource)(nil)
- _ resource.ReadSeekCloserResource = (*errorResource)(nil)
- _ resource.ResourcesLanguageMerger = (*resource.Resources)(nil)
- // Make sure it also fails when passed to a pipe function.
- _ ResourceTransformer = (*errorResource)(nil)
-)
-
-// NewErrorResource wraps err in a Resource where all but the Err method will panic.
-func NewErrorResource(err resource.ResourceError) resource.Resource {
- return &errorResource{ResourceError: err}
-}
-
-type errorResource struct {
- resource.ResourceError
-}
-
-func (e *errorResource) Err() resource.ResourceError {
- return e.ResourceError
-}
-
-func (e *errorResource) ReadSeekCloser() (hugio.ReadSeekCloser, error) {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) Content(context.Context) (any, error) {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) ResourceType() string {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) MediaType() media.Type {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) Permalink() string {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) RelPermalink() string {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) Name() string {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) Title() string {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) Params() maps.Params {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) Data() any {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) Height() int {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) Width() int {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) Process(spec string) (images.ImageResource, error) {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) Crop(spec string) (images.ImageResource, error) {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) Fill(spec string) (images.ImageResource, error) {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) Fit(spec string) (images.ImageResource, error) {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) Resize(spec string) (images.ImageResource, error) {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) Filter(filters ...any) (images.ImageResource, error) {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) Exif() *exif.ExifInfo {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) Colors() ([]images.Color, error) {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) DecodeImage() (image.Image, error) {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) Transform(...ResourceTransformation) (ResourceTransformer, error) {
- panic(e.ResourceError)
-}
-
-func (e *errorResource) TransformWithContext(context.Context, ...ResourceTransformation) (ResourceTransformer, error) {
- panic(e.ResourceError)
-}
diff --git a/resources/image.go b/resources/image.go
index 8f70a665a..c1f107b59 100644
--- a/resources/image.go
+++ b/resources/image.go
@@ -29,9 +29,8 @@ import (
color_extractor "github.com/marekm4/color-extractor"
"github.com/gohugoio/hugo/cache/filecache"
- "github.com/gohugoio/hugo/common/hstrings"
+ "github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/paths"
- "github.com/gohugoio/hugo/identity"
"github.com/disintegration/gift"
@@ -40,7 +39,6 @@ import (
"github.com/gohugoio/hugo/resources/resource"
- "github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/resources/images"
// Blind import for image.Decode
@@ -52,6 +50,7 @@ var (
_ resource.Source = (*imageResource)(nil)
_ resource.Cloner = (*imageResource)(nil)
_ resource.NameNormalizedProvider = (*imageResource)(nil)
+ _ targetPathProvider = (*imageResource)(nil)
)
// imageResource represents an image resource.
@@ -82,8 +81,9 @@ func (i *imageResource) Exif() *exif.ExifInfo {
func (i *imageResource) getExif() *exif.ExifInfo {
i.metaInit.Do(func() {
- supportsExif := i.Format == images.JPEG || i.Format == images.TIFF
- if !supportsExif {
+ mf := i.Format.ToImageMetaImageFormatFormat()
+ if mf == -1 {
+ // No Exif support for this format.
return
}
@@ -114,7 +114,8 @@ func (i *imageResource) getExif() *exif.ExifInfo {
}
defer f.Close()
- x, err := i.getSpec().imaging.DecodeExif(f)
+ filename := i.getResourcePaths().Path()
+ x, err := i.getSpec().imaging.DecodeExif(filename, mf, f)
if err != nil {
i.getSpec().Logger.Warnf("Unable to decode Exif metadata from image: %s", i.Key())
return nil
@@ -159,6 +160,10 @@ func (i *imageResource) Colors() ([]images.Color, error) {
return i.dominantColors, nil
}
+func (i *imageResource) targetPath() string {
+ return i.TargetPath()
+}
+
// Clone is for internal use.
func (i *imageResource) Clone() resource.Resource {
gr := i.baseResource.Clone().(baseResource)
@@ -199,15 +204,12 @@ func (i *imageResource) cloneWithUpdates(u *transformationUpdate) (baseResource,
}, nil
}
-var imageActions = []string{images.ActionResize, images.ActionCrop, images.ActionFit, images.ActionFill}
-
// Process processes the image with the given spec.
// The spec can contain an optional action, one of "resize", "crop", "fit" or "fill".
// This makes this method a more flexible version that covers all of Resize, Crop, Fit and Fill,
// but it also supports e.g. format conversions without any resize action.
func (i *imageResource) Process(spec string) (images.ImageResource, error) {
- action, options := i.resolveActionOptions(spec)
- return i.processActionOptions(action, options)
+ return i.processActionSpec("", spec)
}
// Resize resizes the image to the specified width and height using the specified resampling
@@ -237,7 +239,7 @@ func (i *imageResource) Fill(spec string) (images.ImageResource, error) {
}
func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) {
- var conf images.ImageConfig
+ var confMain images.ImageConfig
var gfilters []gift.Filter
@@ -245,47 +247,30 @@ func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) {
gfilters = append(gfilters, images.ToFilters(f)...)
}
- var (
- targetFormat images.Format
- configSet bool
- )
+ var options []string
+
for _, f := range gfilters {
f = images.UnwrapFilter(f)
if specProvider, ok := f.(images.ImageProcessSpecProvider); ok {
- action, options := i.resolveActionOptions(specProvider.ImageProcessSpec())
- var err error
- conf, err = images.DecodeImageConfig(action, options, i.Proc.Cfg, i.Format)
- if err != nil {
- return nil, err
- }
- configSet = true
- if conf.TargetFormat != 0 {
- targetFormat = conf.TargetFormat
- // We only support one target format, but prefer the last one,
- // so we keep going.
- }
+ options = append(options, strings.Fields(specProvider.ImageProcessSpec())...)
}
}
- if !configSet {
- conf = images.GetDefaultImageConfig("filter", i.Proc.Cfg)
+ confMain, err := images.DecodeImageConfig(options, i.Proc.Cfg, i.Format)
+ if err != nil {
+ return nil, err
}
- conf.Action = "filter"
- conf.Key = identity.HashString(gfilters)
- conf.TargetFormat = targetFormat
- if conf.TargetFormat == 0 {
- conf.TargetFormat = i.Format
- }
+ confMain.Action = "filter"
+ confMain.Key = hashing.HashString(gfilters)
- return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
+ return i.doWithImageConfig(confMain, func(src image.Image) (image.Image, error) {
var filters []gift.Filter
for _, f := range gfilters {
f = images.UnwrapFilter(f)
if specProvider, ok := f.(images.ImageProcessSpecProvider); ok {
- processSpec := specProvider.ImageProcessSpec()
- action, options := i.resolveActionOptions(processSpec)
- conf, err := images.DecodeImageConfig(action, options, i.Proc.Cfg, i.Format)
+ options := strings.Fields(specProvider.ImageProcessSpec())
+ conf, err := images.DecodeImageConfig(options, i.Proc.Cfg, i.Format)
if err != nil {
return nil, err
}
@@ -307,25 +292,13 @@ func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) {
})
}
-func (i *imageResource) resolveActionOptions(spec string) (string, []string) {
- var action string
- options := strings.Fields(spec)
- for i, p := range options {
- if hstrings.InSlicEqualFold(imageActions, p) {
- action = p
- options = append(options[:i], options[i+1:]...)
- break
- }
- }
- return action, options
-}
-
func (i *imageResource) processActionSpec(action, spec string) (images.ImageResource, error) {
- return i.processActionOptions(action, strings.Fields(spec))
+ options := append([]string{action}, strings.Fields(strings.ToLower(spec))...)
+ return i.processOptions(options)
}
-func (i *imageResource) processActionOptions(action string, options []string) (images.ImageResource, error) {
- conf, err := images.DecodeImageConfig(action, options, i.Proc.Cfg, i.Format)
+func (i *imageResource) processOptions(options []string) (images.ImageResource, error) {
+ conf, err := images.DecodeImageConfig(options, i.Proc.Cfg, i.Format)
if err != nil {
return nil, err
}
@@ -337,13 +310,12 @@ func (i *imageResource) processActionOptions(action string, options []string) (i
return nil, err
}
- if action == images.ActionFill {
- if conf.Anchor == 0 && img.Width() == 0 || img.Height() == 0 {
+ if conf.Action == images.ActionFill {
+ if conf.Anchor == images.SmartCropAnchor && img.Width() == 0 || img.Height() == 0 {
// See https://github.com/gohugoio/hugo/issues/7955
// Smartcrop fails silently in some rare cases.
// Fall back to a center fill.
- conf.Anchor = gift.CenterAnchor
- conf.AnchorStr = "center"
+ conf = conf.Reanchor(gift.CenterAnchor)
return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
return i.Proc.ApplyFiltersFromConfig(src, conf)
})
@@ -411,7 +383,7 @@ func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src im
}
ci := i.clone(converted)
- targetPath := i.relTargetPathFromConfig(conf)
+ targetPath := i.relTargetPathFromConfig(conf, i.getSpec().imaging.Cfg.SourceHash)
ci.setTargetPath(targetPath)
ci.Format = conf.TargetFormat
ci.setMediaType(conf.TargetFormat.MediaType())
@@ -471,52 +443,38 @@ func (i *imageResource) clone(img image.Image) *imageResource {
}
func (i *imageResource) getImageMetaCacheTargetPath() string {
- const imageMetaVersionNumber = 1 // Increment to invalidate the meta cache
+ // Increment to invalidate the meta cache
+ // Last increment: v0.130.0 when change to the new imagemeta library for Exif.
+ const imageMetaVersionNumber = 2
cfgHash := i.getSpec().imaging.Cfg.SourceHash
df := i.getResourcePaths()
p1, _ := paths.FileAndExt(df.File)
h := i.hash()
- idStr := identity.HashString(h, i.size(), imageMetaVersionNumber, cfgHash)
+ idStr := hashing.HashStringHex(h, i.size(), imageMetaVersionNumber, cfgHash)
df.File = fmt.Sprintf("%s_%s.json", p1, idStr)
return df.TargetPath()
}
-func (i *imageResource) relTargetPathFromConfig(conf images.ImageConfig) internal.ResourcePaths {
+func (i *imageResource) relTargetPathFromConfig(conf images.ImageConfig, imagingConfigSourceHash string) internal.ResourcePaths {
p1, p2 := paths.FileAndExt(i.getResourcePaths().File)
if conf.TargetFormat != i.Format {
p2 = conf.TargetFormat.DefaultExtension()
}
- h := i.hash()
- idStr := fmt.Sprintf("_hu%s_%d", h, i.size())
+ // Do not change.
+ const imageHashPrefix = "_hu_"
- // Do not change for no good reason.
- const md5Threshold = 100
-
- key := conf.GetKey(i.Format)
-
- // It is useful to have the key in clear text, but when nesting transforms, it
- // can easily be too long to read, and maybe even too long
- // for the different OSes to handle.
- if len(p1)+len(idStr)+len(p2) > md5Threshold {
- key = helpers.MD5String(p1 + key + p2)
- huIdx := strings.Index(p1, "_hu")
- if huIdx != -1 {
- p1 = p1[:huIdx]
- } else {
- // This started out as a very long file name. Making it even longer
- // could melt ice in the Arctic.
- p1 = ""
- }
- } else if strings.Contains(p1, idStr) {
- // On scaling an already scaled image, we get the file info from the original.
- // Repeating the same info in the filename makes it stuttery for no good reason.
- idStr = ""
+ huIdx := strings.LastIndex(p1, imageHashPrefix)
+ incomingID := ""
+ if huIdx > -1 {
+ incomingID = p1[huIdx+len(imageHashPrefix):]
+ p1 = p1[:huIdx]
}
+ hash := hashing.HashStringHex(incomingID, i.hash(), conf.Key, imagingConfigSourceHash)
rp := i.getResourcePaths()
- rp.File = fmt.Sprintf("%s%s_%s%s", p1, idStr, key, p2)
+ rp.File = fmt.Sprintf("%s%s%s%s", p1, imageHashPrefix, hash, p2)
return rp
}
diff --git a/resources/image_cache.go b/resources/image_cache.go
index d824c5d1a..1fc722609 100644
--- a/resources/image_cache.go
+++ b/resources/image_cache.go
@@ -37,7 +37,7 @@ func (c *ImageCache) getOrCreate(
parent *imageResource, conf images.ImageConfig,
createImage func() (*imageResource, image.Image, error),
) (*resourceAdapter, error) {
- relTarget := parent.relTargetPathFromConfig(conf)
+ relTarget := parent.relTargetPathFromConfig(conf, parent.getSpec().imaging.Cfg.SourceHash)
relTargetPath := relTarget.TargetPath()
memKey := relTargetPath
diff --git a/resources/image_extended_test.go b/resources/image_extended_test.go
index 429e51fb6..5865ce75b 100644
--- a/resources/image_extended_test.go
+++ b/resources/image_extended_test.go
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build extended
-// +build extended
package resources_test
@@ -20,22 +19,28 @@ import (
"testing"
qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/htesting/hqt"
"github.com/gohugoio/hugo/media"
)
func TestImageResizeWebP(t *testing.T) {
c := qt.New(t)
- _, image := fetchImage(c, "sunset.webp")
+ _, image := fetchImage(c, "sunrise.webp")
c.Assert(image.MediaType(), qt.Equals, media.Builtin.WEBPType)
- c.Assert(image.RelPermalink(), qt.Equals, "/a/sunset.webp")
+ c.Assert(image.RelPermalink(), qt.Equals, "/a/sunrise.webp")
c.Assert(image.ResourceType(), qt.Equals, "image")
- c.Assert(image.Exif(), qt.IsNil)
+ exif := image.Exif()
+ c.Assert(exif, qt.Not(qt.IsNil))
+ c.Assert(exif.Tags["Copyright"], qt.Equals, "Bjørn Erik Pedersen")
+ c.Assert(exif.Lat, hqt.IsSameFloat64, 36.59744166666667)
+ c.Assert(exif.Long, hqt.IsSameFloat64, -4.50846)
+ c.Assert(exif.Date.IsZero(), qt.Equals, false)
resized, err := image.Resize("123x")
c.Assert(err, qt.IsNil)
c.Assert(image.MediaType(), qt.Equals, media.Builtin.WEBPType)
- c.Assert(resized.RelPermalink(), qt.Equals, "/a/sunset_hu36ee0b61ba924719ad36da960c273f96_59826_123x0_resize_q68_h2_linear_2.webp")
+ c.Assert(resized.RelPermalink(), qt.Equals, "/a/sunrise_hu_a1deb893888915d9.webp")
c.Assert(resized.Width(), qt.Equals, 123)
}
diff --git a/resources/image_test.go b/resources/image_test.go
index 7e26c1f55..ee5de8bec 100644
--- a/resources/image_test.go
+++ b/resources/image_test.go
@@ -16,31 +16,20 @@ package resources_test
import (
"context"
"fmt"
- "image"
- "image/gif"
"io/fs"
- "math/big"
"math/rand"
"os"
- "path/filepath"
- "runtime"
"strconv"
- "strings"
"sync"
"testing"
"time"
- "github.com/gohugoio/hugo/htesting"
- "github.com/gohugoio/hugo/resources/images/webp"
+ "github.com/bep/imagemeta"
"github.com/gohugoio/hugo/common/paths"
"github.com/spf13/afero"
- "github.com/disintegration/gift"
-
- "github.com/gohugoio/hugo/helpers"
-
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources/images"
"github.com/google/go-cmp/cmp"
@@ -67,8 +56,13 @@ var eq = qt.CmpEquals(
return m1.Type == m2.Type
}),
cmp.Comparer(
- func(v1, v2 *big.Rat) bool {
- return v1.RatString() == v2.RatString()
+ func(v1, v2 imagemeta.Rat[uint32]) bool {
+ return v1.String() == v2.String()
+ },
+ ),
+ cmp.Comparer(
+ func(v1, v2 imagemeta.Rat[int32]) bool {
+ return v1.String() == v2.String()
},
),
cmp.Comparer(func(v1, v2 time.Time) bool {
@@ -119,28 +113,28 @@ func TestImageTransformBasic(t *testing.T) {
assertWidthHeight(resizedAndRotated, 125, 200)
assertWidthHeight(resized, 300, 200)
- c.Assert(resized.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x200_resize_q68_linear.jpg")
+ c.Assert(resized.RelPermalink(), qt.Equals, "/a/sunset_hu_d2115125d9324a79.jpg")
fitted, err := resized.Fit("50x50")
c.Assert(err, qt.IsNil)
- c.Assert(fitted.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_625708021e2bb281c9f1002f88e4753f.jpg")
+ c.Assert(fitted.RelPermalink(), qt.Equals, "/a/sunset_hu_c2c98e06123b048e.jpg")
assertWidthHeight(fitted, 50, 33)
// Check the MD5 key threshold
fittedAgain, _ := fitted.Fit("10x20")
fittedAgain, err = fittedAgain.Fit("10x20")
c.Assert(err, qt.IsNil)
- c.Assert(fittedAgain.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_3f65ba24dc2b7fba0f56d7f104519157.jpg")
+ c.Assert(fittedAgain.RelPermalink(), qt.Equals, "/a/sunset_hu_dc9e89c10109de72.jpg")
assertWidthHeight(fittedAgain, 10, 7)
filled, err := image.Fill("200x100 bottomLeft")
c.Assert(err, qt.IsNil)
- c.Assert(filled.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x100_fill_q68_linear_bottomleft.jpg")
+ c.Assert(filled.RelPermalink(), qt.Equals, "/a/sunset_hu_b9f6d350738928fe.jpg")
assertWidthHeight(filled, 200, 100)
smart, err := image.Fill("200x100 smart")
c.Assert(err, qt.IsNil)
- c.Assert(smart.RelPermalink(), qt.Equals, fmt.Sprintf("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x100_fill_q68_linear_smart%d.jpg", 1))
+ c.Assert(smart.RelPermalink(), qt.Equals, "/a/sunset_hu_6fd390e7b0d26f0b.jpg")
assertWidthHeight(smart, 200, 100)
// Check cache
@@ -150,12 +144,12 @@ func TestImageTransformBasic(t *testing.T) {
cropped, err := image.Crop("300x300 topRight")
c.Assert(err, qt.IsNil)
- c.Assert(cropped.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x300_crop_q68_linear_topright.jpg")
+ c.Assert(cropped.RelPermalink(), qt.Equals, "/a/sunset_hu_3df036e11f4ddd43.jpg")
assertWidthHeight(cropped, 300, 300)
smartcropped, err := image.Crop("200x200 smart")
c.Assert(err, qt.IsNil)
- c.Assert(smartcropped.RelPermalink(), qt.Equals, fmt.Sprintf("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x200_crop_q68_linear_smart%d.jpg", 1))
+ c.Assert(smartcropped.RelPermalink(), qt.Equals, "/a/sunset_hu_12e2d26de89b464b.jpg")
assertWidthHeight(smartcropped, 200, 200)
// Check cache
@@ -222,7 +216,7 @@ func TestImageTransformFormat(t *testing.T) {
imagePng, err := image.Resize("450x png")
c.Assert(err, qt.IsNil)
- c.Assert(imagePng.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_450x0_resize_linear.png")
+ c.Assert(imagePng.RelPermalink(), qt.Equals, "/a/sunset_hu_e8b9444dcf2e75ef.png")
c.Assert(imagePng.ResourceType(), qt.Equals, "image")
assertExtWidthHeight(imagePng, ".png", 450, 281)
c.Assert(imagePng.Name(), qt.Equals, "sunset.jpg")
@@ -230,7 +224,7 @@ func TestImageTransformFormat(t *testing.T) {
imageGif, err := image.Resize("225x gif")
c.Assert(err, qt.IsNil)
- c.Assert(imageGif.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_225x0_resize_linear.gif")
+ c.Assert(imageGif.RelPermalink(), qt.Equals, "/a/sunset_hu_f80842d4c3789345.gif")
c.Assert(imageGif.ResourceType(), qt.Equals, "image")
assertExtWidthHeight(imageGif, ".gif", 225, 141)
c.Assert(imageGif.Name(), qt.Equals, "sunset.jpg")
@@ -253,7 +247,7 @@ func TestImagePermalinkPublishOrder(t *testing.T) {
}()
check1 := func(img images.ImageResource) {
- resizedLink := "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_100x50_resize_q75_box.jpg"
+ resizedLink := "/a/sunset_hu_3910bca82e28c9d6.jpg"
c.Assert(img.RelPermalink(), qt.Equals, resizedLink)
assertImageFile(c, spec.PublishFs, resizedLink, 100, 50)
}
@@ -294,12 +288,12 @@ func TestImageBugs(t *testing.T) {
c.Assert(err, qt.IsNil)
c.Assert(resized, qt.Not(qt.IsNil))
c.Assert(resized.Width(), qt.Equals, 200)
- c.Assert(resized.RelPermalink(), qt.Equals, "/a/_hu59e56ffff1bc1d8d122b1403d34e039f_90587_65b757a6e14debeae720fe8831f0a9bc.jpg")
+ c.Assert(resized.RelPermalink(), qt.Equals, "/a/1234567890qwertyuiopasdfghjklzxcvbnm5to6eeeeee7via8eleph_hu_951d3980b18c52a9.jpg")
resized, err = resized.Resize("100x")
c.Assert(err, qt.IsNil)
c.Assert(resized, qt.Not(qt.IsNil))
c.Assert(resized.Width(), qt.Equals, 100)
- c.Assert(resized.RelPermalink(), qt.Equals, "/a/_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c876768085288f41211f768147ba2647.jpg")
+ c.Assert(resized.RelPermalink(), qt.Equals, "/a/1234567890qwertyuiopasdfghjklzxcvbnm5to6eeeeee7via8eleph_hu_1daa203572ecd6ec.jpg")
})
// Issue #6137
@@ -354,13 +348,13 @@ func TestImageTransformConcurrent(t *testing.T) {
image := fetchImageForSpec(spec, c, "sunset.jpg")
- for i := 0; i < 4; i++ {
+ for i := range 4 {
wg.Add(1)
go func(id int) {
defer wg.Done()
- for j := 0; j < 5; j++ {
+ for j := range 5 {
img := image
- for k := 0; k < 2; k++ {
+ for k := range 2 {
r1, err := img.Resize(fmt.Sprintf("%dx", id-k))
if err != nil {
t.Error(err)
@@ -392,12 +386,12 @@ func TestImageResize8BitPNG(t *testing.T) {
c.Assert(image.MediaType().Type, qt.Equals, "image/png")
c.Assert(image.RelPermalink(), qt.Equals, "/a/gohugoio.png")
c.Assert(image.ResourceType(), qt.Equals, "image")
- c.Assert(image.Exif(), qt.IsNil)
+ c.Assert(image.Exif(), qt.IsNotNil)
resized, err := image.Resize("800x")
c.Assert(err, qt.IsNil)
c.Assert(resized.MediaType().Type, qt.Equals, "image/png")
- c.Assert(resized.RelPermalink(), qt.Equals, "/a/gohugoio_hu0e1b9e4a4be4d6f86c7b37b9ccce3fbc_73886_800x0_resize_linear_3.png")
+ c.Assert(resized.RelPermalink(), qt.Equals, "/a/gohugoio_hu_fe2b762e9cac406c.png")
c.Assert(resized.Width(), qt.Equals, 800)
}
@@ -443,6 +437,7 @@ func TestImageExif(t *testing.T) {
c.Assert(lensModel, qt.Equals, "smc PENTAX-DA* 16-50mm F2.8 ED AL [IF] SDM")
resized, _ := image.Resize("300x200")
x2 := resized.Exif()
+
c.Assert(x2, eq, x)
}
@@ -504,7 +499,7 @@ func BenchmarkImageExif(b *testing.B) {
b.StartTimer()
for i := 0; i < b.N; i++ {
- for j := 0; j < 10; j++ {
+ for range 10 {
getAndCheckExif(c, images[i])
}
}
@@ -528,320 +523,6 @@ func BenchmarkImageExif(b *testing.B) {
})
}
-// usesFMA indicates whether "fused multiply and add" (FMA) instruction is
-// used. The command "grep FMADD go/test/codegen/floats.go" can help keep
-// the FMA-using architecture list updated.
-var usesFMA = runtime.GOARCH == "s390x" ||
- runtime.GOARCH == "ppc64" ||
- runtime.GOARCH == "ppc64le" ||
- runtime.GOARCH == "arm64" ||
- runtime.GOARCH == "riscv64"
-
-// goldenEqual compares two NRGBA images. It is used in golden tests only.
-// A small tolerance is allowed on architectures using "fused multiply and add"
-// (FMA) instruction to accommodate for floating-point rounding differences
-// with control golden images that were generated on amd64 architecture.
-// See https://golang.org/ref/spec#Floating_point_operators
-// and https://github.com/gohugoio/hugo/issues/6387 for more information.
-//
-// Borrowed from https://github.com/disintegration/gift/blob/a999ff8d5226e5ab14b64a94fca07c4ac3f357cf/gift_test.go#L598-L625
-// Copyright (c) 2014-2019 Grigory Dryapak
-// Licensed under the MIT License.
-func goldenEqual(img1, img2 *image.NRGBA) bool {
- maxDiff := 0
- if usesFMA {
- maxDiff = 1
- }
- if !img1.Rect.Eq(img2.Rect) {
- return false
- }
- if len(img1.Pix) != len(img2.Pix) {
- return false
- }
- for i := 0; i < len(img1.Pix); i++ {
- diff := int(img1.Pix[i]) - int(img2.Pix[i])
- if diff < 0 {
- diff = -diff
- }
- if diff > maxDiff {
- return false
- }
- }
- return true
-}
-
-// Issue #8729
-func TestImageOperationsGoldenWebp(t *testing.T) {
- if !htesting.IsCI() {
- t.Skip("skip long running test in local mode")
- }
- if !webp.Supports() {
- t.Skip("skip webp test")
- }
- c := qt.New(t)
- c.Parallel()
-
- devMode := false
-
- testImages := []string{"fuzzy-cirlcle.png"}
-
- spec, workDir := newTestResourceOsFs(c)
- defer func() {
- if !devMode {
- os.Remove(workDir)
- }
- }()
-
- if devMode {
- fmt.Println(workDir)
- }
-
- for _, imageName := range testImages {
- image := fetchImageForSpec(spec, c, imageName)
- imageWebp, err := image.Resize("200x webp")
- c.Assert(err, qt.IsNil)
- c.Assert(imageWebp.Width(), qt.Equals, 200)
- }
-
- if devMode {
- return
- }
-
- dir1 := filepath.Join(workDir, "resources/_gen/images/a")
- dir2 := filepath.FromSlash("testdata/golden_webp")
-
- assetGoldenDirs(c, dir1, dir2)
-}
-
-func TestImageOperationsGolden(t *testing.T) {
- if !htesting.IsCI() {
- t.Skip("skip long running test in local mode")
- }
- c := qt.New(t)
- c.Parallel()
-
- // Note, if you're enabling this on a MacOS M1 (ARM) you need to run the test with GOARCH=amd64.
- // GOARCH=amd64 go test -count 1 -timeout 30s -run "^TestImageOperationsGolden$" ./resources -v
- // The above will print out a folder.
- // Replace testdata/golden with resources/_gen/images in that folder.
- devMode := false
-
- testImages := []string{"sunset.jpg", "gohugoio8.png", "gohugoio24.png"}
-
- spec, workDir := newTestResourceOsFs(c)
- defer func() {
- if !devMode {
- os.Remove(workDir)
- }
- }()
-
- if devMode {
- fmt.Println(workDir)
- }
-
- gopher := fetchImageForSpec(spec, c, "gopher-hero8.png")
- var err error
- gopher, err = gopher.Resize("30x")
- c.Assert(err, qt.IsNil)
-
- f := &images.Filters{}
-
- sunset := fetchImageForSpec(spec, c, "sunset.jpg")
-
- // Test PNGs with alpha channel.
- for _, img := range []string{"gopher-hero8.png", "gradient-circle.png"} {
- orig := fetchImageForSpec(spec, c, img)
- for _, resizeSpec := range []string{"200x #e3e615", "200x jpg #e3e615"} {
- resized, err := orig.Resize(resizeSpec)
- c.Assert(err, qt.IsNil)
- rel := resized.RelPermalink()
-
- c.Assert(rel, qt.Not(qt.Equals), "")
-
- }
-
- // Check the Opacity filter.
- opacity30, err := orig.Filter(f.Opacity(30))
- c.Assert(err, qt.IsNil)
- overlay, err := sunset.Filter(f.Overlay(opacity30.(images.ImageSource), 20, 20))
- c.Assert(err, qt.IsNil)
- rel := overlay.RelPermalink()
- c.Assert(rel, qt.Not(qt.Equals), "")
-
- }
-
- // A simple Gif file (no animation).
- orig := fetchImageForSpec(spec, c, "gohugoio-card.gif")
- for _, width := range []int{100, 220} {
- resized, err := orig.Resize(fmt.Sprintf("%dx", width))
- c.Assert(err, qt.IsNil)
- rel := resized.RelPermalink()
- c.Assert(rel, qt.Not(qt.Equals), "")
- c.Assert(resized.Width(), qt.Equals, width)
- }
-
- // Animated GIF
- orig = fetchImageForSpec(spec, c, "giphy.gif")
- for _, resizeSpec := range []string{"200x", "512x", "100x jpg"} {
- resized, err := orig.Resize(resizeSpec)
- c.Assert(err, qt.IsNil)
- rel := resized.RelPermalink()
- c.Assert(rel, qt.Not(qt.Equals), "")
- }
-
- for _, img := range testImages {
-
- orig := fetchImageForSpec(spec, c, img)
- for _, resizeSpec := range []string{"200x100", "600x", "200x r90 q50 Box"} {
- resized, err := orig.Resize(resizeSpec)
- c.Assert(err, qt.IsNil)
- rel := resized.RelPermalink()
- c.Assert(rel, qt.Not(qt.Equals), "")
- }
-
- for _, fillSpec := range []string{"300x200 Gaussian Smart", "100x100 Center", "300x100 TopLeft NearestNeighbor", "400x200 BottomLeft"} {
- resized, err := orig.Fill(fillSpec)
- c.Assert(err, qt.IsNil)
- rel := resized.RelPermalink()
- c.Assert(rel, qt.Not(qt.Equals), "")
- }
-
- for _, fitSpec := range []string{"300x200 Linear"} {
- resized, err := orig.Fit(fitSpec)
- c.Assert(err, qt.IsNil)
- rel := resized.RelPermalink()
- c.Assert(rel, qt.Not(qt.Equals), "")
- }
-
- filters := []gift.Filter{
- f.Grayscale(),
- f.GaussianBlur(6),
- f.Saturation(50),
- f.Sepia(100),
- f.Brightness(30),
- f.ColorBalance(10, -10, -10),
- f.Colorize(240, 50, 100),
- f.Gamma(1.5),
- f.UnsharpMask(1, 1, 0),
- f.Sigmoid(0.5, 7),
- f.Pixelate(5),
- f.Invert(),
- f.Hue(22),
- f.Contrast(32.5),
- f.Overlay(gopher.(images.ImageSource), 20, 30),
- f.Text("No options"),
- f.Text("This long text is to test line breaks. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."),
- f.Text("Hugo rocks!", map[string]any{"x": 3, "y": 3, "size": 20, "color": "#fc03b1"}),
- }
-
- resized, err := orig.Fill("400x200 center")
- c.Assert(err, qt.IsNil)
-
- for _, filter := range filters {
- resized, err := resized.Filter(filter)
- c.Assert(err, qt.IsNil)
- rel := resized.RelPermalink()
- c.Assert(rel, qt.Not(qt.Equals), "")
- }
-
- resized, err = resized.Filter(filters[0:4])
- c.Assert(err, qt.IsNil)
- rel := resized.RelPermalink()
- c.Assert(rel, qt.Not(qt.Equals), "")
- }
-
- if devMode {
- return
- }
-
- dir1 := filepath.Join(workDir, "resources/_gen/images/a/")
- dir2 := filepath.FromSlash("testdata/golden")
-
- assetGoldenDirs(c, dir1, dir2)
-}
-
-func assetGoldenDirs(c *qt.C, dir1, dir2 string) {
- // The two dirs above should now be the same.
- dirinfos1, err := os.ReadDir(dir1)
- c.Assert(err, qt.IsNil)
- dirinfos2, err := os.ReadDir(dir2)
- c.Assert(err, qt.IsNil)
- c.Assert(len(dirinfos1), qt.Equals, len(dirinfos2))
-
- for i, fi1 := range dirinfos1 {
- fi2 := dirinfos2[i]
- c.Assert(fi1.Name(), qt.Equals, fi2.Name(), qt.Commentf("i=%d", i))
-
- f1, err := os.Open(filepath.Join(dir1, fi1.Name()))
- c.Assert(err, qt.IsNil)
- f2, err := os.Open(filepath.Join(dir2, fi2.Name()))
- c.Assert(err, qt.IsNil)
-
- decodeAll := func(f *os.File) []image.Image {
- var images []image.Image
-
- if strings.HasSuffix(f.Name(), ".gif") {
- gif, err := gif.DecodeAll(f)
- c.Assert(err, qt.IsNil)
- images = make([]image.Image, len(gif.Image))
- for i, img := range gif.Image {
- images[i] = img
- }
- } else {
- img, _, err := image.Decode(f)
- c.Assert(err, qt.IsNil)
- images = append(images, img)
- }
- return images
- }
-
- imgs1 := decodeAll(f1)
- imgs2 := decodeAll(f2)
- c.Assert(len(imgs1), qt.Equals, len(imgs2))
-
- LOOP:
- for i, img1 := range imgs1 {
- img2 := imgs2[i]
- nrgba1 := image.NewNRGBA(img1.Bounds())
- gift.New().Draw(nrgba1, img1)
- nrgba2 := image.NewNRGBA(img2.Bounds())
- gift.New().Draw(nrgba2, img2)
-
- if !goldenEqual(nrgba1, nrgba2) {
- switch fi1.Name() {
- case "gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_73c19c5f80881858a85aa23cd0ca400d.png",
- "gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_ae631e5252bb5d7b92bc766ad1a89069.png",
- "gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_d1bbfa2629bffb90118cacce3fcfb924.png",
- "giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_200x0_resize_box_1.gif":
- c.Log("expectedly differs from golden due to dithering:", fi1.Name())
- default:
- c.Errorf("resulting image differs from golden: %s", fi1.Name())
- break LOOP
- }
- }
- }
-
- if !usesFMA {
- c.Assert(fi1, eq, fi2)
-
- _, err = f1.Seek(0, 0)
- c.Assert(err, qt.IsNil)
- _, err = f2.Seek(0, 0)
- c.Assert(err, qt.IsNil)
-
- hash1, err := helpers.MD5FromReader(f1)
- c.Assert(err, qt.IsNil)
- hash2, err := helpers.MD5FromReader(f2)
- c.Assert(err, qt.IsNil)
-
- c.Assert(hash1, qt.Equals, hash2)
- }
-
- f1.Close()
- f2.Close()
- }
-}
-
func BenchmarkResizeParallel(b *testing.B) {
c := qt.New(b)
_, img := fetchSunset(c)
diff --git a/resources/images/auto_orient.go b/resources/images/auto_orient.go
index ed86979e1..a4a61976d 100644
--- a/resources/images/auto_orient.go
+++ b/resources/images/auto_orient.go
@@ -19,6 +19,7 @@ import (
"github.com/disintegration/gift"
"github.com/gohugoio/hugo/resources/images/exif"
+ "github.com/spf13/cast"
)
var _ gift.Filter = (*autoOrientFilter)(nil)
@@ -49,7 +50,8 @@ func (f autoOrientFilter) Bounds(srcBounds image.Rectangle) image.Rectangle {
func (f autoOrientFilter) AutoOrient(exifInfo *exif.ExifInfo) gift.Filter {
if exifInfo != nil {
- if orientation, ok := exifInfo.Tags["Orientation"].(int); ok {
+ if v, ok := exifInfo.Tags["Orientation"]; ok {
+ orientation := cast.ToInt(v)
if filter, ok := transformationFilters[orientation]; ok {
return filter
}
diff --git a/resources/images/color.go b/resources/images/color.go
index e2ff2377f..c7f3b9eb6 100644
--- a/resources/images/color.go
+++ b/resources/images/color.go
@@ -22,6 +22,7 @@ import (
"strings"
"github.com/gohugoio/hugo/common/hstrings"
+ "slices"
)
type colorGoProvider interface {
@@ -91,11 +92,8 @@ func (c Color) toSRGB(i uint8) float64 {
// that the palette is valid for the relevant format.
func AddColorToPalette(c color.Color, p color.Palette) color.Palette {
var found bool
- for _, cc := range p {
- if c == cc {
- found = true
- break
- }
+ if slices.Contains(p, c) {
+ found = true
}
if !found {
diff --git a/resources/images/config.go b/resources/images/config.go
index 9655e9a51..6fcd2e334 100644
--- a/resources/images/config.go
+++ b/resources/images/config.go
@@ -20,6 +20,7 @@ import (
"strconv"
"strings"
+ "github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/media"
@@ -37,6 +38,13 @@ const (
ActionFill = "fill"
)
+var Actions = map[string]bool{
+ ActionResize: true,
+ ActionCrop: true,
+ ActionFit: true,
+ ActionFill: true,
+}
+
var (
imageFormats = map[string]Format{
".jpg": JPEG,
@@ -64,9 +72,9 @@ var (
// Add or increment if changes to an image format's processing requires
// re-generation.
imageFormatsVersions = map[Format]int{
- PNG: 3, // Fix transparency issue with 32 bit images.
- WEBP: 2, // Fix transparency issue with 32 bit images.
- GIF: 1, // Fix resize issue with animated GIFs when target != GIF.
+ PNG: 0,
+ WEBP: 0,
+ GIF: 0,
}
// Increment to mark all processed images as stale. Only use when absolutely needed.
@@ -84,6 +92,7 @@ var anchorPositions = map[string]gift.Anchor{
strings.ToLower("BottomLeft"): gift.BottomLeftAnchor,
strings.ToLower("Bottom"): gift.BottomAnchor,
strings.ToLower("BottomRight"): gift.BottomRightAnchor,
+ smartCropIdentifier: SmartCropAnchor,
}
// These encoding hints are currently only relevant for Webp.
@@ -176,7 +185,7 @@ func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, Ima
return i, nil, err
}
- if i.Imaging.Anchor != "" && i.Imaging.Anchor != smartCropIdentifier {
+ if i.Imaging.Anchor != "" {
anchor, found := anchorPositions[i.Imaging.Anchor]
if !found {
return i, nil, fmt.Errorf("invalid anchor value %q in imaging config", i.Anchor)
@@ -201,36 +210,34 @@ func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, Ima
return ns, nil
}
-func DecodeImageConfig(action string, options []string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal], sourceFormat Format) (ImageConfig, error) {
+func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal], sourceFormat Format) (ImageConfig, error) {
var (
- c ImageConfig = GetDefaultImageConfig(action, defaults)
+ c ImageConfig = GetDefaultImageConfig(defaults)
err error
)
- action = strings.ToLower(action)
-
- c.Action = action
-
- if options == nil {
- return c, errors.New("image options cannot be empty")
+ // Make to lower case, trim space and remove any empty strings.
+ n := 0
+ for _, s := range options {
+ s = strings.TrimSpace(s)
+ if s != "" {
+ options[n] = strings.ToLower(s)
+ n++
+ }
}
+ options = options[:n]
for _, part := range options {
- part = strings.ToLower(part)
-
- if part == smartCropIdentifier {
- c.AnchorStr = smartCropIdentifier
+ if _, ok := Actions[part]; ok {
+ c.Action = part
} else if pos, ok := anchorPositions[part]; ok {
c.Anchor = pos
- c.AnchorStr = part
} else if filter, ok := imageFilters[part]; ok {
c.Filter = filter
- c.FilterStr = part
} else if hint, ok := hints[part]; ok {
c.Hint = hint
} else if part[0] == '#' {
- c.BgColorStr = part[1:]
- c.BgColor, err = hexStringToColorGo(c.BgColorStr)
+ c.BgColor, err = hexStringToColorGo(part[1:])
if err != nil {
return c, err
}
@@ -291,8 +298,7 @@ func DecodeImageConfig(action string, options []string, defaults *config.ConfigN
}
}
- if action != "" && c.FilterStr == "" {
- c.FilterStr = defaults.Config.Imaging.ResampleFilter
+ if c.Action != "" && c.Filter == nil {
c.Filter = defaults.Config.ResampleFilter
}
@@ -300,8 +306,7 @@ func DecodeImageConfig(action string, options []string, defaults *config.ConfigN
c.Hint = webpoptions.EncodingPresetPhoto
}
- if action != "" && c.AnchorStr == "" {
- c.AnchorStr = defaults.Config.Imaging.Anchor
+ if c.Action != "" && c.Anchor == -1 {
c.Anchor = defaults.Config.Anchor
}
@@ -318,10 +323,23 @@ func DecodeImageConfig(action string, options []string, defaults *config.ConfigN
if c.BgColor == nil && c.TargetFormat != sourceFormat {
if sourceFormat.SupportsTransparency() && !c.TargetFormat.SupportsTransparency() {
c.BgColor = defaults.Config.BgColor
- c.BgColorStr = defaults.Config.Imaging.BgColor
}
}
+ if mainImageVersionNumber > 0 {
+ options = append(options, strconv.Itoa(mainImageVersionNumber))
+ }
+
+ if v, ok := imageFormatsVersions[sourceFormat]; ok && v > 0 {
+ options = append(options, strconv.Itoa(v))
+ }
+
+ if smartCropVersionNumber > 0 && c.Anchor == SmartCropAnchor {
+ options = append(options, strconv.Itoa(smartCropVersionNumber))
+ }
+
+ c.Key = hashing.HashStringHex(options)
+
return c, nil
}
@@ -350,8 +368,7 @@ type ImageConfig struct {
// not support transparency.
// When set per image operation, it's used even for formats that does support
// transparency.
- BgColor color.Color
- BgColorStr string
+ BgColor color.Color
// Hint about what type of picture this is. Used to optimize encoding
// when target is set to webp.
@@ -360,57 +377,15 @@ type ImageConfig struct {
Width int
Height int
- Filter gift.Resampling
- FilterStr string
+ Filter gift.Resampling
- Anchor gift.Anchor
- AnchorStr string
+ Anchor gift.Anchor
}
-func (i ImageConfig) GetKey(format Format) string {
- if i.Key != "" {
- return i.Action + "_" + i.Key
- }
-
- k := strconv.Itoa(i.Width) + "x" + strconv.Itoa(i.Height)
- if i.Action != "" {
- k += "_" + i.Action
- }
- // This slightly odd construct is here to preserve the old image keys.
- if i.qualitySetForImage || i.TargetFormat.RequiresDefaultQuality() {
- k += "_q" + strconv.Itoa(i.Quality)
- }
- if i.Rotate != 0 {
- k += "_r" + strconv.Itoa(i.Rotate)
- }
- if i.BgColorStr != "" {
- k += "_bg" + i.BgColorStr
- }
-
- if i.TargetFormat == WEBP {
- k += "_h" + strconv.Itoa(int(i.Hint))
- }
-
- anchor := i.AnchorStr
- if anchor == smartCropIdentifier {
- anchor = anchor + strconv.Itoa(smartCropVersionNumber)
- }
-
- k += "_" + i.FilterStr
-
- if i.Action == ActionFill || i.Action == ActionCrop {
- k += "_" + anchor
- }
-
- if v, ok := imageFormatsVersions[format]; ok {
- k += "_" + strconv.Itoa(v)
- }
-
- if mainImageVersionNumber > 0 {
- k += "_" + strconv.Itoa(mainImageVersionNumber)
- }
-
- return k
+func (cfg ImageConfig) Reanchor(a gift.Anchor) ImageConfig {
+ cfg.Anchor = a
+ cfg.Key = hashing.HashStringHex(cfg.Key, "reanchor", a)
+ return cfg
}
type ImagingConfigInternal struct {
@@ -429,7 +404,7 @@ func (i *ImagingConfigInternal) Compile(externalCfg *ImagingConfig) error {
return err
}
- if externalCfg.Anchor != "" && externalCfg.Anchor != smartCropIdentifier {
+ if externalCfg.Anchor != "" {
anchor, found := anchorPositions[externalCfg.Anchor]
if !found {
return fmt.Errorf("invalid anchor value %q in imaging config", i.Anchor)
diff --git a/resources/images/config_test.go b/resources/images/config_test.go
index 6dd545f2c..d3c9827bd 100644
--- a/resources/images/config_test.go
+++ b/resources/images/config_test.go
@@ -19,6 +19,7 @@ import (
"testing"
qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/common/hashing"
)
func TestDecodeConfig(t *testing.T) {
@@ -106,7 +107,8 @@ func TestDecodeImageConfig(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- result, err := DecodeImageConfig(this.action, strings.Fields(this.in), cfg, PNG)
+ options := append([]string{this.action}, strings.Fields(this.in)...)
+ result, err := DecodeImageConfig(options, cfg, PNG)
if b, ok := this.expect.(bool); ok && !b {
if err == nil {
t.Errorf("[%d] parseImageConfig didn't return an expected error", i)
@@ -115,15 +117,19 @@ func TestDecodeImageConfig(t *testing.T) {
if err != nil {
t.Fatalf("[%d] err: %s", i, err)
}
- if fmt.Sprint(result) != fmt.Sprint(this.expect) {
- t.Fatalf("[%d] got\n%v\n but expected\n%v", i, result, this.expect)
+ expect := this.expect.(ImageConfig)
+ expect.Key = hashing.HashStringHex(options)
+
+ if fmt.Sprint(result) != fmt.Sprint(expect) {
+ t.Fatalf("[%d] got\n%v\n but expected\n%v", i, result, expect)
}
}
}
}
func newImageConfig(action string, width, height, quality, rotate int, filter, anchor, bgColor string) ImageConfig {
- var c ImageConfig = GetDefaultImageConfig(action, nil)
+ var c ImageConfig = GetDefaultImageConfig(nil)
+ c.Action = action
c.TargetFormat = PNG
c.Hint = 2
c.Width = width
@@ -131,26 +137,20 @@ func newImageConfig(action string, width, height, quality, rotate int, filter, a
c.Quality = quality
c.qualitySetForImage = quality != 75
c.Rotate = rotate
- c.BgColorStr = bgColor
c.BgColor, _ = hexStringToColorGo(bgColor)
+ c.Anchor = SmartCropAnchor
if filter != "" {
filter = strings.ToLower(filter)
if v, ok := imageFilters[filter]; ok {
c.Filter = v
- c.FilterStr = filter
}
}
if anchor != "" {
- if anchor == smartCropIdentifier {
- c.AnchorStr = anchor
- } else {
- anchor = strings.ToLower(anchor)
- if v, ok := anchorPositions[anchor]; ok {
- c.Anchor = v
- c.AnchorStr = anchor
- }
+ anchor = strings.ToLower(anchor)
+ if v, ok := anchorPositions[anchor]; ok {
+ c.Anchor = v
}
}
diff --git a/resources/images/exif/exif.go b/resources/images/exif/exif.go
index af92366ca..a7f0e0757 100644
--- a/resources/images/exif/exif.go
+++ b/resources/images/exif/exif.go
@@ -14,24 +14,18 @@
package exif
import (
- "bytes"
"fmt"
"io"
- "math/big"
"regexp"
+ "strconv"
"strings"
"time"
- "unicode"
- "unicode/utf8"
+ "github.com/bep/imagemeta"
+ "github.com/bep/logg"
"github.com/bep/tmc"
-
- _exif "github.com/rwcarlsen/goexif/exif"
- "github.com/rwcarlsen/goexif/tiff"
)
-const exifTimeLayout = "2006:01:02 15:04:05"
-
// ExifInfo holds the decoded Exif data for an Image.
type ExifInfo struct {
// GPS latitude in degrees.
@@ -52,6 +46,15 @@ type Decoder struct {
excludeFieldsrRe *regexp.Regexp
noDate bool
noLatLong bool
+ warnl logg.LevelLogger
+}
+
+func (d *Decoder) shouldInclude(s string) bool {
+ return (d.includeFieldsRe == nil || d.includeFieldsRe.MatchString(s))
+}
+
+func (d *Decoder) shouldExclude(s string) bool {
+ return d.excludeFieldsrRe != nil && d.excludeFieldsrRe.MatchString(s)
}
func IncludeFields(expression string) func(*Decoder) error {
@@ -90,6 +93,13 @@ func WithDateDisabled(disabled bool) func(*Decoder) error {
}
}
+func WithWarnLogger(warnl logg.LevelLogger) func(*Decoder) error {
+ return func(d *Decoder) error {
+ d.warnl = warnl
+ return nil
+ }
+}
+
func compileRegexp(expression string) (*regexp.Regexp, error) {
expression = strings.TrimSpace(expression)
if expression == "" {
@@ -114,142 +124,222 @@ func NewDecoder(options ...func(*Decoder) error) (*Decoder, error) {
return d, nil
}
-func (d *Decoder) Decode(r io.Reader) (ex *ExifInfo, err error) {
+var (
+ isTimeTag = func(s string) bool {
+ return strings.Contains(s, "Time")
+ }
+ isGPSTag = func(s string) bool {
+ return strings.HasPrefix(s, "GPS")
+ }
+)
+
+// Filename is only used for logging.
+func (d *Decoder) Decode(filename string, format imagemeta.ImageFormat, r io.Reader) (ex *ExifInfo, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("exif failed: %v", r)
}
}()
- var x *_exif.Exif
- x, err = _exif.Decode(r)
- if err != nil {
- if err.Error() == "EOF" {
- // Found no Exif
- return nil, nil
- }
- return
+ var tagInfos imagemeta.Tags
+ handleTag := func(ti imagemeta.TagInfo) error {
+ tagInfos.Add(ti)
+ return nil
}
+ shouldInclude := func(ti imagemeta.TagInfo) bool {
+ if ti.Source == imagemeta.EXIF {
+ if !d.noDate {
+ // We need the time tags to calculate the date.
+ if isTimeTag(ti.Tag) {
+ return true
+ }
+ }
+ if !d.noLatLong {
+ // We need to GPS tags to calculate the lat/long.
+ if isGPSTag(ti.Tag) {
+ return true
+ }
+ }
+
+ if !strings.HasPrefix(ti.Namespace, "IFD0") {
+ // Drop thumbnail tags.
+ return false
+ }
+ }
+
+ if d.shouldExclude(ti.Tag) {
+ return false
+ }
+
+ return d.shouldInclude(ti.Tag)
+ }
+
+ var warnf func(string, ...any)
+ if d.warnl != nil {
+ // There should be very little warnings (fingers crossed!),
+ // but this will typically be unrecognized formats.
+ // To be able to possibly get rid of these warnings,
+ // we need to know what images are causing them.
+ warnf = func(format string, args ...any) {
+ format = fmt.Sprintf("%q: %s: ", filename, format)
+ d.warnl.Logf(format, args...)
+ }
+ }
+
+ err = imagemeta.Decode(
+ imagemeta.Options{
+ R: r.(io.ReadSeeker),
+ ImageFormat: format,
+ ShouldHandleTag: shouldInclude,
+ HandleTag: handleTag,
+ Sources: imagemeta.EXIF, // For now. TODO(bep)
+ Warnf: warnf,
+ },
+ )
+
var tm time.Time
var lat, long float64
if !d.noDate {
- tm, _ = x.DateTime()
+ tm, _ = tagInfos.GetDateTime()
}
if !d.noLatLong {
- lat, long, _ = x.LatLong()
+ lat, long, _ = tagInfos.GetLatLong()
}
- walker := &exifWalker{x: x, vals: make(map[string]any), includeMatcher: d.includeFieldsRe, excludeMatcher: d.excludeFieldsrRe}
- if err = x.Walk(walker); err != nil {
- return
+ tags := make(map[string]any)
+ for k, v := range tagInfos.All() {
+ if d.shouldExclude(k) {
+ continue
+ }
+ if !d.shouldInclude(k) {
+ continue
+ }
+ tags[k] = v.Value
}
- ex = &ExifInfo{Lat: lat, Long: long, Date: tm, Tags: walker.vals}
+ ex = &ExifInfo{Lat: lat, Long: long, Date: tm, Tags: tags}
return
}
-func decodeTag(x *_exif.Exif, f _exif.FieldName, t *tiff.Tag) (any, error) {
- switch t.Format() {
- case tiff.StringVal, tiff.UndefVal:
- s := nullString(t.Val)
- if strings.Contains(string(f), "DateTime") {
- if d, err := tryParseDate(x, s); err == nil {
- return d, nil
- }
- }
- return s, nil
- case tiff.OtherVal:
- return "unknown", nil
- }
-
- var rv []any
-
- for i := 0; i < int(t.Count); i++ {
- switch t.Format() {
- case tiff.RatVal:
- n, d, _ := t.Rat2(i)
- rat := big.NewRat(n, d)
- // if t is int or t > 1, use float64
- if rat.IsInt() || rat.Cmp(big.NewRat(1, 1)) == 1 {
- f, _ := rat.Float64()
- rv = append(rv, f)
- } else {
- rv = append(rv, rat)
- }
-
- case tiff.FloatVal:
- v, _ := t.Float(i)
- rv = append(rv, v)
- case tiff.IntVal:
- v, _ := t.Int(i)
- rv = append(rv, v)
- }
- }
-
- if t.Count == 1 {
- if len(rv) == 1 {
- return rv[0], nil
- }
- }
-
- return rv, nil
-}
-
-// Code borrowed from exif.DateTime and adjusted.
-func tryParseDate(x *_exif.Exif, s string) (time.Time, error) {
- dateStr := strings.TrimRight(s, "\x00")
- // TODO(bep): look for timezone offset, GPS time, etc.
- timeZone := time.Local
- if tz, _ := x.TimeZone(); tz != nil {
- timeZone = tz
- }
- return time.ParseInLocation(exifTimeLayout, dateStr, timeZone)
-}
-
-type exifWalker struct {
- x *_exif.Exif
- vals map[string]any
- includeMatcher *regexp.Regexp
- excludeMatcher *regexp.Regexp
-}
-
-func (e *exifWalker) Walk(f _exif.FieldName, tag *tiff.Tag) error {
- name := string(f)
- if e.excludeMatcher != nil && e.excludeMatcher.MatchString(name) {
- return nil
- }
- if e.includeMatcher != nil && !e.includeMatcher.MatchString(name) {
- return nil
- }
- val, err := decodeTag(e.x, f, tag)
- if err != nil {
- return err
- }
- e.vals[name] = val
- return nil
-}
-
-func nullString(in []byte) string {
- var rv bytes.Buffer
- for len(in) > 0 {
- r, size := utf8.DecodeRune(in)
- if unicode.IsGraphic(r) {
- rv.WriteRune(r)
- }
- in = in[size:]
- }
- return rv.String()
-}
-
var tcodec *tmc.Codec
func init() {
+ newIntadapter := func(target any) tmc.Adapter {
+ var bitSize int
+ var isSigned bool
+
+ switch target.(type) {
+ case int:
+ bitSize = 0
+ isSigned = true
+ case int8:
+ bitSize = 8
+ isSigned = true
+ case int16:
+ bitSize = 16
+ isSigned = true
+ case int32:
+ bitSize = 32
+ isSigned = true
+ case int64:
+ bitSize = 64
+ isSigned = true
+ case uint:
+ bitSize = 0
+ case uint8:
+ bitSize = 8
+ case uint16:
+ bitSize = 16
+ case uint32:
+ bitSize = 32
+ case uint64:
+ bitSize = 64
+ }
+
+ intFromString := func(s string) (any, error) {
+ if bitSize == 0 {
+ return strconv.Atoi(s)
+ }
+
+ var v any
+ var err error
+
+ if isSigned {
+ v, err = strconv.ParseInt(s, 10, bitSize)
+ } else {
+ v, err = strconv.ParseUint(s, 10, bitSize)
+ }
+
+ if err != nil {
+ return 0, err
+ }
+
+ if isSigned {
+ i := v.(int64)
+ switch target.(type) {
+ case int:
+ return int(i), nil
+ case int8:
+ return int8(i), nil
+ case int16:
+ return int16(i), nil
+ case int32:
+ return int32(i), nil
+ case int64:
+ return i, nil
+ }
+ }
+
+ i := v.(uint64)
+ switch target.(type) {
+ case uint:
+ return uint(i), nil
+ case uint8:
+ return uint8(i), nil
+ case uint16:
+ return uint16(i), nil
+ case uint32:
+ return uint32(i), nil
+ case uint64:
+ return i, nil
+
+ }
+
+ return 0, fmt.Errorf("unsupported target type %T", target)
+ }
+
+ intToString := func(v any) (string, error) {
+ return fmt.Sprintf("%d", v), nil
+ }
+
+ return tmc.NewAdapter(target, intFromString, intToString)
+ }
+
+ ru, _ := imagemeta.NewRat[uint32](1, 2)
+ ri, _ := imagemeta.NewRat[int32](1, 2)
+ tmcAdapters := []tmc.Adapter{
+ tmc.NewAdapter(ru, nil, nil),
+ tmc.NewAdapter(ri, nil, nil),
+ newIntadapter(int(1)),
+ newIntadapter(int8(1)),
+ newIntadapter(int16(1)),
+ newIntadapter(int32(1)),
+ newIntadapter(int64(1)),
+ newIntadapter(uint(1)),
+ newIntadapter(uint8(1)),
+ newIntadapter(uint16(1)),
+ newIntadapter(uint32(1)),
+ newIntadapter(uint64(1)),
+ }
+
+ tmcAdapters = append(tmc.DefaultTypeAdapters, tmcAdapters...)
+
var err error
- tcodec, err = tmc.New()
+ tcodec, err = tmc.New(tmc.WithTypeAdapters(tmcAdapters))
if err != nil {
panic(err)
}
diff --git a/resources/images/exif/exif_test.go b/resources/images/exif/exif_test.go
index 64c5a39e3..278bc761a 100644
--- a/resources/images/exif/exif_test.go
+++ b/resources/images/exif/exif_test.go
@@ -15,13 +15,12 @@ package exif
import (
"encoding/json"
- "math/big"
"os"
"path/filepath"
"testing"
"time"
- "github.com/gohugoio/hugo/htesting/hqt"
+ "github.com/bep/imagemeta"
"github.com/google/go-cmp/cmp"
qt "github.com/frankban/quicktest"
@@ -35,11 +34,12 @@ func TestExif(t *testing.T) {
d, err := NewDecoder(IncludeFields("Lens|Date"))
c.Assert(err, qt.IsNil)
- x, err := d.Decode(f)
+ x, err := d.Decode("", imagemeta.JPEG, f)
c.Assert(err, qt.IsNil)
c.Assert(x.Date.Format("2006-01-02"), qt.Equals, "2017-10-27")
// Malaga: https://goo.gl/taazZy
+
c.Assert(x.Lat, qt.Equals, float64(36.59744166666667))
c.Assert(x.Long, qt.Equals, float64(-4.50846))
@@ -49,9 +49,9 @@ func TestExif(t *testing.T) {
c.Assert(ok, qt.Equals, true)
c.Assert(lensModel, qt.Equals, "smc PENTAX-DA* 16-50mm F2.8 ED AL [IF] SDM")
- v, found = x.Tags["DateTime"]
+ v, found = x.Tags["ModifyDate"]
c.Assert(found, qt.Equals, true)
- c.Assert(v, hqt.IsSameType, time.Time{})
+ c.Assert(v, qt.Equals, "2017:11:23 09:56:54")
// Verify that it survives a round-trip to JSON and back.
data, err := json.Marshal(x)
@@ -72,8 +72,8 @@ func TestExifPNG(t *testing.T) {
d, err := NewDecoder()
c.Assert(err, qt.IsNil)
- _, err = d.Decode(f)
- c.Assert(err, qt.Not(qt.IsNil))
+ _, err = d.Decode("", imagemeta.PNG, f)
+ c.Assert(err, qt.IsNil)
}
func TestIssue8079(t *testing.T) {
@@ -85,28 +85,11 @@ func TestIssue8079(t *testing.T) {
d, err := NewDecoder()
c.Assert(err, qt.IsNil)
- x, err := d.Decode(f)
+ x, err := d.Decode("", imagemeta.JPEG, f)
c.Assert(err, qt.IsNil)
c.Assert(x.Tags["ImageDescription"], qt.Equals, "Città del Vaticano #nanoblock #vatican #vaticancity")
}
-func TestNullString(t *testing.T) {
- c := qt.New(t)
-
- for _, test := range []struct {
- in string
- expect string
- }{
- {"foo", "foo"},
- {"\x20", "\x20"},
- {"\xc4\x81", "\xc4\x81"}, // \u0101
- {"\u0160", "\u0160"}, // non-breaking space
- } {
- res := nullString([]byte(test.in))
- c.Assert(res, qt.Equals, test.expect)
- }
-}
-
func BenchmarkDecodeExif(b *testing.B) {
c := qt.New(b)
f, err := os.Open(filepath.FromSlash("../../testdata/sunset.jpg"))
@@ -118,7 +101,7 @@ func BenchmarkDecodeExif(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
- _, err = d.Decode(f)
+ _, err = d.Decode("", imagemeta.JPEG, f)
c.Assert(err, qt.IsNil)
f.Seek(0, 0)
}
@@ -126,8 +109,13 @@ func BenchmarkDecodeExif(b *testing.B) {
var eq = qt.CmpEquals(
cmp.Comparer(
- func(v1, v2 *big.Rat) bool {
- return v1.RatString() == v2.RatString()
+ func(v1, v2 imagemeta.Rat[uint32]) bool {
+ return v1.String() == v2.String()
+ },
+ ),
+ cmp.Comparer(
+ func(v1, v2 imagemeta.Rat[int32]) bool {
+ return v1.String() == v2.String()
},
),
cmp.Comparer(func(v1, v2 time.Time) bool {
@@ -138,14 +126,15 @@ var eq = qt.CmpEquals(
func TestIssue10738(t *testing.T) {
c := qt.New(t)
- testFunc := func(path, include string) any {
+ testFunc := func(c *qt.C, path, include string) any {
+ c.Helper()
f, err := os.Open(filepath.FromSlash(path))
c.Assert(err, qt.IsNil)
defer f.Close()
d, err := NewDecoder(IncludeFields(include))
c.Assert(err, qt.IsNil)
- x, err := d.Decode(f)
+ x, err := d.Decode("", imagemeta.JPEG, f)
c.Assert(err, qt.IsNil)
// Verify that it survives a round-trip to JSON and back.
@@ -194,7 +183,7 @@ func TestIssue10738(t *testing.T) {
include: "Lens|Date|ExposureTime",
}, want{
10,
- 0,
+ 1,
},
},
{
@@ -221,7 +210,7 @@ func TestIssue10738(t *testing.T) {
include: "Lens|Date|ExposureTime",
}, want{
1,
- 0,
+ 1,
},
},
{
@@ -266,7 +255,7 @@ func TestIssue10738(t *testing.T) {
include: "Lens|Date|ExposureTime",
}, want{
30,
- 0,
+ 1,
},
},
{
@@ -293,19 +282,21 @@ func TestIssue10738(t *testing.T) {
include: "Lens|Date|ExposureTime",
}, want{
4,
- 0,
+ 1,
},
},
}
for _, tt := range tests {
c.Run(tt.name, func(c *qt.C) {
- got := testFunc(tt.args.path, tt.args.include)
+ got := testFunc(c, tt.args.path, tt.args.include)
switch v := got.(type) {
case float64:
c.Assert(v, qt.Equals, float64(tt.want.vN))
- case *big.Rat:
- c.Assert(v, eq, big.NewRat(tt.want.vN, tt.want.vD))
+ case imagemeta.Rat[uint32]:
+ r, err := imagemeta.NewRat[uint32](uint32(tt.want.vN), uint32(tt.want.vD))
+ c.Assert(err, qt.IsNil)
+ c.Assert(v, eq, r)
default:
c.Fatalf("unexpected type: %T", got)
}
diff --git a/resources/images/filters.go b/resources/images/filters.go
index 0a620716d..1e44f1184 100644
--- a/resources/images/filters.go
+++ b/resources/images/filters.go
@@ -36,10 +36,11 @@ type Filters struct{}
// Process creates a filter that processes an image using the given specification.
func (*Filters) Process(spec any) gift.Filter {
+ specs := strings.ToLower(cast.ToString(spec))
return filter{
- Options: newFilterOpts(spec),
+ Options: newFilterOpts(specs),
Filter: processFilter{
- spec: cast.ToString(spec),
+ spec: specs,
},
}
}
@@ -52,6 +53,14 @@ func (*Filters) Overlay(src ImageSource, x, y any) gift.Filter {
}
}
+// Mask creates a filter that applies a mask image to the source image.
+func (*Filters) Mask(mask ImageSource) gift.Filter {
+ return filter{
+ Options: newFilterOpts(mask.Key()),
+ Filter: maskFilter{mask: mask},
+ }
+}
+
// Opacity creates a filter that changes the opacity of an image.
// The opacity parameter must be in range (0, 1).
func (*Filters) Opacity(opacity any) gift.Filter {
@@ -69,6 +78,8 @@ func (*Filters) Text(text string, options ...any) gift.Filter {
size: 20,
x: 10,
y: 10,
+ alignx: "left",
+ aligny: "top",
linespacing: 2,
}
@@ -87,6 +98,17 @@ func (*Filters) Text(text string, options ...any) gift.Filter {
tf.x = cast.ToInt(v)
case "y":
tf.y = cast.ToInt(v)
+ case "alignx":
+ tf.alignx = cast.ToString(v)
+ if tf.alignx != "left" && tf.alignx != "center" && tf.alignx != "right" {
+ panic("alignx must be one of left, center, right")
+ }
+ case "aligny":
+ tf.aligny = cast.ToString(v)
+ if tf.aligny != "top" && tf.aligny != "center" && tf.aligny != "bottom" {
+ panic("aligny must be one of top, center, bottom")
+ }
+
case "linespacing":
tf.linespacing = cast.ToInt(v)
case "font":
diff --git a/resources/images/filters_test.go b/resources/images/filters_test.go
index a8c5601d6..ce8ae9f5a 100644
--- a/resources/images/filters_test.go
+++ b/resources/images/filters_test.go
@@ -17,7 +17,7 @@ import (
"testing"
qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/common/hashing"
)
func TestFilterHash(t *testing.T) {
@@ -25,8 +25,8 @@ func TestFilterHash(t *testing.T) {
f := &Filters{}
- c.Assert(identity.HashString(f.Grayscale()), qt.Equals, identity.HashString(f.Grayscale()))
- c.Assert(identity.HashString(f.Grayscale()), qt.Not(qt.Equals), identity.HashString(f.Invert()))
- c.Assert(identity.HashString(f.Gamma(32)), qt.Not(qt.Equals), identity.HashString(f.Gamma(33)))
- c.Assert(identity.HashString(f.Gamma(32)), qt.Equals, identity.HashString(f.Gamma(32)))
+ c.Assert(hashing.HashString(f.Grayscale()), qt.Equals, hashing.HashString(f.Grayscale()))
+ c.Assert(hashing.HashString(f.Grayscale()), qt.Not(qt.Equals), hashing.HashString(f.Invert()))
+ c.Assert(hashing.HashString(f.Gamma(32)), qt.Not(qt.Equals), hashing.HashString(f.Gamma(33)))
+ c.Assert(hashing.HashString(f.Gamma(32)), qt.Equals, hashing.HashString(f.Gamma(32)))
}
diff --git a/resources/images/image.go b/resources/images/image.go
index 1637d0cf8..c891b0168 100644
--- a/resources/images/image.go
+++ b/resources/images/image.go
@@ -26,6 +26,8 @@ import (
"sync"
"github.com/bep/gowebp/libwebp/webpoptions"
+ "github.com/bep/imagemeta"
+ "github.com/bep/logg"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/resources/images/webp"
@@ -174,13 +176,14 @@ func (i *Image) initConfig() error {
return nil
}
-func NewImageProcessor(cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) (*ImageProcessor, error) {
+func NewImageProcessor(warnl logg.LevelLogger, cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) (*ImageProcessor, error) {
e := cfg.Config.Imaging.Exif
exifDecoder, err := exif.NewDecoder(
exif.WithDateDisabled(e.DisableDate),
exif.WithLatLongDisabled(e.DisableLatLong),
exif.ExcludeFields(e.ExcludeFields),
exif.IncludeFields(e.IncludeFields),
+ exif.WithWarnLogger(warnl),
)
if err != nil {
return nil, err
@@ -197,8 +200,9 @@ type ImageProcessor struct {
exifDecoder *exif.Decoder
}
-func (p *ImageProcessor) DecodeExif(r io.Reader) (*exif.ExifInfo, error) {
- return p.exifDecoder.Decode(r)
+// Filename is only used for logging.
+func (p *ImageProcessor) DecodeExif(filename string, format imagemeta.ImageFormat, r io.Reader) (*exif.ExifInfo, error) {
+ return p.exifDecoder.Decode(filename, format, r)
}
func (p *ImageProcessor) FiltersFromConfig(src image.Image, conf ImageConfig) ([]gift.Filter, error) {
@@ -213,7 +217,7 @@ func (p *ImageProcessor) FiltersFromConfig(src image.Image, conf ImageConfig) ([
case "resize":
filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter))
case "crop":
- if conf.AnchorStr == smartCropIdentifier {
+ if conf.Anchor == SmartCropAnchor {
bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter)
if err != nil {
return nil, err
@@ -228,7 +232,7 @@ func (p *ImageProcessor) FiltersFromConfig(src image.Image, conf ImageConfig) ([
filters = append(filters, gift.CropToSize(conf.Width, conf.Height, conf.Anchor))
}
case "fill":
- if conf.AnchorStr == smartCropIdentifier {
+ if conf.Anchor == SmartCropAnchor {
bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter)
if err != nil {
return nil, err
@@ -325,12 +329,12 @@ func (p *ImageProcessor) doFilter(src image.Image, targetFormat Format, filters
return dst, nil
}
-func GetDefaultImageConfig(action string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) ImageConfig {
+func GetDefaultImageConfig(defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) ImageConfig {
if defaults == nil {
defaults = defaultImageConfig
}
return ImageConfig{
- Action: action,
+ Anchor: -1, // The real values start at 0.
Hint: defaults.Config.Hint,
Quality: defaults.Config.Imaging.Quality,
}
@@ -353,6 +357,21 @@ const (
WEBP
)
+func (f Format) ToImageMetaImageFormatFormat() imagemeta.ImageFormat {
+ switch f {
+ case JPEG:
+ return imagemeta.JPEG
+ case PNG:
+ return imagemeta.PNG
+ case TIFF:
+ return imagemeta.TIFF
+ case WEBP:
+ return imagemeta.WebP
+ default:
+ return -1
+ }
+}
+
// RequiresDefaultQuality returns if the default quality needs to be applied to
// images of this format.
func (f Format) RequiresDefaultQuality() bool {
diff --git a/resources/images/images_golden_integration_test.go b/resources/images/images_golden_integration_test.go
new file mode 100644
index 000000000..5397bee23
--- /dev/null
+++ b/resources/images/images_golden_integration_test.go
@@ -0,0 +1,414 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package images_test
+
+import (
+ _ "image/jpeg"
+ "strings"
+ "testing"
+
+ "github.com/gohugoio/hugo/resources/images/imagetesting"
+)
+
+// Note, if you're enabling writeGoldenFiles on a MacOS ARM 64 you need to run the test with GOARCH=amd64, e.g.
+func TestImagesGoldenFiltersMisc(t *testing.T) {
+ t.Parallel()
+
+ if imagetesting.SkipGoldenTests {
+ t.Skip("Skip golden test on this architecture")
+ }
+
+ // Will be used as the base folder for generated images.
+ name := "filters/misc"
+
+ files := `
+-- hugo.toml --
+-- assets/rotate270.jpg --
+sourcefilename: ../testdata/exif/orientation6.jpg
+-- assets/sunset.jpg --
+sourcefilename: ../testdata/sunset.jpg
+-- assets/gopher.png --
+sourcefilename: ../testdata/gopher-hero8.png
+-- layouts/index.html --
+Home.
+{{ $sunset := (resources.Get "sunset.jpg").Resize "x300" }}
+{{ $sunsetGrayscale := $sunset.Filter (images.Grayscale) }}
+{{ $gopher := (resources.Get "gopher.png").Resize "x80" }}
+{{ $overlayFilter := images.Overlay $gopher 20 20 }}
+
+{{ $textOpts := dict
+ "color" "#fbfaf5"
+ "linespacing" 8
+ "size" 40
+ "x" 25
+ "y" 190
+}}
+
+{{/* These are sorted. */}}
+{{ template "filters" (dict "name" "brightness-40.jpg" "img" $sunset "filters" (images.Brightness 40)) }}
+{{ template "filters" (dict "name" "contrast-50.jpg" "img" $sunset "filters" (images.Contrast 50)) }}
+{{ template "filters" (dict "name" "dither-default.jpg" "img" $sunset "filters" (images.Dither)) }}
+{{ template "filters" (dict "name" "gamma-1.667.jpg" "img" $sunset "filters" (images.Gamma 1.667)) }}
+{{ template "filters" (dict "name" "gaussianblur-5.jpg" "img" $sunset "filters" (images.GaussianBlur 5)) }}
+{{ template "filters" (dict "name" "grayscale.jpg" "img" $sunset "filters" (images.Grayscale)) }}
+{{ template "filters" (dict "name" "grayscale+colorize-180-50-20.jpg" "img" $sunset "filters" (slice images.Grayscale (images.Colorize 180 50 20))) }}
+{{ template "filters" (dict "name" "colorbalance-180-50-20.jpg" "img" $sunset "filters" (images.ColorBalance 180 50 20)) }}
+{{ template "filters" (dict "name" "hue--15.jpg" "img" $sunset "filters" (images.Hue -15)) }}
+{{ template "filters" (dict "name" "invert.jpg" "img" $sunset "filters" (images.Invert)) }}
+{{ template "filters" (dict "name" "opacity-0.65.jpg" "img" $sunset "filters" (images.Opacity 0.65)) }}
+{{ template "filters" (dict "name" "overlay-20-20.jpg" "img" $sunset "filters" ($overlayFilter)) }}
+{{ template "filters" (dict "name" "padding-20-40-#976941.jpg" "img" $sunset "filters" (images.Padding 20 40 "#976941" )) }}
+{{ template "filters" (dict "name" "pixelate-10.jpg" "img" $sunset "filters" (images.Pixelate 10)) }}
+{{ template "filters" (dict "name" "rotate270.jpg" "img" (resources.Get "rotate270.jpg") "filters" images.AutoOrient) }}
+{{ template "filters" (dict "name" "saturation-65.jpg" "img" $sunset "filters" (images.Saturation 65)) }}
+{{ template "filters" (dict "name" "sepia-80.jpg" "img" $sunsetGrayscale "filters" (images.Sepia 80)) }}
+{{ template "filters" (dict "name" "sigmoid-0.6--4.jpg" "img" $sunset "filters" (images.Sigmoid 0.6 -4 )) }}
+{{ template "filters" (dict "name" "text.jpg" "img" $sunset "filters" (images.Text "Hugo Rocks!" $textOpts )) }}
+{{ template "filters" (dict "name" "unsharpmask.jpg" "img" $sunset "filters" (images.UnsharpMask 10 0.4 0.03)) }}
+
+
+{{ define "filters"}}
+{{ if lt (len (path.Ext .name)) 4 }}
+ {{ errorf "No extension in %q" .name }}
+{{ end }}
+{{ $img := .img.Filter .filters }}
+{{ $name := printf "images/%s" .name }}
+{{ with $img | resources.Copy $name }}
+{{ .Publish }}
+{{ end }}
+{{ end }}
+`
+
+ opts := imagetesting.DefaultGoldenOpts
+ opts.T = t
+ opts.Name = name
+ opts.Files = files
+
+ imagetesting.RunGolden(opts)
+}
+
+func TestImagesGoldenFiltersMask(t *testing.T) {
+ t.Parallel()
+
+ if imagetesting.SkipGoldenTests {
+ t.Skip("Skip golden test on this architecture")
+ }
+
+ // Will be used as the base folder for generated images.
+ name := "filters/mask"
+
+ files := `
+-- hugo.toml --
+[imaging]
+ bgColor = '#ebcc34'
+ hint = 'photo'
+ quality = 75
+ resampleFilter = 'Lanczos'
+-- assets/sunset.jpg --
+sourcefilename: ../testdata/sunset.jpg
+-- assets/mask.png --
+sourcefilename: ../testdata/mask.png
+
+-- layouts/index.html --
+Home.
+{{ $sunset := resources.Get "sunset.jpg" }}
+{{ $mask := resources.Get "mask.png" }}
+
+{{ template "mask" (dict "name" "transparant.png" "base" $sunset "mask" $mask) }}
+{{ template "mask" (dict "name" "yellow.jpg" "base" $sunset "mask" $mask) }}
+{{ template "mask" (dict "name" "wide.jpg" "base" $sunset "mask" $mask "spec" "resize 600x200") }}
+{{/* This looks a little odd, but is correct and the recommended way to do this.
+This will 1. Scale the image to x300, 2. Apply the mask, 3. Create the final image with background color #323ea.
+It's possible to have multiple images.Process filters in the chain, but for the options for the final image (target format, bgGolor etc.),
+the last entry will win.
+*/}}
+{{ template "mask" (dict "name" "blue.jpg" "base" $sunset "mask" $mask "spec" "resize x300 #323ea8") }}
+
+{{ define "mask"}}
+{{ $ext := path.Ext .name }}
+{{ if lt (len (path.Ext .name)) 4 }}
+ {{ errorf "No extension in %q" .name }}
+{{ end }}
+{{ $format := strings.TrimPrefix "." $ext }}
+{{ $spec := .spec | default (printf "resize x300 %s" $format) }}
+{{ $filters := slice (images.Process $spec) (images.Mask .mask) }}
+{{ $name := printf "images/%s" .name }}
+{{ $img := .base.Filter $filters }}
+{{ with $img | resources.Copy $name }}
+{{ .Publish }}
+{{ end }}
+{{ end }}
+`
+
+ opts := imagetesting.DefaultGoldenOpts
+ opts.T = t
+ opts.Name = name
+ opts.Files = files
+
+ imagetesting.RunGolden(opts)
+}
+
+// Issue 13272, 13273.
+func TestImagesGoldenFiltersMaskCacheIssues(t *testing.T) {
+ if imagetesting.SkipGoldenTests {
+ t.Skip("Skip golden test on this architecture")
+ }
+
+ // Will be used as the base folder for generated images.
+ name := "filters/mask2"
+
+ files := `
+-- hugo.toml --
+[caches]
+ [caches.images]
+ dir = ':cacheDir/golden_images'
+ maxAge = "30s"
+[imaging]
+ bgColor = '#33ff44'
+ hint = 'photo'
+ quality = 75
+ resampleFilter = 'Lanczos'
+-- assets/sunset.jpg --
+sourcefilename: ../testdata/sunset.jpg
+-- assets/mask.png --
+sourcefilename: ../testdata/mask.png
+
+-- layouts/index.html --
+Home.
+{{ $sunset := resources.Get "sunset.jpg" }}
+{{ $mask := resources.Get "mask.png" }}
+
+
+{{ template "mask" (dict "name" "green.jpg" "base" $sunset "mask" $mask) }}
+
+{{ define "mask"}}
+{{ $ext := path.Ext .name }}
+{{ if lt (len (path.Ext .name)) 4 }}
+ {{ errorf "No extension in %q" .name }}
+{{ end }}
+{{ $format := strings.TrimPrefix "." $ext }}
+{{ $spec := .spec | default (printf "resize x300 %s" $format) }}
+{{ $filters := slice (images.Process $spec) (images.Mask .mask) }}
+{{ $name := printf "images/%s" .name }}
+{{ $img := .base.Filter $filters }}
+{{ with $img | resources.Copy $name }}
+{{ .Publish }}
+{{ end }}
+{{ end }}
+`
+
+ tempDir := t.TempDir()
+
+ opts := imagetesting.DefaultGoldenOpts
+ opts.WorkingDir = tempDir
+ opts.T = t
+ opts.Name = name
+ opts.Files = files
+ opts.SkipAssertions = true
+
+ imagetesting.RunGolden(opts)
+
+ files = strings.Replace(files, "#33ff44", "#a83269", -1)
+ files = strings.Replace(files, "green", "pink", -1)
+ files = strings.Replace(files, "mask.png", "mask2.png", -1)
+ opts.Files = files
+ opts.SkipAssertions = false
+ opts.Rebuild = true
+
+ imagetesting.RunGolden(opts)
+}
+
+func TestImagesGoldenFiltersText(t *testing.T) {
+ t.Parallel()
+
+ if imagetesting.SkipGoldenTests {
+ t.Skip("Skip golden test on this architecture")
+ }
+
+ // Will be used as the base folder for generated images.
+ name := "filters/text"
+
+ files := `
+-- hugo.toml --
+-- assets/sunset.jpg --
+sourcefilename: ../testdata/sunset.jpg
+
+-- layouts/index.html --
+Home.
+{{ $sunset := resources.Get "sunset.jpg" }}
+{{ $textOpts := dict
+ "color" "#fbfaf5"
+ "linespacing" 8
+ "size" 28
+ "x" (div $sunset.Width 2 | int)
+ "y" (div $sunset.Height 2 | int)
+ "alignx" "center"
+}}
+
+{{ $text := "Pariatur deserunt sunt nisi sunt tempor quis eu. Sint et nulla enim officia sunt cupidatat. Eu amet ipsum qui velit cillum cillum ad Lorem in non ad aute." }}
+{{ template "filters" (dict "name" "text_alignx-center.jpg" "img" $sunset "filters" (images.Text $text $textOpts )) }}
+{{ $textOpts = (dict "alignx" "right") | merge $textOpts }}
+{{ template "filters" (dict "name" "text_alignx-right.jpg" "img" $sunset "filters" (images.Text $text $textOpts )) }}
+{{ $textOpts = (dict "alignx" "left") | merge $textOpts }}
+{{ template "filters" (dict "name" "text_alignx-left.jpg" "img" $sunset "filters" (images.Text $text $textOpts )) }}
+{{ $textOpts = (dict "alignx" "center" "aligny" "center") | merge $textOpts }}
+{{ $text = "Est exercitation deserunt exercitation nostrud magna. Eiusmod anim deserunt sit elit dolore ea incididunt nisi. Ea ullamco excepteur voluptate occaecat duis pariatur proident cupidatat. Eu id esse qui consectetur commodo ad ex esse cupidatat velit duis cupidatat. Aliquip irure tempor consequat non amet in mollit ipsum officia tempor laborum." }}
+{{ template "filters" (dict "name" "text_alignx-center_aligny-center.jpg" "img" $sunset "filters" (images.Text $text $textOpts )) }}
+{{ $textOpts = (dict "alignx" "center" "aligny" "bottom") | merge $textOpts }}
+{{ template "filters" (dict "name" "text_alignx-center_aligny-bottom.jpg" "img" $sunset "filters" (images.Text $text $textOpts )) }}
+
+{{ define "filters"}}
+{{ if lt (len (path.Ext .name)) 4 }}
+ {{ errorf "No extension in %q" .name }}
+{{ end }}
+{{ $img := .img.Filter .filters }}
+{{ $name := printf "images/%s" .name }}
+{{ with $img | resources.Copy $name }}
+{{ .Publish }}
+{{ end }}
+{{ end }}
+`
+
+ opts := imagetesting.DefaultGoldenOpts
+ opts.T = t
+ opts.Name = name
+ opts.Files = files
+ // opts.WriteFiles = true
+ // opts.DevMode = true
+
+ imagetesting.RunGolden(opts)
+}
+
+func TestImagesGoldenProcessMisc(t *testing.T) {
+ t.Parallel()
+
+ if imagetesting.SkipGoldenTests {
+ t.Skip("Skip golden test on this architecture")
+ }
+
+ // Will be used as the base folder for generated images.
+ name := "process/misc"
+
+ files := `
+-- hugo.toml --
+-- assets/giphy.gif --
+sourcefilename: ../testdata/giphy.gif
+-- assets/sunset.jpg --
+sourcefilename: ../testdata/sunset.jpg
+-- assets/gopher.png --
+sourcefilename: ../testdata/gopher-hero8.png
+-- layouts/index.html --
+Home.
+{{ $sunset := resources.Get "sunset.jpg" }}
+{{ $sunsetGrayscale := $sunset.Filter (images.Grayscale) }}
+{{ $gopher := resources.Get "gopher.png" }}
+{{ $giphy := resources.Get "giphy.gif" }}
+
+
+{{/* These are sorted. The end file name will be created from the spec + extension, so make sure these are unique. */}}
+{{ template "process" (dict "spec" "crop 500x200 smart" "img" $sunset) }}
+{{ template "process" (dict "spec" "fill 500x200 smart" "img" $sunset) }}
+{{ template "process" (dict "spec" "fit 500x200 smart" "img" $sunset) }}
+{{ template "process" (dict "spec" "resize 100x100 gif" "img" $giphy) }}
+{{ template "process" (dict "spec" "resize 100x100 r180" "img" $gopher) }}
+{{ template "process" (dict "spec" "resize 300x300 jpg #b31280" "img" $gopher) }}
+
+{{ define "process"}}
+{{ $img := .img.Process .spec }}
+{{ $ext := path.Ext $img.RelPermalink }}
+{{ $name := printf "images/%s%s" (.spec | anchorize) $ext }}
+{{ with $img | resources.Copy $name }}
+{{ .Publish }}
+{{ end }}
+{{ end }}
+`
+
+ opts := imagetesting.DefaultGoldenOpts
+ opts.T = t
+ opts.Name = name
+ opts.Files = files
+
+ imagetesting.RunGolden(opts)
+}
+
+func TestImagesGoldenMethods(t *testing.T) {
+ t.Parallel()
+
+ if imagetesting.SkipGoldenTests {
+ t.Skip("Skip golden test on this architecture")
+ }
+
+ // Will be used as the base folder for generated images.
+ name := "methods"
+
+ files := `
+-- hugo.toml --
+[imaging]
+ bgColor = '#ebcc34'
+ hint = 'photo'
+ quality = 75
+ resampleFilter = 'MitchellNetravali'
+-- assets/sunset.jpg --
+sourcefilename: ../testdata/sunset.jpg
+-- assets/gopher.png --
+sourcefilename: ../testdata/gopher-hero8.png
+
+-- layouts/index.html --
+Home.
+{{ $sunset := resources.Get "sunset.jpg" }}
+{{ $gopher := resources.Get "gopher.png" }}
+
+
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "resize" "spec" "300x" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "resize" "spec" "x200" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "fill" "spec" "90x120 left" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "fill" "spec" "90x120 right" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "fit" "spec" "200x200" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop" "spec" "200x200" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop" "spec" "350x400 center" ) }}
+ {{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop" "spec" "350x400 smart" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop" "spec" "350x400 center r90" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop" "spec" "350x400 center q20" ) }}
+{{ template "invoke" (dict "copyFormat" "png" "base" $gopher "method" "resize" "spec" "100x" ) }}
+{{ template "invoke" (dict "copyFormat" "png" "base" $gopher "method" "resize" "spec" "100x #fc03ec" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $gopher "method" "resize" "spec" "100x #03fc56 jpg" ) }}
+
+{{ define "invoke"}}
+{{ $spec := .spec }}
+{{ $name := printf "images/%s-%s-%s.%s" .method ((trim .base.Name "/") | lower | anchorize) ($spec | anchorize) .copyFormat }}
+{{ $img := ""}}
+{{ if eq .method "resize" }}
+ {{ $img = .base.Resize $spec }}
+{{ else if eq .method "fill" }}
+ {{ $img = .base.Fill $spec }}
+{{ else if eq .method "fit" }}
+ {{ $img = .base.Fit $spec }}
+{{ else if eq .method "crop" }}
+ {{ $img = .base.Crop $spec }}
+{{ else }}
+ {{ errorf "Unknown method %q" .method }}
+{{ end }}
+{{ with $img | resources.Copy $name }}
+{{ .Publish }}
+{{ end }}
+{{ end }}
+`
+
+ opts := imagetesting.DefaultGoldenOpts
+ opts.T = t
+ opts.Name = name
+ opts.Files = files
+
+ imagetesting.RunGolden(opts)
+}
diff --git a/resources/images/images_integration_test.go b/resources/images/images_integration_test.go
new file mode 100644
index 000000000..caba42e03
--- /dev/null
+++ b/resources/images/images_integration_test.go
@@ -0,0 +1,52 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package images_test
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/hugolib"
+)
+
+func TestAutoOrient(t *testing.T) {
+ files := `
+-- hugo.toml --
+-- assets/rotate270.jpg --
+sourcefilename: ../testdata/exif/orientation6.jpg
+-- layouts/index.html --
+{{ $img := resources.Get "rotate270.jpg" }}
+W/H original: {{ $img.Width }}/{{ $img.Height }}
+{{ $rotated := $img.Filter images.AutoOrient }}
+W/H rotated: {{ $rotated.Width }}/{{ $rotated.Height }}
+`
+
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.html", "W/H original: 80/40\n\nW/H rotated: 40/80")
+}
+
+// Issue 12733.
+func TestOrientationEq(t *testing.T) {
+ files := `
+-- hugo.toml --
+-- assets/rotate270.jpg --
+sourcefilename: ../testdata/exif/orientation6.jpg
+-- layouts/index.html --
+{{ $img := resources.Get "rotate270.jpg" }}
+{{ $orientation := $img.Exif.Tags.Orientation }}
+Orientation: {{ $orientation }}|eq 6: {{ eq $orientation 6 }}|Type: {{ printf "%T" $orientation }}|
+`
+
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.html", "Orientation: 6|eq 6: true|")
+}
diff --git a/resources/images/imagetesting/testing.go b/resources/images/imagetesting/testing.go
new file mode 100644
index 000000000..f7a6af7ea
--- /dev/null
+++ b/resources/images/imagetesting/testing.go
@@ -0,0 +1,235 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package imagetesting
+
+import (
+ "image"
+ "image/gif"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+ "github.com/google/go-cmp/cmp"
+
+ "github.com/disintegration/gift"
+ "github.com/gohugoio/hugo/common/hashing"
+ "github.com/gohugoio/hugo/common/hugio"
+ "github.com/gohugoio/hugo/htesting"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/hugolib"
+)
+
+var eq = qt.CmpEquals(
+ cmp.Comparer(func(p1, p2 os.FileInfo) bool {
+ return p1.Name() == p2.Name() && p1.Size() == p2.Size() && p1.IsDir() == p2.IsDir()
+ }),
+ cmp.Comparer(func(d1, d2 fs.DirEntry) bool {
+ p1, err1 := d1.Info()
+ p2, err2 := d2.Info()
+ if err1 != nil || err2 != nil {
+ return false
+ }
+ return p1.Name() == p2.Name() && p1.Size() == p2.Size() && p1.IsDir() == p2.IsDir()
+ }),
+)
+
+// GoldenImageTestOpts provides options for a golden image test.
+type GoldenImageTestOpts struct {
+ // The test.
+ T testing.TB
+
+ // Name of the test. Will be used as the base folder for generated images.
+ // Slashes allowed and encouraged.
+ Name string
+
+ // The test site's files in txttar format.
+ Files string
+
+ // Set to true to write golden files to disk.
+ WriteFiles bool
+
+ // If not set, a temporary directory will be created.
+ WorkingDir string
+
+ // Set to true to skip any assertions. Useful when adding new golden variants to a test.
+ DevMode bool
+
+ // Set to skip any assertions.
+ SkipAssertions bool
+
+ // Whether this represents a rebuild of the same site.
+ // Setting this to true will keep the previous golden image set.
+ Rebuild bool
+}
+
+// To rebuild all Golden image tests, toggle WriteFiles=true and run:
+// GOARCH=amd64 go test -count 1 -timeout 30s -run "^TestImagesGolden" ./...
+// TODO(bep) see if we can do this via flags.
+var DefaultGoldenOpts = GoldenImageTestOpts{
+ WriteFiles: false,
+ DevMode: false,
+}
+
+func RunGolden(opts GoldenImageTestOpts) *hugolib.IntegrationTestBuilder {
+ opts.T.Helper()
+
+ c := hugolib.Test(opts.T, opts.Files, hugolib.TestOptWithConfig(func(conf *hugolib.IntegrationTestConfig) {
+ conf.NeedsOsFS = true
+ conf.WorkingDir = opts.WorkingDir
+ }))
+ c.AssertFileContent("public/index.html", "Home.")
+
+ outputDir := filepath.Join(c.H.Conf.WorkingDir(), "public", "images")
+ goldenBaseDir := filepath.Join("testdata", "images_golden")
+ goldenDir := filepath.Join(goldenBaseDir, filepath.FromSlash(opts.Name))
+ if opts.WriteFiles {
+ c.Assert(htesting.IsRealCI(), qt.IsFalse)
+ if !opts.Rebuild {
+ c.Assert(os.MkdirAll(goldenBaseDir, 0o777), qt.IsNil)
+ c.Assert(os.RemoveAll(goldenDir), qt.IsNil)
+ }
+ c.Assert(hugio.CopyDir(hugofs.Os, outputDir, goldenDir, nil), qt.IsNil)
+ return c
+ }
+
+ if opts.SkipAssertions {
+ return c
+ }
+
+ if opts.DevMode {
+ c.Assert(htesting.IsRealCI(), qt.IsFalse)
+ return c
+ }
+
+ decodeAll := func(f *os.File) []image.Image {
+ c.Helper()
+
+ var images []image.Image
+
+ if strings.HasSuffix(f.Name(), ".gif") {
+ gif, err := gif.DecodeAll(f)
+ c.Assert(err, qt.IsNil, qt.Commentf(f.Name()))
+ images = make([]image.Image, len(gif.Image))
+ for i, img := range gif.Image {
+ images[i] = img
+ }
+ } else {
+ img, _, err := image.Decode(f)
+ c.Assert(err, qt.IsNil, qt.Commentf(f.Name()))
+ images = append(images, img)
+ }
+ return images
+ }
+
+ entries1, err := os.ReadDir(outputDir)
+ c.Assert(err, qt.IsNil)
+ entries2, err := os.ReadDir(goldenDir)
+ c.Assert(err, qt.IsNil)
+ c.Assert(len(entries1), qt.Equals, len(entries2))
+ for i, e1 := range entries1 {
+ c.Assert(filepath.Ext(e1.Name()), qt.Not(qt.Equals), "")
+ func() {
+ e2 := entries2[i]
+
+ f1, err := os.Open(filepath.Join(outputDir, e1.Name()))
+ c.Assert(err, qt.IsNil)
+ defer f1.Close()
+
+ f2, err := os.Open(filepath.Join(goldenDir, e2.Name()))
+ c.Assert(err, qt.IsNil)
+ defer f2.Close()
+
+ imgs2 := decodeAll(f2)
+ imgs1 := decodeAll(f1)
+ c.Assert(len(imgs1), qt.Equals, len(imgs2))
+
+ if !UsesFMA {
+ c.Assert(e1, eq, e2)
+ _, err = f1.Seek(0, 0)
+ c.Assert(err, qt.IsNil)
+ _, err = f2.Seek(0, 0)
+ c.Assert(err, qt.IsNil)
+
+ hash1, _, err := hashing.XXHashFromReader(f1)
+ c.Assert(err, qt.IsNil)
+ hash2, _, err := hashing.XXHashFromReader(f2)
+ c.Assert(err, qt.IsNil)
+
+ c.Assert(hash1, qt.Equals, hash2)
+ }
+
+ for i, img1 := range imgs1 {
+ img2 := imgs2[i]
+ nrgba1 := image.NewNRGBA(img1.Bounds())
+ gift.New().Draw(nrgba1, img1)
+ nrgba2 := image.NewNRGBA(img2.Bounds())
+ gift.New().Draw(nrgba2, img2)
+ c.Assert(goldenEqual(nrgba1, nrgba2), qt.Equals, true, qt.Commentf(e1.Name()))
+ }
+ }()
+ }
+ return c
+}
+
+// goldenEqual compares two NRGBA images. It is used in golden tests only.
+// A small tolerance is allowed on architectures using "fused multiply and add"
+// (FMA) instruction to accommodate for floating-point rounding differences
+// with control golden images that were generated on amd64 architecture.
+// See https://golang.org/ref/spec#Floating_point_operators
+// and https://github.com/gohugoio/hugo/issues/6387 for more information.
+//
+// Based on https://github.com/disintegration/gift/blob/a999ff8d5226e5ab14b64a94fca07c4ac3f357cf/gift_test.go#L598-L625
+// Copyright (c) 2014-2019 Grigory Dryapak
+// Licensed under the MIT License.
+func goldenEqual(img1, img2 *image.NRGBA) bool {
+ maxDiff := 0
+ if runtime.GOARCH != "amd64" {
+ // The golden files are created using the AMD64 architecture.
+ // Be lenient on other platforms due to floaging point and dithering differences.
+ maxDiff = 15
+ }
+ if !img1.Rect.Eq(img2.Rect) {
+ return false
+ }
+ if len(img1.Pix) != len(img2.Pix) {
+ return false
+ }
+ for i := range img1.Pix {
+ diff := int(img1.Pix[i]) - int(img2.Pix[i])
+ if diff < 0 {
+ diff = -diff
+ }
+ if diff > maxDiff {
+ return false
+ }
+ }
+ return true
+}
+
+// We don't have a CI test environment for these, and there are known dithering issues that makes these time consuming to maintain.
+var SkipGoldenTests = runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "s390x"
+
+// UsesFMA indicates whether "fused multiply and add" (FMA) instruction is
+// used. The command "grep FMADD go/test/codegen/floats.go" can help keep
+// the FMA-using architecture list updated.
+var UsesFMA = runtime.GOARCH == "s390x" ||
+ runtime.GOARCH == "ppc64" ||
+ runtime.GOARCH == "ppc64le" ||
+ runtime.GOARCH == "arm64" ||
+ runtime.GOARCH == "riscv64" ||
+ runtime.GOARCH == "loong64"
diff --git a/resources/images/mask.go b/resources/images/mask.go
new file mode 100644
index 000000000..5ce7c5d43
--- /dev/null
+++ b/resources/images/mask.go
@@ -0,0 +1,63 @@
+package images
+
+import (
+ "fmt"
+ "image"
+ "image/color"
+ "image/draw"
+
+ "github.com/disintegration/gift"
+)
+
+// maskFilter applies a mask image to a base image.
+type maskFilter struct {
+ mask ImageSource
+}
+
+// Draw applies the mask to the base image.
+func (f maskFilter) Draw(dst draw.Image, baseImage image.Image, options *gift.Options) {
+ maskImage, err := f.mask.DecodeImage()
+ if err != nil {
+ panic(fmt.Sprintf("failed to decode image: %s", err))
+ }
+
+ // Ensure the mask is the same size as the base image
+ baseBounds := baseImage.Bounds()
+ maskBounds := maskImage.Bounds()
+
+ // Resize mask to match base image size if necessary
+ if maskBounds.Dx() != baseBounds.Dx() || maskBounds.Dy() != baseBounds.Dy() {
+ g := gift.New(gift.Resize(baseBounds.Dx(), baseBounds.Dy(), gift.LanczosResampling))
+ resizedMask := image.NewRGBA(g.Bounds(maskImage.Bounds()))
+ g.Draw(resizedMask, maskImage)
+ maskImage = resizedMask
+ }
+
+ // Use gift to convert the resized mask to grayscale
+ g := gift.New(gift.Grayscale())
+ grayscaleMask := image.NewGray(g.Bounds(maskImage.Bounds()))
+ g.Draw(grayscaleMask, maskImage)
+
+ // Convert grayscale mask to alpha mask
+ alphaMask := image.NewAlpha(baseBounds)
+ for y := baseBounds.Min.Y; y < baseBounds.Max.Y; y++ {
+ for x := baseBounds.Min.X; x < baseBounds.Max.X; x++ {
+ grayValue := grayscaleMask.GrayAt(x, y).Y
+ alphaMask.SetAlpha(x, y, color.Alpha{A: grayValue})
+ }
+ }
+
+ // Create an RGBA output image
+ outputImage := image.NewRGBA(baseBounds)
+
+ // Apply the mask using draw.DrawMask
+ draw.DrawMask(outputImage, baseBounds, baseImage, image.Point{}, alphaMask, image.Point{}, draw.Over)
+
+ // Copy the result to the destination
+ gift.New().Draw(dst, outputImage)
+}
+
+// Bounds returns the bounds of the resulting image.
+func (f maskFilter) Bounds(imgBounds image.Rectangle) image.Rectangle {
+ return image.Rect(0, 0, imgBounds.Dx(), imgBounds.Dy())
+}
diff --git a/resources/images/smartcrop.go b/resources/images/smartcrop.go
index 864c6de0a..af45c241c 100644
--- a/resources/images/smartcrop.go
+++ b/resources/images/smartcrop.go
@@ -25,10 +25,10 @@ import (
const (
// Do not change.
smartCropIdentifier = "smart"
-
- // This is just a increment, starting on 1. If Smart Crop improves its cropping, we
+ SmartCropAnchor = 1000
+ // This is just a increment, starting on 0. If Smart Crop improves its cropping, we
// need a way to trigger a re-generation of the crops in the wild, so increment this.
- smartCropVersionNumber = 1
+ smartCropVersionNumber = 0
)
func (p *ImageProcessor) newSmartCropAnalyzer(filter gift.Resampling) smartcrop.Analyzer {
diff --git a/resources/images/testdata/images_golden/filters/mask/blue.jpg b/resources/images/testdata/images_golden/filters/mask/blue.jpg
new file mode 100644
index 000000000..7c8097741
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/mask/blue.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/mask/transparant.png b/resources/images/testdata/images_golden/filters/mask/transparant.png
new file mode 100644
index 000000000..4d8c57ace
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/mask/transparant.png differ
diff --git a/resources/images/testdata/images_golden/filters/mask/wide.jpg b/resources/images/testdata/images_golden/filters/mask/wide.jpg
new file mode 100644
index 000000000..38ef715ba
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/mask/wide.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/mask/yellow.jpg b/resources/images/testdata/images_golden/filters/mask/yellow.jpg
new file mode 100644
index 000000000..e7b3073db
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/mask/yellow.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/mask2/green.jpg b/resources/images/testdata/images_golden/filters/mask2/green.jpg
new file mode 100644
index 000000000..48a9dd083
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/mask2/green.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/mask2/pink.jpg b/resources/images/testdata/images_golden/filters/mask2/pink.jpg
new file mode 100644
index 000000000..640e41ab1
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/mask2/pink.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/brightness-40.jpg b/resources/images/testdata/images_golden/filters/misc/brightness-40.jpg
new file mode 100644
index 000000000..92d03e2f1
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/brightness-40.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/colorbalance-180-50-20.jpg b/resources/images/testdata/images_golden/filters/misc/colorbalance-180-50-20.jpg
new file mode 100644
index 000000000..1f34922eb
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/colorbalance-180-50-20.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/contrast-50.jpg b/resources/images/testdata/images_golden/filters/misc/contrast-50.jpg
new file mode 100644
index 000000000..24a064338
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/contrast-50.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/dither-default.jpg b/resources/images/testdata/images_golden/filters/misc/dither-default.jpg
new file mode 100644
index 000000000..3960f94b2
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/dither-default.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/gamma-1.667.jpg b/resources/images/testdata/images_golden/filters/misc/gamma-1.667.jpg
new file mode 100644
index 000000000..e8fcbe753
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/gamma-1.667.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/gaussianblur-5.jpg b/resources/images/testdata/images_golden/filters/misc/gaussianblur-5.jpg
new file mode 100644
index 000000000..36783bb6f
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/gaussianblur-5.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/grayscale+colorize-180-50-20.jpg b/resources/images/testdata/images_golden/filters/misc/grayscale+colorize-180-50-20.jpg
new file mode 100644
index 000000000..4902922b3
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/grayscale+colorize-180-50-20.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/grayscale.jpg b/resources/images/testdata/images_golden/filters/misc/grayscale.jpg
new file mode 100644
index 000000000..06617ee00
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/grayscale.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/hue--15.jpg b/resources/images/testdata/images_golden/filters/misc/hue--15.jpg
new file mode 100644
index 000000000..68b191ec2
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/hue--15.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/invert.jpg b/resources/images/testdata/images_golden/filters/misc/invert.jpg
new file mode 100644
index 000000000..69ab0fc1b
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/invert.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/opacity-0.65.jpg b/resources/images/testdata/images_golden/filters/misc/opacity-0.65.jpg
new file mode 100644
index 000000000..6da3c980e
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/opacity-0.65.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/overlay-20-20.jpg b/resources/images/testdata/images_golden/filters/misc/overlay-20-20.jpg
new file mode 100644
index 000000000..3a6ca0b30
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/overlay-20-20.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/padding-20-40-#976941.jpg b/resources/images/testdata/images_golden/filters/misc/padding-20-40-#976941.jpg
new file mode 100644
index 000000000..14a443f9a
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/padding-20-40-#976941.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/pixelate-10.jpg b/resources/images/testdata/images_golden/filters/misc/pixelate-10.jpg
new file mode 100644
index 000000000..094de575e
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/pixelate-10.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/rotate270.jpg b/resources/images/testdata/images_golden/filters/misc/rotate270.jpg
new file mode 100644
index 000000000..3a5b2483a
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/rotate270.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/saturation-65.jpg b/resources/images/testdata/images_golden/filters/misc/saturation-65.jpg
new file mode 100644
index 000000000..d26585e66
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/saturation-65.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/sepia-80.jpg b/resources/images/testdata/images_golden/filters/misc/sepia-80.jpg
new file mode 100644
index 000000000..76d08ad8c
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/sepia-80.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/sigmoid-0.6--4.jpg b/resources/images/testdata/images_golden/filters/misc/sigmoid-0.6--4.jpg
new file mode 100644
index 000000000..c6df06715
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/sigmoid-0.6--4.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/text.jpg b/resources/images/testdata/images_golden/filters/misc/text.jpg
new file mode 100644
index 000000000..20e79dbad
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/text.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/misc/unsharpmask.jpg b/resources/images/testdata/images_golden/filters/misc/unsharpmask.jpg
new file mode 100644
index 000000000..9b04b6701
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/misc/unsharpmask.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/text/text_alignx-center.jpg b/resources/images/testdata/images_golden/filters/text/text_alignx-center.jpg
new file mode 100644
index 000000000..94bcb811a
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/text/text_alignx-center.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-bottom.jpg b/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-bottom.jpg
new file mode 100644
index 000000000..ca8893e4e
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-bottom.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-center.jpg b/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-center.jpg
new file mode 100644
index 000000000..828b6e6a3
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-center.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/text/text_alignx-left.jpg b/resources/images/testdata/images_golden/filters/text/text_alignx-left.jpg
new file mode 100644
index 000000000..2894fae42
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/text/text_alignx-left.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/text/text_alignx-right.jpg b/resources/images/testdata/images_golden/filters/text/text_alignx-right.jpg
new file mode 100644
index 000000000..207e88a49
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/text/text_alignx-right.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/crop-sunsetjpg-200x200.jpg b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-200x200.jpg
new file mode 100644
index 000000000..10569ab2c
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-200x200.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center-q20.jpg b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center-q20.jpg
new file mode 100644
index 000000000..1b316ff17
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center-q20.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center-r90.jpg b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center-r90.jpg
new file mode 100644
index 000000000..9e3bc4bf5
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center-r90.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center.jpg b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center.jpg
new file mode 100644
index 000000000..aa89eedf2
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-smart.jpg b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-smart.jpg
new file mode 100644
index 000000000..7a203920e
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-smart.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/fill-sunsetjpg-90x120-left.jpg b/resources/images/testdata/images_golden/methods/fill-sunsetjpg-90x120-left.jpg
new file mode 100644
index 000000000..39f1823f5
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/fill-sunsetjpg-90x120-left.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/fill-sunsetjpg-90x120-right.jpg b/resources/images/testdata/images_golden/methods/fill-sunsetjpg-90x120-right.jpg
new file mode 100644
index 000000000..4afcb6dec
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/fill-sunsetjpg-90x120-right.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/fit-sunsetjpg-200x200.jpg b/resources/images/testdata/images_golden/methods/fit-sunsetjpg-200x200.jpg
new file mode 100644
index 000000000..75c6a30b0
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/fit-sunsetjpg-200x200.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-03fc56-jpg.jpg b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-03fc56-jpg.jpg
new file mode 100644
index 000000000..2e78ce461
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-03fc56-jpg.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-fc03ec.png b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-fc03ec.png
new file mode 100644
index 000000000..44af25303
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-fc03ec.png differ
diff --git a/resources/images/testdata/images_golden/methods/resize-gopherpng-100x.png b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x.png
new file mode 100644
index 000000000..9bca47b14
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x.png differ
diff --git a/resources/images/testdata/images_golden/methods/resize-sunsetjpg-300x.jpg b/resources/images/testdata/images_golden/methods/resize-sunsetjpg-300x.jpg
new file mode 100644
index 000000000..319385e9a
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/resize-sunsetjpg-300x.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/resize-sunsetjpg-x200.jpg b/resources/images/testdata/images_golden/methods/resize-sunsetjpg-x200.jpg
new file mode 100644
index 000000000..77b2555cb
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/resize-sunsetjpg-x200.jpg differ
diff --git a/resources/images/testdata/images_golden/process/misc/crop-500x200-smart.jpg b/resources/images/testdata/images_golden/process/misc/crop-500x200-smart.jpg
new file mode 100644
index 000000000..6df66064b
Binary files /dev/null and b/resources/images/testdata/images_golden/process/misc/crop-500x200-smart.jpg differ
diff --git a/resources/images/testdata/images_golden/process/misc/fill-500x200-smart.jpg b/resources/images/testdata/images_golden/process/misc/fill-500x200-smart.jpg
new file mode 100644
index 000000000..cad8927b5
Binary files /dev/null and b/resources/images/testdata/images_golden/process/misc/fill-500x200-smart.jpg differ
diff --git a/resources/images/testdata/images_golden/process/misc/fit-500x200-smart.jpg b/resources/images/testdata/images_golden/process/misc/fit-500x200-smart.jpg
new file mode 100644
index 000000000..a320cda99
Binary files /dev/null and b/resources/images/testdata/images_golden/process/misc/fit-500x200-smart.jpg differ
diff --git a/resources/images/testdata/images_golden/process/misc/resize-100x100-gif.gif b/resources/images/testdata/images_golden/process/misc/resize-100x100-gif.gif
new file mode 100644
index 000000000..66a6b73de
Binary files /dev/null and b/resources/images/testdata/images_golden/process/misc/resize-100x100-gif.gif differ
diff --git a/resources/images/testdata/images_golden/process/misc/resize-100x100-r180.png b/resources/images/testdata/images_golden/process/misc/resize-100x100-r180.png
new file mode 100644
index 000000000..ec5042e67
Binary files /dev/null and b/resources/images/testdata/images_golden/process/misc/resize-100x100-r180.png differ
diff --git a/resources/images/testdata/images_golden/process/misc/resize-300x300-jpg-b31280.jpg b/resources/images/testdata/images_golden/process/misc/resize-300x300-jpg-b31280.jpg
new file mode 100644
index 000000000..1000d8f34
Binary files /dev/null and b/resources/images/testdata/images_golden/process/misc/resize-300x300-jpg-b31280.jpg differ
diff --git a/resources/images/text.go b/resources/images/text.go
index c1abc60bd..f3943a475 100644
--- a/resources/images/text.go
+++ b/resources/images/text.go
@@ -35,6 +35,8 @@ type textFilter struct {
text string
color color.Color
x, y int
+ alignx string
+ aligny string
size float64
linespacing int
fontSource hugio.ReadSeekCloserProvider
@@ -77,30 +79,69 @@ func (f textFilter) Draw(dst draw.Image, src image.Image, options *gift.Options)
gift.New().Draw(dst, src)
- // Draw text, consider and include linebreaks
maxWidth := dst.Bounds().Dx() - 20
+
+ var availableWidth int
+ switch f.alignx {
+ case "right":
+ availableWidth = f.x
+ case "center":
+ availableWidth = min((maxWidth-f.x), f.x) * 2
+ case "left":
+ availableWidth = maxWidth - f.x
+ }
+
fontHeight := face.Metrics().Ascent.Ceil()
- // Correct y position based on font and size
- f.y = f.y + fontHeight
-
- // Start position
- y := f.y
- d.Dot = fixed.P(f.x, f.y)
-
- // Draw text line by line, breaking each line at the maximum width.
+ // Calculate lines, consider and include linebreaks
+ finalLines := []string{}
f.text = strings.ReplaceAll(f.text, "\r", "")
for _, line := range strings.Split(f.text, "\n") {
+ currentLine := ""
+ // Break each line at the maximum width.
for _, str := range strings.Fields(line) {
- strWidth := font.MeasureString(face, str)
- if (d.Dot.X.Ceil() + strWidth.Ceil()) >= maxWidth {
- y = y + fontHeight + f.linespacing
- d.Dot = fixed.P(f.x, y)
+ fieldStrWidth := font.MeasureString(face, str)
+ currentLineStrWidth := font.MeasureString(face, currentLine)
+
+ if (currentLineStrWidth.Ceil() + fieldStrWidth.Ceil()) >= availableWidth {
+ finalLines = append(finalLines, currentLine)
+ currentLine = ""
}
- d.DrawString(str + " ")
+ currentLine += str + " "
}
+ finalLines = append(finalLines, currentLine)
+ }
+ // Total height of the text from the top of the first line to the baseline of the last line
+ totalHeight := len(finalLines)*fontHeight + (len(finalLines)-1)*f.linespacing
+
+ // Correct y position based on font and size
+ y := f.y + fontHeight
+ switch f.aligny {
+ case "top":
+ // Do nothing
+ case "center":
+ y = y - totalHeight/2
+ case "bottom":
+ y = y - totalHeight
+ }
+
+ // Draw text line by line
+ for _, line := range finalLines {
+ line = strings.TrimSpace(line)
+ strWidth := font.MeasureString(face, line)
+ var x int
+ switch f.alignx {
+ case "right":
+ x = f.x - strWidth.Ceil()
+ case "center":
+ x = f.x - (strWidth.Ceil() / 2)
+
+ case "left":
+ x = f.x
+ }
+ d.Dot = fixed.P(x, y)
+ d.DrawString(line)
y = y + fontHeight + f.linespacing
- d.Dot = fixed.P(f.x, y)
}
}
diff --git a/resources/images/webp/webp.go b/resources/images/webp/webp.go
index 28336d2e0..d7407214f 100644
--- a/resources/images/webp/webp.go
+++ b/resources/images/webp/webp.go
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build extended
-// +build extended
package webp
diff --git a/resources/images/webp/webp_notavailable.go b/resources/images/webp/webp_notavailable.go
index 70407f94e..b617eeb51 100644
--- a/resources/images/webp/webp_notavailable.go
+++ b/resources/images/webp/webp_notavailable.go
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build !extended
-// +build !extended
package webp
diff --git a/resources/internal/key.go b/resources/internal/key.go
index 2b95871aa..b0ac9703f 100644
--- a/resources/internal/key.go
+++ b/resources/internal/key.go
@@ -13,7 +13,7 @@
package internal
-import "github.com/gohugoio/hugo/identity"
+import "github.com/gohugoio/hugo/common/hashing"
// ResourceTransformationKey are provided by the different transformation implementations.
// It identifies the transformation (name) and its configuration (elements).
@@ -38,5 +38,5 @@ func (k ResourceTransformationKey) Value() string {
return k.Name
}
- return k.Name + "_" + identity.HashString(k.elements...)
+ return k.Name + "_" + hashing.HashString(k.elements...)
}
diff --git a/resources/internal/key_test.go b/resources/internal/key_test.go
index 38286333d..fcad7d754 100644
--- a/resources/internal/key_test.go
+++ b/resources/internal/key_test.go
@@ -32,5 +32,5 @@ func TestResourceTransformationKey(t *testing.T) {
key := NewResourceTransformationKey("testing",
testStruct{Name: "test", V1: int64(10), V2: int32(20), V3: 30, V4: uint64(40)})
c := qt.New(t)
- c.Assert(key.Value(), qt.Equals, "testing_518996646957295636")
+ c.Assert(key.Value(), qt.Equals, "testing_4231238781487357822")
}
diff --git a/resources/kinds/kinds.go b/resources/kinds/kinds.go
index 2660ec719..30bc35e43 100644
--- a/resources/kinds/kinds.go
+++ b/resources/kinds/kinds.go
@@ -34,6 +34,7 @@ const (
// The following are (currently) temporary nodes,
// i.e. nodes we create just to render in isolation.
+ KindTemporary = "temporary"
KindRSS = "rss"
KindSitemap = "sitemap"
KindSitemapIndex = "sitemapindex"
diff --git a/resources/page/page.go b/resources/page/page.go
index 9647a916b..cbcfad557 100644
--- a/resources/page/page.go
+++ b/resources/page/page.go
@@ -17,6 +17,7 @@ package page
import (
"context"
+ "fmt"
"html/template"
"github.com/gohugoio/hugo/markup/converter"
@@ -50,14 +51,6 @@ type AlternativeOutputFormatsProvider interface {
AlternativeOutputFormats() OutputFormats
}
-// AuthorProvider provides author information.
-type AuthorProvider interface {
- // Deprecated: Use taxonomies instead.
- Author() Author
- // Deprecated: Use taxonomies instead.
- Authors() AuthorList
-}
-
// ChildCareProvider provides accessors to child resources.
type ChildCareProvider interface {
// Pages returns a list of pages of all kinds.
@@ -70,14 +63,20 @@ type ChildCareProvider interface {
// section.
RegularPagesRecursive() Pages
- // Resources returns a list of all resources.
- Resources() resource.Resources
+ resource.ResourcesProvider
+}
+
+type MarkupProvider interface {
+ Markup(opts ...any) Markup
}
// ContentProvider provides the content related values for a Page.
type ContentProvider interface {
Content(context.Context) (any, error)
+ // ContentWithoutSummary returns the Page Content stripped of the summary.
+ ContentWithoutSummary(ctx context.Context) (template.HTML, error)
+
// Plain returns the Page Content stripped of HTML markup.
Plain(context.Context) string
@@ -149,10 +148,10 @@ type InSectionPositioner interface {
PrevInSection() Page
}
-// InternalDependencies is considered an internal interface.
-type InternalDependencies interface {
- // GetRelatedDocsHandler is for internal use only.
- GetRelatedDocsHandler() *RelatedDocsHandler
+// RelatedDocsHandlerProvider is considered an internal interface.
+type RelatedDocsHandlerProvider interface {
+ // GetInternalRelatedDocsHandler is for internal use only.
+ GetInternalRelatedDocsHandler() *RelatedDocsHandler
}
// OutputFormatsProvider provides the OutputFormats of a Page.
@@ -169,9 +168,11 @@ type PageProvider interface {
// Page is the core interface in Hugo and what you get as the top level data context in your templates.
type Page interface {
+ MarkupProvider
ContentProvider
TableOfContentsProvider
PageWithoutContent
+ fmt.Stringer
}
type PageFragment interface {
@@ -179,6 +180,11 @@ type PageFragment interface {
resource.ResourceNameTitleProvider
}
+type PageMetaResource interface {
+ PageMetaProvider
+ resource.Resource
+}
+
// PageMetaProvider provides page metadata, typically provided via front matter.
type PageMetaProvider interface {
// The 4 page dates
@@ -250,6 +256,68 @@ type PageMetaProvider interface {
Weight() int
}
+// NamedPageMetaValue returns a named metadata value from a PageMetaResource.
+// This is currently only used to generate keywords for related content.
+// If nameLower is not one of the metadata interface methods, we
+// look in Params.
+func NamedPageMetaValue(p PageMetaResource, nameLower string) (any, bool, error) {
+ var (
+ v any
+ err error
+ )
+
+ switch nameLower {
+ case "kind":
+ v = p.Kind()
+ case "bundletype":
+ v = p.BundleType()
+ case "mediatype":
+ v = p.MediaType()
+ case "section":
+ v = p.Section()
+ case "lang":
+ v = p.Lang()
+ case "aliases":
+ v = p.Aliases()
+ case "name":
+ v = p.Name()
+ case "keywords":
+ v = p.Keywords()
+ case "description":
+ v = p.Description()
+ case "title":
+ v = p.Title()
+ case "linktitle":
+ v = p.LinkTitle()
+ case "slug":
+ v = p.Slug()
+ case "date":
+ v = p.Date()
+ case "publishdate":
+ v = p.PublishDate()
+ case "expirydate":
+ v = p.ExpiryDate()
+ case "lastmod":
+ v = p.Lastmod()
+ case "draft":
+ v = p.Draft()
+ case "type":
+ v = p.Type()
+ case "layout":
+ v = p.Layout()
+ case "weight":
+ v = p.Weight()
+ default:
+ // Try params.
+ v, err = resource.Param(p, nil, nameLower)
+ if v == nil {
+ return nil, false, nil
+ }
+ }
+
+ return v, err == nil, err
+}
+
// PageMetaInternalProvider provides internal page metadata.
type PageMetaInternalProvider interface {
// This is for internal use only.
@@ -260,7 +328,7 @@ type PageMetaInternalProvider interface {
type PageRenderProvider interface {
// Render renders the given layout with this Page as context.
Render(ctx context.Context, layout ...string) (template.HTML, error)
- // RenderString renders the first value in args with tPaginatorhe content renderer defined
+ // RenderString renders the first value in args with the content renderer defined
// for this Page.
// It takes an optional map as a second argument:
//
@@ -299,9 +367,6 @@ type PageWithoutContent interface {
Positioner
navigation.PageMenusProvider
- // TODO(bep)
- AuthorProvider
-
// Page lookups/refs
GetPageProvider
RefProvider
@@ -317,11 +382,11 @@ type PageWithoutContent interface {
// Scratch returns a Scratch that can be used to store temporary state.
// Note that this Scratch gets reset on server rebuilds. See Store() for a variant that survives.
- maps.Scratcher
+ // Scratch returns a "scratch pad" that can be used to store state.
+ // Deprecated: From Hugo v0.138.0 this is just an alias for Store.
+ Scratch() *maps.Scratch
- // Store returns a Scratch that can be used to store temporary state.
- // In contrast to Scratch(), this Scratch is not reset on server rebuilds.
- Store() *maps.Scratch
+ maps.StoreProvider
RelatedKeywordsProvider
diff --git a/resources/page/page_lazy_contentprovider.go b/resources/page/page_lazy_contentprovider.go
index 665b2d003..8e66a03e4 100644
--- a/resources/page/page_lazy_contentprovider.go
+++ b/resources/page/page_lazy_contentprovider.go
@@ -35,6 +35,7 @@ type OutputFormatContentProvider interface {
// OutputFormatPageContentProvider holds the exported methods from Page that are "outputFormat aware".
type OutputFormatPageContentProvider interface {
+ MarkupProvider
ContentProvider
TableOfContentsProvider
PageRenderProvider
@@ -74,6 +75,11 @@ func (lcp *LazyContentProvider) Reset() {
lcp.init.Reset()
}
+func (lcp *LazyContentProvider) Markup(opts ...any) Markup {
+ lcp.init.Do(context.Background())
+ return lcp.cp.Markup(opts...)
+}
+
func (lcp *LazyContentProvider) TableOfContents(ctx context.Context) template.HTML {
lcp.init.Do(ctx)
return lcp.cp.TableOfContents(ctx)
@@ -89,6 +95,11 @@ func (lcp *LazyContentProvider) Content(ctx context.Context) (any, error) {
return lcp.cp.Content(ctx)
}
+func (lcp *LazyContentProvider) ContentWithoutSummary(ctx context.Context) (template.HTML, error) {
+ lcp.init.Do(ctx)
+ return lcp.cp.ContentWithoutSummary(ctx)
+}
+
func (lcp *LazyContentProvider) Plain(ctx context.Context) string {
lcp.init.Do(ctx)
return lcp.cp.Plain(ctx)
diff --git a/resources/page/page_markup.go b/resources/page/page_markup.go
new file mode 100644
index 000000000..44980e8b0
--- /dev/null
+++ b/resources/page/page_markup.go
@@ -0,0 +1,362 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "context"
+ "html/template"
+ "regexp"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+
+ "github.com/gohugoio/hugo/common/types"
+ "github.com/gohugoio/hugo/markup/tableofcontents"
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/tpl"
+)
+
+type Content interface {
+ Content(context.Context) (template.HTML, error)
+ ContentWithoutSummary(context.Context) (template.HTML, error)
+ Summary(context.Context) (Summary, error)
+ Plain(context.Context) string
+ PlainWords(context.Context) []string
+ WordCount(context.Context) int
+ FuzzyWordCount(context.Context) int
+ ReadingTime(context.Context) int
+ Len(context.Context) int
+}
+
+type Markup interface {
+ Render(context.Context) (Content, error)
+ RenderString(ctx context.Context, args ...any) (template.HTML, error)
+ RenderShortcodes(context.Context) (template.HTML, error)
+ Fragments(context.Context) *tableofcontents.Fragments
+}
+
+var _ types.PrintableValueProvider = Summary{}
+
+const (
+ SummaryTypeAuto = "auto"
+ SummaryTypeManual = "manual"
+ SummaryTypeFrontMatter = "frontmatter"
+)
+
+type Summary struct {
+ Text template.HTML
+ Type string // "auto", "manual" or "frontmatter"
+ Truncated bool
+}
+
+func (s Summary) IsZero() bool {
+ return s.Text == ""
+}
+
+func (s Summary) PrintableValue() any {
+ return s.Text
+}
+
+var _ types.PrintableValueProvider = (*Summary)(nil)
+
+type HtmlSummary struct {
+ source string
+ SummaryLowHigh types.LowHigh[string]
+ SummaryEndTag types.LowHigh[string]
+ WrapperStart types.LowHigh[string]
+ WrapperEnd types.LowHigh[string]
+ Divider types.LowHigh[string]
+}
+
+func (s HtmlSummary) wrap(ss string) string {
+ if s.WrapperStart.IsZero() {
+ return ss
+ }
+ return s.source[s.WrapperStart.Low:s.WrapperStart.High] + ss + s.source[s.WrapperEnd.Low:s.WrapperEnd.High]
+}
+
+func (s HtmlSummary) wrapLeft(ss string) string {
+ if s.WrapperStart.IsZero() {
+ return ss
+ }
+
+ return s.source[s.WrapperStart.Low:s.WrapperStart.High] + ss
+}
+
+func (s HtmlSummary) Value(l types.LowHigh[string]) string {
+ return s.source[l.Low:l.High]
+}
+
+func (s HtmlSummary) trimSpace(ss string) string {
+ return strings.TrimSpace(ss)
+}
+
+func (s HtmlSummary) Content() string {
+ if s.Divider.IsZero() {
+ return s.source
+ }
+ ss := s.source[:s.Divider.Low]
+ ss += s.source[s.Divider.High:]
+ return s.trimSpace(ss)
+}
+
+func (s HtmlSummary) Summary() string {
+ if s.Divider.IsZero() {
+ return s.trimSpace(s.wrap(s.Value(s.SummaryLowHigh)))
+ }
+ ss := s.source[s.SummaryLowHigh.Low:s.Divider.Low]
+ if s.SummaryLowHigh.High > s.Divider.High {
+ ss += s.source[s.Divider.High:s.SummaryLowHigh.High]
+ }
+ if !s.SummaryEndTag.IsZero() {
+ ss += s.Value(s.SummaryEndTag)
+ }
+ return s.trimSpace(s.wrap(ss))
+}
+
+func (s HtmlSummary) ContentWithoutSummary() string {
+ if s.Divider.IsZero() {
+ if s.SummaryLowHigh.Low == s.WrapperStart.High && s.SummaryLowHigh.High == s.WrapperEnd.Low {
+ return ""
+ }
+ return s.trimSpace(s.wrapLeft(s.source[s.SummaryLowHigh.High:]))
+ }
+ if s.SummaryEndTag.IsZero() {
+ return s.trimSpace(s.wrapLeft(s.source[s.Divider.High:]))
+ }
+ return s.trimSpace(s.wrapLeft(s.source[s.SummaryEndTag.High:]))
+}
+
+func (s HtmlSummary) Truncated() bool {
+ return s.SummaryLowHigh.High < len(s.source)
+}
+
+func (s *HtmlSummary) resolveParagraphTagAndSetWrapper(mt media.Type) tagReStartEnd {
+ ptag := startEndP
+
+ switch mt.SubType {
+ case media.DefaultContentTypes.AsciiDoc.SubType:
+ ptag = startEndDiv
+ case media.DefaultContentTypes.ReStructuredText.SubType:
+ const markerStart = ""
+ const markerEnd = "
"
+ i1 := strings.Index(s.source, markerStart)
+ i2 := strings.LastIndex(s.source, markerEnd)
+ if i1 > -1 && i2 > -1 {
+ s.WrapperStart = types.LowHigh[string]{Low: 0, High: i1 + len(markerStart)}
+ s.WrapperEnd = types.LowHigh[string]{Low: i2, High: len(s.source)}
+ }
+ }
+ return ptag
+}
+
+// Avoid counting words that are most likely HTML tokens.
+var (
+ isProbablyHTMLTag = regexp.MustCompile(`^<\/?[A-Za-z]+>?$`)
+ isProablyHTMLAttribute = regexp.MustCompile(`^[A-Za-z]+=["']`)
+)
+
+func isProbablyHTMLToken(s string) bool {
+ return s == ">" || isProbablyHTMLTag.MatchString(s) || isProablyHTMLAttribute.MatchString(s)
+}
+
+// ExtractSummaryFromHTML extracts a summary from the given HTML content.
+func ExtractSummaryFromHTML(mt media.Type, input string, numWords int, isCJK bool) (result HtmlSummary) {
+ result.source = input
+ ptag := result.resolveParagraphTagAndSetWrapper(mt)
+
+ if numWords <= 0 {
+ return result
+ }
+
+ var count int
+
+ countWord := func(word string) int {
+ word = strings.TrimSpace(word)
+ if len(word) == 0 {
+ return 0
+ }
+ if isProbablyHTMLToken(word) {
+ return 0
+ }
+
+ if isCJK {
+ word = tpl.StripHTML(word)
+ runeCount := utf8.RuneCountInString(word)
+ if len(word) == runeCount {
+ return 1
+ } else {
+ return runeCount
+ }
+ }
+
+ return 1
+ }
+
+ high := len(input)
+ if result.WrapperEnd.Low > 0 {
+ high = result.WrapperEnd.Low
+ }
+
+ for j := result.WrapperStart.High; j < high; {
+ s := input[j:]
+ closingIndex := strings.Index(s, ""+ptag.tagName+">")
+
+ if closingIndex == -1 {
+ break
+ }
+
+ s = s[:closingIndex]
+
+ // Count the words in the current paragraph.
+ var wi int
+
+ for i, r := range s {
+ if unicode.IsSpace(r) || (i+utf8.RuneLen(r) == len(s)) {
+ word := s[wi:i]
+ count += countWord(word)
+ wi = i
+ if count >= numWords {
+ break
+ }
+ }
+ }
+
+ if count >= numWords {
+ result.SummaryLowHigh = types.LowHigh[string]{
+ Low: result.WrapperStart.High,
+ High: j + closingIndex + len(ptag.tagName) + 3,
+ }
+ return
+ }
+
+ j += closingIndex + len(ptag.tagName) + 2
+
+ }
+
+ result.SummaryLowHigh = types.LowHigh[string]{
+ Low: result.WrapperStart.High,
+ High: high,
+ }
+
+ return
+}
+
+// ExtractSummaryFromHTMLWithDivider extracts a summary from the given HTML content with
+// a manual summary divider.
+func ExtractSummaryFromHTMLWithDivider(mt media.Type, input, divider string) (result HtmlSummary) {
+ result.source = input
+ result.Divider.Low = strings.Index(input, divider)
+ result.Divider.High = result.Divider.Low + len(divider)
+
+ if result.Divider.Low == -1 {
+ // No summary.
+ return
+ }
+
+ ptag := result.resolveParagraphTagAndSetWrapper(mt)
+
+ if !mt.IsHTML() {
+ result.Divider, result.SummaryEndTag = expandSummaryDivider(result.source, ptag, result.Divider)
+ }
+
+ result.SummaryLowHigh = types.LowHigh[string]{
+ Low: result.WrapperStart.High,
+ High: result.Divider.Low,
+ }
+
+ return
+}
+
+var (
+ pOrDiv = regexp.MustCompile(`]?>|
]?>$`)
+
+ startEndDiv = tagReStartEnd{
+ startEndOfString: regexp.MustCompile(`
]*?>$`),
+ endEndOfString: regexp.MustCompile(`
$`),
+ tagName: "div",
+ }
+
+ startEndP = tagReStartEnd{
+ startEndOfString: regexp.MustCompile(`
]*?>$`),
+ endEndOfString: regexp.MustCompile(`
$`),
+ tagName: "p",
+ }
+)
+
+type tagReStartEnd struct {
+ startEndOfString *regexp.Regexp
+ endEndOfString *regexp.Regexp
+ tagName string
+}
+
+func expandSummaryDivider(s string, re tagReStartEnd, divider types.LowHigh[string]) (types.LowHigh[string], types.LowHigh[string]) {
+ var endMarkup types.LowHigh[string]
+
+ if divider.IsZero() {
+ return divider, endMarkup
+ }
+
+ lo, hi := divider.Low, divider.High
+
+ var preserveEndMarkup bool
+
+ // Find the start of the paragraph.
+
+ for i := lo - 1; i >= 0; i-- {
+ if s[i] == '>' {
+ if match := re.startEndOfString.FindString(s[:i+1]); match != "" {
+ lo = i - len(match) + 1
+ break
+ }
+ if match := pOrDiv.FindString(s[:i+1]); match != "" {
+ i -= len(match) - 1
+ continue
+ }
+ }
+
+ r, _ := utf8.DecodeRuneInString(s[i:])
+ if !unicode.IsSpace(r) {
+ preserveEndMarkup = true
+ break
+ }
+ }
+
+ divider.Low = lo
+
+ // Now walk forward to the end of the paragraph.
+ for ; hi < len(s); hi++ {
+ if s[hi] != '>' {
+ continue
+ }
+ if match := re.endEndOfString.FindString(s[:hi+1]); match != "" {
+ hi++
+ break
+ }
+ }
+
+ if preserveEndMarkup {
+ endMarkup.Low = divider.High
+ endMarkup.High = hi
+ } else {
+ divider.High = hi
+ }
+
+ // Consume trailing newline if any.
+ if divider.High < len(s) && s[divider.High] == '\n' {
+ divider.High++
+ }
+
+ return divider, endMarkup
+}
diff --git a/resources/page/page_markup_integration_test.go b/resources/page/page_markup_integration_test.go
new file mode 100644
index 000000000..425099215
--- /dev/null
+++ b/resources/page/page_markup_integration_test.go
@@ -0,0 +1,349 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page_test
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/markup/asciidocext"
+ "github.com/gohugoio/hugo/markup/rst"
+)
+
+func TestPageMarkupMethods(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+summaryLength=2
+-- content/p1.md --
+---
+title: "Post 1"
+date: "2020-01-01"
+---
+{{% foo %}}
+-- layouts/shortcodes/foo.html --
+Two *words*.
+{{/* Test that markup scope is set in all relevant constructs. */}}
+{{ if eq hugo.Context.MarkupScope "foo" }}
+
+## Heading 1
+Sint ad mollit qui Lorem ut occaecat culpa officia. Et consectetur aute voluptate non sit ullamco adipisicing occaecat. Sunt deserunt amet sit ad. Deserunt enim voluptate proident ipsum dolore dolor ut sit velit esse est mollit irure esse. Mollit incididunt veniam laboris magna et excepteur sit duis. Magna adipisicing reprehenderit tempor irure.
+### Heading 2
+Exercitation quis est consectetur occaecat nostrud. Ullamco aute mollit aliqua est amet. Exercitation ullamco consectetur dolor labore et non irure eu cillum Lorem.
+{{ end }}
+-- layouts/index.html --
+Home.
+{{ .Content }}
+-- layouts/_default/single.html --
+Single.
+Page.ContentWithoutSummmary: {{ .ContentWithoutSummary }}|
+{{ template "render-scope" (dict "page" . "scope" "main") }}
+{{ template "render-scope" (dict "page" . "scope" "foo") }}
+{{ define "render-scope" }}
+{{ $c := .page.Markup .scope }}
+{{ with $c.Render }}
+{{ $.scope }}: Content: {{ .Content }}|
+ {{ $.scope }}: ContentWithoutSummary: {{ .ContentWithoutSummary }}|
+{{ $.scope }}: Plain: {{ .Plain }}|
+{{ $.scope }}: PlainWords: {{ .PlainWords }}|
+{{ $.scope }}: WordCount: {{ .WordCount }}|
+{{ $.scope }}: FuzzyWordCount: {{ .FuzzyWordCount }}|
+{{ $.scope }}: ReadingTime: {{ .ReadingTime }}|
+{{ $.scope }}: Len: {{ .Len }}|
+{{ $.scope }}: Summary: {{ with .Summary }}{{ . }}{{ else }}nil{{ end }}|
+{{ end }}
+{{ $.scope }}: Fragments: {{ $c.Fragments.Identifiers }}|
+{{ end }}
+
+
+
+`
+
+ b := hugolib.Test(t, files)
+
+ // Main scope.
+ b.AssertFileContent("public/p1/index.html",
+ "Page.ContentWithoutSummmary: |",
+ "main: Content:
Two words .
\n|",
+ "main: ContentWithoutSummary: |",
+ "main: Plain: Two words.\n|",
+ "PlainWords: [Two words.]|\nmain: WordCount: 2|\nmain: FuzzyWordCount: 100|\nmain: ReadingTime: 1|",
+ "main: Summary:
Two words .
|\n\nmain: Fragments: []|",
+ "main: Len: 27|",
+ )
+
+ // Foo scope (has more content).
+ b.AssertFileContent("public/p1/index.html",
+ "foo: Content:
Two words .
\n
Two words .|",
+ "foo: Fragments: [heading-1 heading-2]|",
+ )
+}
+
+func TestPageMarkupScope(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "rss", "section"]
+-- content/p1.md --
+---
+title: "Post 1"
+date: "2020-01-01"
+---
+
+# P1
+
+{{< foo >}}
+
+Begin:{{% includerendershortcodes "p2" %}}:End
+Begin:{{< includecontent "p3" >}}:End
+
+-- content/p2.md --
+---
+title: "Post 2"
+date: "2020-01-02"
+---
+
+# P2
+-- content/p3.md --
+---
+title: "Post 3"
+date: "2020-01-03"
+---
+
+# P3
+
+{{< foo >}}
+
+-- layouts/index.html --
+Home.
+{{ with site.GetPage "p1" }}
+ {{ with .Markup "home" }}
+ {{ .Render.Content }}
+ {{ end }}
+{{ end }}
+-- layouts/_default/single.html --
+Single.
+{{ with .Markup }}
+ {{ with .Render }}
+ {{ .Content }}
+ {{ end }}
+{{ end }}
+-- layouts/_default/_markup/render-heading.html --
+Render heading: title: {{ .Text}} scope: {{ hugo.Context.MarkupScope }}|
+-- layouts/shortcodes/foo.html --
+Foo scope: {{ hugo.Context.MarkupScope }}|
+-- layouts/shortcodes/includerendershortcodes.html --
+{{ $p := site.GetPage (.Get 0) }}
+includerendershortcodes: {{ hugo.Context.MarkupScope }}|{{ $p.Markup.RenderShortcodes }}|
+-- layouts/shortcodes/includecontent.html --
+{{ $p := site.GetPage (.Get 0) }}
+includecontent: {{ hugo.Context.MarkupScope }}|{{ $p.Markup.Render.Content }}|
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContentExact("public/p1/index.html", "Render heading: title: P1 scope: |", "Foo scope: |")
+
+ b.AssertFileContentExact("public/index.html",
+ "Begin:\nincludecontent: home|Render heading: title: P3 scope: home|Foo scope: home|\n|\n:End",
+ "Render heading: title: P1 scope: home|",
+ "Foo scope: home|",
+ "Begin:\nincluderendershortcodes: home|\nRender heading: title: P2 scope: home| |:End",
+ )
+}
+
+func TestPageContentWithoutSummary(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+summaryLength=5
+-- content/p1.md --
+---
+title: "Post 1"
+date: "2020-01-01"
+---
+This is summary.
+
+This is content.
+-- content/p2.md --
+---
+title: "Post 2"
+date: "2020-01-01"
+---
+This is some content about a summary and more.
+
+Another paragraph.
+
+Third paragraph.
+-- content/p3.md --
+---
+title: "Post 3"
+date: "2020-01-01"
+summary: "This is summary in front matter."
+---
+This is content.
+-- layouts/_default/single.html --
+Single.
+Page.Summary: {{ .Summary }}|
+{{ with .Markup.Render }}
+Content: {{ .Content }}|
+ContentWithoutSummary: {{ .ContentWithoutSummary }}|
+WordCount: {{ .WordCount }}|
+FuzzyWordCount: {{ .FuzzyWordCount }}|
+{{ with .Summary }}
+Summary: {{ . }}|
+Summary Type: {{ .Type }}|
+Summary Truncated: {{ .Truncated }}|
+{{ end }}
+{{ end }}
+
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContentExact("public/p1/index.html",
+ "Content:
This is summary.
\n
This is content.
",
+ "ContentWithoutSummary:
This is content.
|",
+ "WordCount: 6|",
+ "FuzzyWordCount: 100|",
+ "Summary:
This is summary.
|",
+ "Summary Type: manual|",
+ "Summary Truncated: true|",
+ )
+ b.AssertFileContent("public/p2/index.html",
+ "Summary:
This is some content about a summary and more.
|",
+ "WordCount: 13|",
+ "FuzzyWordCount: 100|",
+ "Summary Type: auto",
+ "Summary Truncated: true",
+ )
+
+ b.AssertFileContentExact("public/p3/index.html",
+ "Summary: This is summary in front matter.|",
+ "ContentWithoutSummary:
This is content.
\n|",
+ )
+}
+
+func TestPageMarkupWithoutSummaryRST(t *testing.T) {
+ t.Parallel()
+ if !rst.Supports() {
+ t.Skip("Skip RST test as not supported")
+ }
+
+ files := `
+-- hugo.toml --
+summaryLength=5
+[security.exec]
+allow = ["rst", "python"]
+
+-- content/p1.rst --
+This is a story about a summary and more.
+
+Another paragraph.
+-- content/p2.rst --
+This is summary.
+
+This is content.
+-- layouts/_default/single.html --
+Single.
+Page.Summary: {{ .Summary }}|
+{{ with .Markup.Render }}
+Content: {{ .Content }}|
+ContentWithoutSummary: {{ .ContentWithoutSummary }}|
+{{ with .Summary }}
+Summary: {{ . }}|
+Summary Type: {{ .Type }}|
+Summary Truncated: {{ .Truncated }}|
+{{ end }}
+{{ end }}
+
+`
+
+ b := hugolib.Test(t, files)
+
+ // Auto summary.
+ b.AssertFileContentExact("public/p1/index.html",
+ "Content:
\n\n\n
This is a story about a summary and more.
\n
Another paragraph.
\n
|",
+ "Summary:
\n\n\n
This is a story about a summary and more.
|\nSummary Type: auto|\nSummary Truncated: true|",
+ "ContentWithoutSummary:
|",
+ )
+
+ // Manual summary.
+ b.AssertFileContentExact("public/p2/index.html",
+ "Content:
\n\n\n
This is summary.
\n
This is content.
\n
|",
+ "ContentWithoutSummary:
|",
+ "Summary:
|\nSummary Type: manual|\nSummary Truncated: true|",
+ )
+}
+
+func TestPageMarkupWithoutSummaryAsciidoc(t *testing.T) {
+ t.Parallel()
+ if !asciidocext.Supports() {
+ t.Skip("Skip asiidoc test as not supported")
+ }
+
+ files := `
+-- hugo.toml --
+summaryLength=5
+[security.exec]
+allow = ["asciidoc", "python"]
+
+-- content/p1.ad --
+This is a story about a summary and more.
+
+Another paragraph.
+-- content/p2.ad --
+This is summary.
+
+This is content.
+-- layouts/_default/single.html --
+Single.
+Page.Summary: {{ .Summary }}|
+{{ with .Markup.Render }}
+Content: {{ .Content }}|
+ContentWithoutSummary: {{ .ContentWithoutSummary }}|
+{{ with .Summary }}
+Summary: {{ . }}|
+Summary Type: {{ .Type }}|
+Summary Truncated: {{ .Truncated }}|
+{{ end }}
+{{ end }}
+
+`
+
+ b := hugolib.Test(t, files)
+
+ // Auto summary.
+ b.AssertFileContentExact("public/p1/index.html",
+ "Content:
\n
This is a story about a summary and more.
\n
\n
\n|",
+ "Summary:
\n
This is a story about a summary and more.
\n
|",
+ "Summary Type: auto|\nSummary Truncated: true|",
+ "ContentWithoutSummary:
|",
+ )
+
+ // Manual summary.
+ b.AssertFileContentExact("public/p2/index.html",
+ "Content:
\n
|",
+ "ContentWithoutSummary:
|",
+ "Summary:
|\nSummary Type: manual|\nSummary Truncated: true|",
+ )
+}
diff --git a/resources/page/page_markup_test.go b/resources/page/page_markup_test.go
new file mode 100644
index 000000000..43eaae6f6
--- /dev/null
+++ b/resources/page/page_markup_test.go
@@ -0,0 +1,208 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "strings"
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/common/types"
+ "github.com/gohugoio/hugo/media"
+)
+
+func TestExtractSummaryFromHTML(t *testing.T) {
+ c := qt.New(t)
+
+ tests := []struct {
+ mt media.Type
+ input string
+ isCJK bool
+ numWords int
+ expectSummary string
+ expectContentWithoutSummary string
+ }{
+ {media.Builtin.ReStructuredTextType, "
", false, 70, "
", ""},
+ {media.Builtin.ReStructuredTextType, "
First paragraph
Second paragraph
", false, 2, `
`, "
"},
+ {media.Builtin.MarkdownType, "
First paragraph
", false, 10, "
First paragraph
", ""},
+ {media.Builtin.MarkdownType, "
First paragraph
Second paragraph
", false, 2, "
First paragraph
", "
Second paragraph
"},
+ {media.Builtin.MarkdownType, "
First paragraph
Second paragraph
Third paragraph
", false, 3, "
First paragraph
Second paragraph
", "
Third paragraph
"},
+ {media.Builtin.AsciiDocType, "
", false, 2, "
", "
"},
+ {media.Builtin.MarkdownType, "
这是中文,全中文
a这是中文,全中文
", true, 5, "
这是中文,全中文
", "
a这是中文,全中文
"},
+ }
+
+ for i, test := range tests {
+ summary := ExtractSummaryFromHTML(test.mt, test.input, test.numWords, test.isCJK)
+ c.Assert(summary.Summary(), qt.Equals, test.expectSummary, qt.Commentf("Summary %d", i))
+ c.Assert(summary.ContentWithoutSummary(), qt.Equals, test.expectContentWithoutSummary, qt.Commentf("ContentWithoutSummary %d", i))
+ }
+}
+
+// See https://discourse.gohugo.io/t/automatic-summarys-summarylength-seems-broken-in-the-case-of-plainify/51466/4
+// Also issue 12837
+func TestExtractSummaryFromHTMLLotsOfHTMLInSummary(t *testing.T) {
+ c := qt.New(t)
+
+ input := `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+This is a story about a cat.
+
+
+The cat was white and fluffy.
+
+
+And it liked milk.
+
+`
+
+ summary := ExtractSummaryFromHTML(media.Builtin.MarkdownType, input, 10, false)
+ c.Assert(strings.HasSuffix(summary.Summary(), "
\nThis is a story about a cat.\n
\n
\nThe cat was white and fluffy.\n
"), qt.IsTrue)
+}
+
+func TestExtractSummaryFromHTMLWithDivider(t *testing.T) {
+ c := qt.New(t)
+
+ const divider = "FOOO"
+
+ tests := []struct {
+ mt media.Type
+ input string
+ expectSummary string
+ expectContentWithoutSummary string
+ expectContent string
+ }{
+ {media.Builtin.MarkdownType, "
First paragraph
FOOO
Second paragraph
", "
First paragraph
", "
Second paragraph
", "
First paragraph
Second paragraph
"},
+ {media.Builtin.MarkdownType, "
First paragraph
\n
FOOO
\n
Second paragraph
", "
First paragraph
", "
Second paragraph
", "
First paragraph
\n
Second paragraph
"},
+ {media.Builtin.MarkdownType, "
FOOO
\n
First paragraph
", "", "
First paragraph
", "
First paragraph
"},
+ {media.Builtin.MarkdownType, "
First paragraph
Second paragraphFOOO
Third paragraph
", "
First paragraph
Second paragraph
", "
Third paragraph
", "
First paragraph
Second paragraph
Third paragraph
"},
+ {media.Builtin.MarkdownType, "
这是中文,全中文FOOO
a这是中文,全中文
", "
这是中文,全中文
", "
a这是中文,全中文
", "
这是中文,全中文
a这是中文,全中文
"},
+ {media.Builtin.MarkdownType, `
a b ` + "\v" + ` c
` + "\n
FOOO
", "
a b \v c
", "", "
a b \v c
"},
+
+ {media.Builtin.HTMLType, "
First paragraph
FOOO
Second paragraph
", "
First paragraph
", "
Second paragraph
", "
First paragraph
Second paragraph
"},
+
+ {media.Builtin.ReStructuredTextType, "
\n\n\n
This is summary.
\n
FOOO
\n
This is content.
\n
", "
", "
", "
\n\n\n
This is summary.
\n
This is content.
\n
"},
+ {media.Builtin.ReStructuredTextType, "
First paragraphFOOO
Second paragraph
", "
", "
", `
First paragraph
Second paragraph
`},
+
+ {media.Builtin.AsciiDocType, "
", "
", "
", "
"},
+ {media.Builtin.AsciiDocType, "
\n
\n
\n", "
", "
", "
\n
"},
+ {media.Builtin.AsciiDocType, "
", "", "
", "
"},
+ {media.Builtin.AsciiDocType, "
", "
", "
", "
"},
+ }
+
+ for i, test := range tests {
+ summary := ExtractSummaryFromHTMLWithDivider(test.mt, test.input, divider)
+ c.Assert(summary.Summary(), qt.Equals, test.expectSummary, qt.Commentf("Summary %d", i))
+ c.Assert(summary.ContentWithoutSummary(), qt.Equals, test.expectContentWithoutSummary, qt.Commentf("ContentWithoutSummary %d", i))
+ c.Assert(summary.Content(), qt.Equals, test.expectContent, qt.Commentf("Content %d", i))
+ }
+}
+
+func TestExpandDivider(t *testing.T) {
+ c := qt.New(t)
+
+ for i, test := range []struct {
+ input string
+ divider string
+ ptag tagReStartEnd
+ expect string
+ expectEndMarkup string
+ }{
+ {"
First paragraph
\n
FOOO
\n
Second paragraph
", "FOOO", startEndP, "
FOOO
\n", ""},
+ {"
", "FOOO", startEndDiv, "
", ""},
+ {"
", "FOOO", startEndDiv, "
", ""},
+ {"
", "FOOO", startEndDiv, "FOOO", "
"},
+ {" abc FOOO
", "FOOO", startEndP, "FOOO", " "},
+ {" FOOO
", "FOOO", startEndP, " FOOO
", ""},
+ {" \n \nFOOO
", "FOOO", startEndP, "\n \nFOOO
", ""},
+ {" FOOO
", "FOOO", startEndDiv, " FOOO
", ""},
+ } {
+
+ l := types.LowHigh[string]{Low: strings.Index(test.input, test.divider), High: strings.Index(test.input, test.divider) + len(test.divider)}
+ e, t := expandSummaryDivider(test.input, test.ptag, l)
+ c.Assert(test.input[e.Low:e.High], qt.Equals, test.expect, qt.Commentf("[%d] Test.expect %q", i, test.input))
+ c.Assert(test.input[t.Low:t.High], qt.Equals, test.expectEndMarkup, qt.Commentf("[%d] Test.expectEndMarkup %q", i, test.input))
+ }
+}
+
+func TestIsProbablyHTMLToken(t *testing.T) {
+ c := qt.New(t)
+
+ for i, test := range []struct {
+ input string
+ expect bool
+ }{
+ {"", true},
+ {"
Æøå", false},
+ } {
+ c.Assert(isProbablyHTMLToken(test.input), qt.Equals, test.expect, qt.Commentf("[%d] Test.expect %q", i, test.input))
+ }
+}
+
+func BenchmarkSummaryFromHTML(b *testing.B) {
+ b.StopTimer()
+ input := "
First paragraph
Second paragraph
"
+ b.StartTimer()
+ for i := 0; i < b.N; i++ {
+ summary := ExtractSummaryFromHTML(media.Builtin.MarkdownType, input, 2, false)
+ if s := summary.Content(); s != input {
+ b.Fatalf("unexpected content: %q", s)
+ }
+ if s := summary.ContentWithoutSummary(); s != "Second paragraph
" {
+ b.Fatalf("unexpected content without summary: %q", s)
+ }
+ if s := summary.Summary(); s != "First paragraph
" {
+ b.Fatalf("unexpected summary: %q", s)
+ }
+ }
+}
+
+func BenchmarkSummaryFromHTMLWithDivider(b *testing.B) {
+ b.StopTimer()
+ input := "First paragraph
FOOO
Second paragraph
"
+ b.StartTimer()
+ for i := 0; i < b.N; i++ {
+ summary := ExtractSummaryFromHTMLWithDivider(media.Builtin.MarkdownType, input, "FOOO")
+ if s := summary.Content(); s != "First paragraph
Second paragraph
" {
+ b.Fatalf("unexpected content: %q", s)
+ }
+ if s := summary.ContentWithoutSummary(); s != "Second paragraph
" {
+ b.Fatalf("unexpected content without summary: %q", s)
+ }
+ if s := summary.Summary(); s != "First paragraph
" {
+ b.Fatalf("unexpected summary: %q", s)
+ }
+ }
+}
diff --git a/resources/page/page_matcher.go b/resources/page/page_matcher.go
index f2075273a..858def5ef 100644
--- a/resources/page/page_matcher.go
+++ b/resources/page/page_matcher.go
@@ -16,6 +16,7 @@ package page
import (
"fmt"
"path/filepath"
+ "slices"
"strings"
"github.com/gohugoio/hugo/common/loggers"
@@ -104,9 +105,9 @@ func CheckCascadePattern(logger loggers.Logger, m PageMatcher) {
}
}
-func DecodeCascadeConfig(logger loggers.Logger, in any) (*config.ConfigNamespace[[]PageMatcherParamsConfig, map[PageMatcher]maps.Params], error) {
- buildConfig := func(in any) (map[PageMatcher]maps.Params, any, error) {
- cascade := make(map[PageMatcher]maps.Params)
+func DecodeCascadeConfig(logger loggers.Logger, handleLegacyFormat bool, in any) (*config.ConfigNamespace[[]PageMatcherParamsConfig, *maps.Ordered[PageMatcher, PageMatcherParamsConfig]], error) {
+ buildConfig := func(in any) (*maps.Ordered[PageMatcher, PageMatcherParamsConfig], any, error) {
+ cascade := maps.NewOrdered[PageMatcher, PageMatcherParamsConfig]()
if in == nil {
return cascade, []map[string]any{}, nil
}
@@ -119,7 +120,11 @@ func DecodeCascadeConfig(logger loggers.Logger, in any) (*config.ConfigNamespace
for _, m := range ms {
m = maps.CleanConfigStringMap(m)
- c, err := mapToPageMatcherParamsConfig(m)
+ var (
+ c PageMatcherParamsConfig
+ err error
+ )
+ c, err = mapToPageMatcherParamsConfig(m)
if err != nil {
return nil, nil, err
}
@@ -134,28 +139,33 @@ func DecodeCascadeConfig(logger loggers.Logger, in any) (*config.ConfigNamespace
for _, cfg := range cfgs {
m := cfg.Target
CheckCascadePattern(logger, m)
- c, found := cascade[m]
+ c, found := cascade.Get(m)
if found {
// Merge
for k, v := range cfg.Params {
- if _, found := c[k]; !found {
- c[k] = v
+ if _, found := c.Params[k]; !found {
+ c.Params[k] = v
+ }
+ }
+ for k, v := range cfg.Fields {
+ if _, found := c.Fields[k]; !found {
+ c.Fields[k] = v
}
}
} else {
- cascade[m] = cfg.Params
+ cascade.Set(m, cfg)
}
}
return cascade, cfgs, nil
}
- return config.DecodeNamespace[[]PageMatcherParamsConfig](in, buildConfig)
+ return config.DecodeNamespace[[]PageMatcherParamsConfig, *maps.Ordered[PageMatcher, PageMatcherParamsConfig]](in, buildConfig)
}
// DecodeCascade decodes in which could be either a map or a slice of maps.
-func DecodeCascade(logger loggers.Logger, in any) (map[PageMatcher]maps.Params, error) {
- conf, err := DecodeCascadeConfig(logger, in)
+func DecodeCascade(logger loggers.Logger, handleLegacyFormat bool, in any) (*maps.Ordered[PageMatcher, PageMatcherParamsConfig], error) {
+ conf, err := DecodeCascadeConfig(logger, handleLegacyFormat, in)
if err != nil {
return nil, err
}
@@ -164,36 +174,30 @@ func DecodeCascade(logger loggers.Logger, in any) (map[PageMatcher]maps.Params,
func mapToPageMatcherParamsConfig(m map[string]any) (PageMatcherParamsConfig, error) {
var pcfg PageMatcherParamsConfig
+ if pcfg.Fields == nil {
+ pcfg.Fields = make(maps.Params)
+ }
for k, v := range m {
switch strings.ToLower(k) {
- case "params":
- // We simplified the structure of the cascade config in Hugo 0.111.0.
- // There is a small chance that someone has used the old structure with the params keyword,
- // those values will now be moved to the top level.
- // This should be very unlikely as it would lead to constructs like .Params.params.foo,
- // and most people see params as an Hugo internal keyword.
- params := maps.ToStringMap(v)
- if pcfg.Params == nil {
- pcfg.Params = params
- } else {
- for k, v := range params {
- if _, found := pcfg.Params[k]; !found {
- pcfg.Params[k] = v
- }
- }
- }
case "_target", "target":
var target PageMatcher
if err := decodePageMatcher(v, &target); err != nil {
return pcfg, err
}
pcfg.Target = target
- default:
- // Legacy config.
+ case "params":
if pcfg.Params == nil {
pcfg.Params = make(maps.Params)
}
- pcfg.Params[k] = v
+ params := maps.ToStringMap(v)
+ for k, v := range params {
+ if _, found := pcfg.Params[k]; !found {
+ pcfg.Params[k] = v
+ }
+ }
+ default:
+
+ pcfg.Fields[k] = v
}
}
return pcfg, pcfg.init()
@@ -208,13 +212,7 @@ func decodePageMatcher(m any, v *PageMatcher) error {
v.Kind = strings.ToLower(v.Kind)
if v.Kind != "" {
g, _ := glob.GetGlob(v.Kind)
- found := false
- for _, k := range kinds.AllKindsInPages {
- if g.Match(k) {
- found = true
- break
- }
- }
+ found := slices.ContainsFunc(kinds.AllKindsInPages, g.Match)
if !found {
return fmt.Errorf("%q did not match a valid Page Kind", v.Kind)
}
@@ -228,10 +226,14 @@ func decodePageMatcher(m any, v *PageMatcher) error {
type PageMatcherParamsConfig struct {
// Apply Params to all Pages matching Target.
Params maps.Params
+ // Fields holds all fields but Params.
+ Fields maps.Params
+ // Target is the PageMatcher that this config applies to.
Target PageMatcher
}
func (p *PageMatcherParamsConfig) init() error {
maps.PrepareParams(p.Params)
+ maps.PrepareParams(p.Fields)
return nil
}
diff --git a/resources/page/page_matcher_test.go b/resources/page/page_matcher_test.go
index dfb479d5e..7f441d3ab 100644
--- a/resources/page/page_matcher_test.go
+++ b/resources/page/page_matcher_test.go
@@ -88,19 +88,18 @@ func TestPageMatcher(t *testing.T) {
c.Assert(err, qt.IsNil)
return v
}
- // Legacy.
c.Assert(fn(map[string]any{"_target": map[string]any{"kind": "page"}, "foo": "bar"}), qt.DeepEquals, PageMatcherParamsConfig{
- Params: maps.Params{
+ Fields: maps.Params{
"foo": "bar",
},
Target: PageMatcher{Path: "", Kind: "page", Lang: "", Environment: ""},
})
- // Current format.
c.Assert(fn(map[string]any{"target": map[string]any{"kind": "page"}, "params": map[string]any{"foo": "bar"}}), qt.DeepEquals, PageMatcherParamsConfig{
Params: maps.Params{
"foo": "bar",
},
+ Fields: maps.Params{},
Target: PageMatcher{Path: "", Kind: "page", Lang: "", Environment: ""},
})
})
@@ -129,29 +128,22 @@ func TestDecodeCascadeConfig(t *testing.T) {
},
}
- got, err := DecodeCascadeConfig(loggers.NewDefault(), in)
+ got, err := DecodeCascadeConfig(loggers.NewDefault(), true, in)
c.Assert(err, qt.IsNil)
c.Assert(got, qt.IsNotNil)
- c.Assert(got.Config, qt.DeepEquals,
- map[PageMatcher]maps.Params{
- {Path: "", Kind: "page", Lang: "", Environment: ""}: {
- "b": "bv",
- },
- {Path: "", Kind: "page", Lang: "", Environment: "production"}: {
- "a": "av",
- },
- },
- )
+ c.Assert(got.Config.Keys(), qt.DeepEquals, []PageMatcher{{Kind: "page", Environment: "production"}, {Kind: "page"}})
+
c.Assert(got.SourceStructure, qt.DeepEquals, []PageMatcherParamsConfig{
{
Params: maps.Params{"a": string("av")},
+ Fields: maps.Params{},
Target: PageMatcher{Kind: "page", Environment: "production"},
},
- {Params: maps.Params{"b": string("bv")}, Target: PageMatcher{Kind: "page"}},
+ {Params: maps.Params{"b": string("bv")}, Fields: maps.Params{}, Target: PageMatcher{Kind: "page"}},
})
- got, err = DecodeCascadeConfig(loggers.NewDefault(), nil)
+ got, err = DecodeCascadeConfig(loggers.NewDefault(), true, nil)
c.Assert(err, qt.IsNil)
c.Assert(got, qt.IsNotNil)
}
diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go
index d3813337d..398a7df02 100644
--- a/resources/page/page_nop.go
+++ b/resources/page/page_nop.go
@@ -21,7 +21,6 @@ import (
"html/template"
"time"
- "github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/tableofcontents"
@@ -44,6 +43,8 @@ import (
var (
NopPage Page = new(nopPage)
NopContentRenderer ContentRenderer = new(nopContentRenderer)
+ NopMarkup Markup = new(nopMarkup)
+ NopContent Content = new(nopContent)
NopCPageContentRenderer = struct {
OutputFormatPageContentProvider
ContentRenderer
@@ -57,12 +58,6 @@ var (
// PageNop implements Page, but does nothing.
type nopPage int
-var noOpPathInfo = paths.Parse(files.ComponentFolderContent, "no-op.md")
-
-func (p *nopPage) Err() resource.ResourceError {
- return nil
-}
-
func (p *nopPage) Aliases() []string {
return nil
}
@@ -75,20 +70,6 @@ func (p *nopPage) Layout() string {
return ""
}
-func (p *nopPage) RSSLink() template.URL {
- return ""
-}
-
-// Deprecated: Use taxonomies instead.
-func (p *nopPage) Author() Author {
- return Author{}
-}
-
-// Deprecated: Use taxonomies instead.
-func (p *nopPage) Authors() AuthorList {
- return nil
-}
-
func (p *nopPage) AllTranslations() Pages {
return nil
}
@@ -109,10 +90,18 @@ func (p *nopPage) BundleType() string {
return ""
}
+func (p *nopPage) Markup(...any) Markup {
+ return NopMarkup
+}
+
func (p *nopPage) Content(context.Context) (any, error) {
return "", nil
}
+func (p *nopPage) ContentWithoutSummary(ctx context.Context) (template.HTML, error) {
+ return "", nil
+}
+
func (p *nopPage) ContentBaseName() string {
return ""
}
@@ -157,14 +146,6 @@ func (p *nopPage) ExpiryDate() (t time.Time) {
return
}
-func (p *nopPage) Ext() string {
- return ""
-}
-
-func (p *nopPage) Extension() string {
- return ""
-}
-
func (p *nopPage) File() *source.File {
return nil
}
@@ -354,7 +335,7 @@ func (p *nopPage) Path() string {
}
func (p *nopPage) PathInfo() *paths.Path {
- return noOpPathInfo
+ return nil
}
func (p *nopPage) Permalink() string {
@@ -547,3 +528,69 @@ func (r *nopContentRenderer) ParseContent(ctx context.Context, content []byte) (
func (r *nopContentRenderer) RenderContent(ctx context.Context, content []byte, doc any) (converter.ResultRender, bool, error) {
return nil, false, nil
}
+
+type (
+ nopMarkup int
+ nopContent int
+)
+
+var (
+ _ Markup = (*nopMarkup)(nil)
+ _ Content = (*nopContent)(nil)
+)
+
+func (c *nopMarkup) Render(context.Context) (Content, error) {
+ return NopContent, nil
+}
+
+func (c *nopMarkup) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
+ return "", nil
+}
+
+func (c *nopMarkup) RenderShortcodes(context.Context) (template.HTML, error) {
+ return "", nil
+}
+
+func (c *nopContent) Plain(context.Context) string {
+ return ""
+}
+
+func (c *nopContent) PlainWords(context.Context) []string {
+ return nil
+}
+
+func (c *nopContent) WordCount(context.Context) int {
+ return 0
+}
+
+func (c *nopContent) FuzzyWordCount(context.Context) int {
+ return 0
+}
+
+func (c *nopContent) ReadingTime(context.Context) int {
+ return 0
+}
+
+func (c *nopContent) Len(context.Context) int {
+ return 0
+}
+
+func (c *nopContent) Content(context.Context) (template.HTML, error) {
+ return "", nil
+}
+
+func (c *nopContent) ContentWithoutSummary(context.Context) (template.HTML, error) {
+ return "", nil
+}
+
+func (c *nopMarkup) Fragments(context.Context) *tableofcontents.Fragments {
+ return nil
+}
+
+func (c *nopMarkup) FragmentsHTML(context.Context) template.HTML {
+ return ""
+}
+
+func (c *nopContent) Summary(context.Context) (Summary, error) {
+ return Summary{}, nil
+}
diff --git a/resources/page/page_paths.go b/resources/page/page_paths.go
index 4826ed5f9..6b2c8e8b1 100644
--- a/resources/page/page_paths.go
+++ b/resources/page/page_paths.go
@@ -145,7 +145,7 @@ func CreateTargetPaths(d TargetPathDescriptor) (tp TargetPaths) {
pb.isUgly = true
}
- if d.Type == output.HTTPStatusHTMLFormat || d.Type == output.SitemapFormat || d.Type == output.RobotsTxtFormat {
+ if d.Type == output.HTTPStatus404HTMLFormat || d.Type == output.SitemapFormat || d.Type == output.RobotsTxtFormat {
pb.noSubResources = true
} else if d.Kind != kinds.KindPage && d.URL == "" && d.Section.Base() != "/" {
if d.ExpandedPermalink != "" {
@@ -254,7 +254,7 @@ func CreateTargetPaths(d TargetPathDescriptor) (tp TargetPaths) {
// if page URL is explicitly set in frontmatter,
// preserve its value without sanitization
- if d.Kind != kinds.KindPage || d.URL == "" {
+ if d.URL == "" {
// Note: MakePathSanitized will lower case the path if
// disablePathToLower isn't set.
pb.Sanitize()
diff --git a/resources/page/pagegroup.go b/resources/page/pagegroup.go
index 7129fae17..081708d62 100644
--- a/resources/page/pagegroup.go
+++ b/resources/page/pagegroup.go
@@ -205,7 +205,7 @@ func (p Pages) GroupByParam(key string, order ...string) (PagesGroup, error) {
}
}
if !tmp.IsValid() {
- return nil, errors.New("there is no such param")
+ return nil, nil
}
for _, e := range p {
diff --git a/resources/page/pagegroup_test.go b/resources/page/pagegroup_test.go
index 91f05b24a..69243ac48 100644
--- a/resources/page/pagegroup_test.go
+++ b/resources/page/pagegroup_test.go
@@ -15,7 +15,7 @@ package page
import (
"context"
- "reflect"
+ "github.com/google/go-cmp/cmp"
"strings"
"testing"
@@ -55,12 +55,23 @@ func preparePageGroupTestPages(t *testing.T) Pages {
p.params["custom_param"] = src.param
p.params["custom_date"] = cast.ToTime(src.date)
p.params["custom_string_date"] = src.date
+ p.params["custom_object"] = map[string]any{
+ "param": src.param,
+ "date": cast.ToTime(src.date),
+ "string_date": src.date,
+ }
pages = append(pages, p)
}
return pages
}
+var comparePageGroup = qt.CmpEquals(cmp.Comparer(func(a, b Page) bool {
+ return a == b
+}))
+
func TestGroupByWithFieldNameArg(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
expect := PagesGroup{
@@ -70,15 +81,13 @@ func TestGroupByWithFieldNameArg(t *testing.T) {
}
groups, err := pages.GroupBy(context.Background(), "Weight")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if !reflect.DeepEqual(groups, expect) {
- t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
func TestGroupByWithMethodNameArg(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
expect := PagesGroup{
@@ -87,15 +96,13 @@ func TestGroupByWithMethodNameArg(t *testing.T) {
}
groups, err := pages.GroupBy(context.Background(), "Type")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if !reflect.DeepEqual(groups, expect) {
- t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
func TestGroupByWithSectionArg(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
expect := PagesGroup{
@@ -104,15 +111,13 @@ func TestGroupByWithSectionArg(t *testing.T) {
}
groups, err := pages.GroupBy(context.Background(), "Section")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if !reflect.DeepEqual(groups, expect) {
- t.Errorf("PagesGroup has unexpected groups. It should be\n%#v, got\n%#v", expect, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
func TestGroupByInReverseOrder(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
expect := PagesGroup{
@@ -122,56 +127,39 @@ func TestGroupByInReverseOrder(t *testing.T) {
}
groups, err := pages.GroupBy(context.Background(), "Weight", "desc")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if !reflect.DeepEqual(groups, expect) {
- t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
func TestGroupByCalledWithEmptyPages(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
var pages Pages
groups, err := pages.GroupBy(context.Background(), "Weight")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if groups != nil {
- t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups)
- }
-}
-
-func TestGroupByParamCalledWithUnavailableKey(t *testing.T) {
- t.Parallel()
- pages := preparePageGroupTestPages(t)
- _, err := pages.GroupByParam("UnavailableKey")
- if err == nil {
- t.Errorf("GroupByParam should return an error but didn't")
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, qt.IsNil)
}
func TestReverse(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
groups1, err := pages.GroupBy(context.Background(), "Weight", "desc")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
+ c.Assert(err, qt.IsNil)
groups2, err := pages.GroupBy(context.Background(), "Weight")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- groups2 = groups2.Reverse()
+ c.Assert(err, qt.IsNil)
- if !reflect.DeepEqual(groups2, groups1) {
- t.Errorf("PagesGroup is sorted in unexpected order. It should be %#v, got %#v", groups2, groups1)
- }
+ groups2 = groups2.Reverse()
+ c.Assert(groups2, comparePageGroup, groups1)
}
func TestGroupByParam(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
expect := PagesGroup{
@@ -181,15 +169,13 @@ func TestGroupByParam(t *testing.T) {
}
groups, err := pages.GroupByParam("custom_param")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if !reflect.DeepEqual(groups, expect) {
- t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
func TestGroupByParamInReverseOrder(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
expect := PagesGroup{
@@ -199,12 +185,8 @@ func TestGroupByParamInReverseOrder(t *testing.T) {
}
groups, err := pages.GroupByParam("custom_param", "desc")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if !reflect.DeepEqual(groups, expect) {
- t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
func TestGroupByParamCalledWithCapitalLetterString(t *testing.T) {
@@ -217,10 +199,12 @@ func TestGroupByParamCalledWithCapitalLetterString(t *testing.T) {
groups, err := pages.GroupByParam("custom_param")
c.Assert(err, qt.IsNil)
- c.Assert(groups[0].Key, qt.Equals, testStr)
+ c.Assert(groups[0].Key, qt.DeepEquals, testStr)
}
func TestGroupByParamCalledWithSomeUnavailableParams(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
delete(pages[1].Params(), "custom_param")
@@ -232,36 +216,49 @@ func TestGroupByParamCalledWithSomeUnavailableParams(t *testing.T) {
}
groups, err := pages.GroupByParam("custom_param")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if !reflect.DeepEqual(groups, expect) {
- t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
func TestGroupByParamCalledWithEmptyPages(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
var pages Pages
groups, err := pages.GroupByParam("custom_param")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if groups != nil {
- t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, qt.IsNil)
}
func TestGroupByParamCalledWithUnavailableParam(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
_, err := pages.GroupByParam("unavailable_param")
- if err == nil {
- t.Errorf("GroupByParam should return an error but didn't")
+ c.Assert(err, qt.IsNil)
+}
+
+func TestGroupByParamNested(t *testing.T) {
+ c := qt.New(t)
+
+ t.Parallel()
+ pages := preparePageGroupTestPages(t)
+
+ expect := PagesGroup{
+ {Key: "bar", Pages: Pages{pages[1], pages[3]}},
+ {Key: "baz", Pages: Pages{pages[4]}},
+ {Key: "foo", Pages: Pages{pages[0], pages[2]}},
}
+
+ groups, err := pages.GroupByParam("custom_object.param")
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
func TestGroupByDate(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
expect := PagesGroup{
@@ -271,15 +268,13 @@ func TestGroupByDate(t *testing.T) {
}
groups, err := pages.GroupByDate("2006-01")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if !reflect.DeepEqual(groups, expect) {
- t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
func TestGroupByDateInReverseOrder(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
expect := PagesGroup{
@@ -289,15 +284,13 @@ func TestGroupByDateInReverseOrder(t *testing.T) {
}
groups, err := pages.GroupByDate("2006-01", "asc")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if !reflect.DeepEqual(groups, expect) {
- t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
func TestGroupByPublishDate(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
expect := PagesGroup{
@@ -307,15 +300,13 @@ func TestGroupByPublishDate(t *testing.T) {
}
groups, err := pages.GroupByPublishDate("2006-01")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if !reflect.DeepEqual(groups, expect) {
- t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
func TestGroupByPublishDateInReverseOrder(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
expect := PagesGroup{
@@ -325,27 +316,23 @@ func TestGroupByPublishDateInReverseOrder(t *testing.T) {
}
groups, err := pages.GroupByDate("2006-01", "asc")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if !reflect.DeepEqual(groups, expect) {
- t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
func TestGroupByPublishDateWithEmptyPages(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
var pages Pages
groups, err := pages.GroupByPublishDate("2006-01")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if groups != nil {
- t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, qt.IsNil)
}
func TestGroupByExpiryDate(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
expect := PagesGroup{
@@ -355,15 +342,13 @@ func TestGroupByExpiryDate(t *testing.T) {
}
groups, err := pages.GroupByExpiryDate("2006-01")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if !reflect.DeepEqual(groups, expect) {
- t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
func TestGroupByParamDate(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
expect := PagesGroup{
@@ -373,16 +358,30 @@ func TestGroupByParamDate(t *testing.T) {
}
groups, err := pages.GroupByParamDate("custom_date", "2006-01")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if !reflect.DeepEqual(groups, expect) {
- t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
+}
+
+func TestGroupByParamDateNested(t *testing.T) {
+ c := qt.New(t)
+
+ t.Parallel()
+ pages := preparePageGroupTestPages(t)
+ expect := PagesGroup{
+ {Key: "2012-04", Pages: Pages{pages[4], pages[2], pages[0]}},
+ {Key: "2012-03", Pages: Pages{pages[3]}},
+ {Key: "2012-01", Pages: Pages{pages[1]}},
}
+
+ groups, err := pages.GroupByParamDate("custom_object.date", "2006-01")
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
// https://github.com/gohugoio/hugo/issues/3983
func TestGroupByParamDateWithStringParams(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
expect := PagesGroup{
@@ -392,15 +391,29 @@ func TestGroupByParamDateWithStringParams(t *testing.T) {
}
groups, err := pages.GroupByParamDate("custom_string_date", "2006-01")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if !reflect.DeepEqual(groups, expect) {
- t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
+}
+
+func TestGroupByParamDateNestedWithStringParams(t *testing.T) {
+ c := qt.New(t)
+
+ t.Parallel()
+ pages := preparePageGroupTestPages(t)
+ expect := PagesGroup{
+ {Key: "2012-04", Pages: Pages{pages[4], pages[2], pages[0]}},
+ {Key: "2012-03", Pages: Pages{pages[3]}},
+ {Key: "2012-01", Pages: Pages{pages[1]}},
}
+
+ groups, err := pages.GroupByParamDate("custom_object.string_date", "2006-01")
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
func TestGroupByLastmod(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
expect := PagesGroup{
@@ -410,15 +423,13 @@ func TestGroupByLastmod(t *testing.T) {
}
groups, err := pages.GroupByLastmod("2006-01")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if !reflect.DeepEqual(groups, expect) {
- t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
func TestGroupByLastmodInReverseOrder(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
expect := PagesGroup{
@@ -428,15 +439,13 @@ func TestGroupByLastmodInReverseOrder(t *testing.T) {
}
groups, err := pages.GroupByLastmod("2006-01", "asc")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if !reflect.DeepEqual(groups, expect) {
- t.Errorf("PagesGroup has unexpected groups. It should be\n%#v, got\n%#v", expect, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
func TestGroupByParamDateInReverseOrder(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
pages := preparePageGroupTestPages(t)
expect := PagesGroup{
@@ -446,22 +455,16 @@ func TestGroupByParamDateInReverseOrder(t *testing.T) {
}
groups, err := pages.GroupByParamDate("custom_date", "2006-01", "asc")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if !reflect.DeepEqual(groups, expect) {
- t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, comparePageGroup, expect)
}
func TestGroupByParamDateWithEmptyPages(t *testing.T) {
+ c := qt.New(t)
+
t.Parallel()
var pages Pages
groups, err := pages.GroupByParamDate("custom_date", "2006-01")
- if err != nil {
- t.Fatalf("Unable to make PagesGroup array: %s", err)
- }
- if groups != nil {
- t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups)
- }
+ c.Assert(err, qt.IsNil)
+ c.Assert(groups, qt.IsNil)
}
diff --git a/resources/page/pagemeta/page_frontmatter.go b/resources/page/pagemeta/page_frontmatter.go
index 123dd4b70..fd4f7759b 100644
--- a/resources/page/pagemeta/page_frontmatter.go
+++ b/resources/page/pagemeta/page_frontmatter.go
@@ -14,14 +14,26 @@
package pagemeta
import (
+ "errors"
+ "fmt"
+ "path"
"strings"
"time"
+ "github.com/gohugoio/hugo/common/hreflect"
"github.com/gohugoio/hugo/common/htime"
+ "github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
+ "github.com/gohugoio/hugo/hugofs/files"
+ "github.com/gohugoio/hugo/markup"
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/resources/kinds"
"github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/mitchellh/mapstructure"
"github.com/gohugoio/hugo/helpers"
@@ -29,6 +41,13 @@ import (
"github.com/spf13/cast"
)
+type DatesStrings struct {
+ Date string `json:"date"`
+ Lastmod string `json:"lastMod"`
+ PublishDate string `json:"publishDate"`
+ ExpiryDate string `json:"expiryDate"`
+}
+
type Dates struct {
Date time.Time
Lastmod time.Time
@@ -40,57 +59,310 @@ func (d Dates) IsDateOrLastModAfter(in Dates) bool {
return d.Date.After(in.Date) || d.Lastmod.After(in.Lastmod)
}
-func (d *Dates) UpdateDateAndLastmodIfAfter(in Dates) {
+func (d *Dates) UpdateDateAndLastmodAndPublishDateIfAfter(in Dates) {
if in.Date.After(d.Date) {
d.Date = in.Date
}
if in.Lastmod.After(d.Lastmod) {
d.Lastmod = in.Lastmod
}
+
+ if in.PublishDate.After(d.PublishDate) && in.PublishDate.Before(htime.Now()) {
+ d.PublishDate = in.PublishDate
+ }
}
func (d Dates) IsAllDatesZero() bool {
return d.Date.IsZero() && d.Lastmod.IsZero() && d.PublishDate.IsZero() && d.ExpiryDate.IsZero()
}
+// Page config that needs to be set early. These cannot be modified by cascade.
+type PageConfigEarly struct {
+ Kind string // The kind of page, e.g. "page", "section", "home" etc. This is usually derived from the content path.
+ Path string // The canonical path to the page, e.g. /sect/mypage. Note: Leading slash, no trailing slash, no extensions or language identifiers.
+ Lang string // The language code for this page. This is usually derived from the module mount or filename.
+ Cascade []map[string]any
+
+ // Content holds the content for this page.
+ Content Source
+}
+
// PageConfig configures a Page, typically from front matter.
// Note that all the top level fields are reserved Hugo keywords.
// Any custom configuration needs to be set in the Params map.
type PageConfig struct {
- Dates // Dates holds the four core dates for this page.
- Title string // The title of the page.
- LinkTitle string // The link title of the page.
- Type string // The content type of the page.
- Layout string // The layout to use for to render this page.
- Markup string // The markup used in the content file.
- Weight int // The weight of the page, used in sorting if set to a non-zero value.
- Kind string // The kind of page, e.g. "page", "section", "home" etc. This is usually derived from the content path.
- Path string // The canonical path to the page, e.g. /sect/mypage. Note: Leading slash, no trailing slash, no extensions or language identifiers.
- URL string // The URL to the rendered page, e.g. /sect/mypage.html.
- Lang string // The language code for this page. This is usually derived from the module mount or filename.
- Slug string // The slug for this page.
- Description string // The description for this page.
- Summary string // The summary for this page.
- Draft bool // Whether or not the content is a draft.
- Headless bool // Whether or not the page should be rendered.
- IsCJKLanguage bool // Whether or not the content is in a CJK language.
- TranslationKey string // The translation key for this page.
- Keywords []string // The keywords for this page.
- Aliases []string // The aliases for this page.
- Outputs []string // The output formats to render this page in. If not set, the site's configured output formats for this page kind will be used.
+ Dates Dates `json:"-"` // Dates holds the four core dates for this page.
+ DatesStrings
+ PageConfigEarly `mapstructure:",squash"`
+ Title string // The title of the page.
+ LinkTitle string // The link title of the page.
+ Type string // The content type of the page.
+ Layout string // The layout to use for to render this page.
+ Weight int // The weight of the page, used in sorting if set to a non-zero value.
+ URL string // The URL to the rendered page, e.g. /sect/mypage.html.
+ Slug string // The slug for this page.
+ Description string // The description for this page.
+ Summary string // The summary for this page.
+ Draft bool // Whether or not the content is a draft.
+ Headless bool `json:"-"` // Whether or not the page should be rendered.
+ IsCJKLanguage bool // Whether or not the content is in a CJK language.
+ TranslationKey string // The translation key for this page.
+ Keywords []string // The keywords for this page.
+ Aliases []string // The aliases for this page.
+ Outputs []string // The output formats to render this page in. If not set, the site's configured output formats for this page kind will be used.
- // These build options are set in the front matter,
- // but not passed on to .Params.
- Resources []map[string]any
- Cascade map[page.PageMatcher]maps.Params // Only relevant for branch nodes.
- Sitemap config.SitemapConfig
- Build BuildConfig
+ FrontMatterOnlyValues `mapstructure:"-" json:"-"`
+
+ Sitemap config.SitemapConfig
+ Build BuildConfig
+ Menus any // Can be a string, []string or map[string]any.
// User defined params.
Params maps.Params
+ // The raw data from the content adapter.
+ // TODO(bep) clean up the ContentAdapterData vs Params.
+ ContentAdapterData map[string]any `mapstructure:"-" json:"-"`
+
// Compiled values.
- IsGoldmark bool `json:"-"`
+ CascadeCompiled *maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig] `mapstructure:"-" json:"-"`
+ ContentMediaType media.Type `mapstructure:"-" json:"-"`
+ ConfiguredOutputFormats output.Formats `mapstructure:"-" json:"-"`
+ IsFromContentAdapter bool `mapstructure:"-" json:"-"`
+}
+
+func ClonePageConfigForRebuild(p *PageConfig, params map[string]any) *PageConfig {
+ pp := &PageConfig{
+ PageConfigEarly: p.PageConfigEarly,
+ IsFromContentAdapter: p.IsFromContentAdapter,
+ }
+ if pp.IsFromContentAdapter {
+ pp.ContentAdapterData = params
+ } else {
+ pp.Params = params
+ }
+
+ return pp
+}
+
+var DefaultPageConfig = PageConfig{
+ Build: DefaultBuildConfig,
+}
+
+func (p *PageConfig) Validate(pagesFromData bool) error {
+ if pagesFromData {
+ if p.Path == "" {
+ return errors.New("path must be set")
+ }
+ if strings.HasPrefix(p.Path, "/") {
+ return fmt.Errorf("path %q must not start with a /", p.Path)
+ }
+ if p.Lang != "" {
+ return errors.New("lang must not be set")
+ }
+
+ if p.Content.Markup != "" {
+ return errors.New("markup must not be set, use mediaType")
+ }
+ }
+
+ if p.Cascade != nil {
+ if !kinds.IsBranch(p.Kind) {
+ return errors.New("cascade is only supported for branch nodes")
+ }
+ }
+
+ return nil
+}
+
+func (p *PageConfig) CompileForPagesFromDataPre(basePath string, logger loggers.Logger, mediaTypes media.Types) error {
+ // In content adapters, we always get relative paths.
+ if basePath != "" {
+ p.Path = path.Join(basePath, p.Path)
+ }
+
+ if p.Params == nil {
+ p.Params = make(maps.Params)
+ } else {
+ p.Params = maps.PrepareParamsClone(p.Params)
+ }
+
+ if p.Kind == "" {
+ p.Kind = kinds.KindPage
+ }
+
+ if p.Cascade != nil {
+ cascade, err := page.DecodeCascade(logger, false, p.Cascade)
+ if err != nil {
+ return fmt.Errorf("failed to decode cascade: %w", err)
+ }
+ p.CascadeCompiled = cascade
+ }
+
+ // Note that NormalizePathStringBasic will make sure that we don't preserve the unnormalized path.
+ // We do that when we create pages from the file system; mostly for backward compatibility,
+ // but also because people tend to use use the filename to name their resources (with spaces and all),
+ // and this isn't relevant when creating resources from an API where it's easy to add textual meta data.
+ p.Path = paths.NormalizePathStringBasic(p.Path)
+
+ return p.compilePrePost("", mediaTypes)
+}
+
+func (p *PageConfig) compilePrePost(ext string, mediaTypes media.Types) error {
+ if p.Content.Markup == "" && p.Content.MediaType == "" {
+ if ext == "" {
+ ext = "md"
+ }
+ p.ContentMediaType = MarkupToMediaType(ext, mediaTypes)
+ if p.ContentMediaType.IsZero() {
+ return fmt.Errorf("failed to resolve media type for suffix %q", ext)
+ }
+ }
+
+ var s string
+ if p.ContentMediaType.IsZero() {
+ if p.Content.MediaType != "" {
+ s = p.Content.MediaType
+ p.ContentMediaType, _ = mediaTypes.GetByType(s)
+ }
+
+ if p.ContentMediaType.IsZero() && p.Content.Markup != "" {
+ s = p.Content.Markup
+ p.ContentMediaType = MarkupToMediaType(s, mediaTypes)
+ }
+ }
+
+ if p.ContentMediaType.IsZero() {
+ return fmt.Errorf("failed to resolve media type for %q", s)
+ }
+
+ if p.Content.Markup == "" {
+ p.Content.Markup = p.ContentMediaType.SubType
+ }
+ return nil
+}
+
+// Compile sets up the page configuration after all fields have been set.
+func (p *PageConfig) Compile(ext string, logger loggers.Logger, outputFormats output.Formats, mediaTypes media.Types) error {
+ if p.IsFromContentAdapter {
+ if err := mapstructure.WeakDecode(p.ContentAdapterData, p); err != nil {
+ err = fmt.Errorf("failed to decode page map: %w", err)
+ return err
+ }
+ // Not needed anymore.
+ p.ContentAdapterData = nil
+ }
+
+ if p.Params == nil {
+ p.Params = make(maps.Params)
+ } else {
+ maps.PrepareParams(p.Params)
+ }
+
+ if err := p.compilePrePost(ext, mediaTypes); err != nil {
+ return err
+ }
+
+ if len(p.Outputs) > 0 {
+ outFormats, err := outputFormats.GetByNames(p.Outputs...)
+ if err != nil {
+ return fmt.Errorf("failed to resolve output formats %v: %w", p.Outputs, err)
+ } else {
+ p.ConfiguredOutputFormats = outFormats
+ }
+ }
+
+ return nil
+}
+
+// MarkupToMediaType converts a markup string to a media type.
+func MarkupToMediaType(s string, mediaTypes media.Types) media.Type {
+ s = strings.ToLower(s)
+ mt, _ := mediaTypes.GetBestMatch(markup.ResolveMarkup(s))
+ return mt
+}
+
+type ResourceConfig struct {
+ Path string
+ Name string
+ Title string
+ Params maps.Params
+ Content Source
+
+ // Compiled values.
+ PathInfo *paths.Path `mapstructure:"-" json:"-"`
+ ContentMediaType media.Type
+}
+
+func (rc *ResourceConfig) Validate() error {
+ if rc.Path == "" {
+ return errors.New("path must be set")
+ }
+ if rc.Content.Markup != "" {
+ return errors.New("markup must not be set, use mediaType")
+ }
+ return nil
+}
+
+func (rc *ResourceConfig) Compile(basePath string, pathParser *paths.PathParser, mediaTypes media.Types) error {
+ if rc.Params != nil {
+ maps.PrepareParams(rc.Params)
+ }
+
+ // Note that NormalizePathStringBasic will make sure that we don't preserve the unnormalized path.
+ // We do that when we create resources from the file system; mostly for backward compatibility,
+ // but also because people tend to use use the filename to name their resources (with spaces and all),
+ // and this isn't relevant when creating resources from an API where it's easy to add textual meta data.
+ rc.Path = paths.NormalizePathStringBasic(path.Join(basePath, rc.Path))
+ rc.PathInfo = pathParser.Parse(files.ComponentFolderContent, rc.Path)
+ if rc.Content.MediaType != "" {
+ var found bool
+ rc.ContentMediaType, found = mediaTypes.GetByType(rc.Content.MediaType)
+ if !found {
+ return fmt.Errorf("media type %q not found", rc.Content.MediaType)
+ }
+ }
+ return nil
+}
+
+type Source struct {
+ // MediaType is the media type of the content.
+ MediaType string
+
+ // The markup used in Value. Only used in front matter.
+ Markup string
+
+ // The content.
+ Value any
+}
+
+func (s Source) IsZero() bool {
+ return !hreflect.IsTruthful(s.Value)
+}
+
+func (s Source) IsResourceValue() bool {
+ _, ok := s.Value.(resource.Resource)
+ return ok
+}
+
+func (s Source) ValueAsString() string {
+ if s.Value == nil {
+ return ""
+ }
+ ss, err := cast.ToStringE(s.Value)
+ if err != nil {
+ panic(fmt.Errorf("content source: failed to convert %T to string: %s", s.Value, err))
+ }
+ return ss
+}
+
+func (s Source) ValueAsOpenReadSeekCloser() hugio.OpenReadSeekCloser {
+ return hugio.NewOpenReadSeekCloser(hugio.NewReadSeekerNoOpCloserFromString(s.ValueAsString()))
+}
+
+// FrontMatterOnlyValues holds values that can only be set via front matter.
+type FrontMatterOnlyValues struct {
+ ResourcesMeta []map[string]any
}
// FrontMatterHandler maps front matter into Page fields and .Params.
@@ -98,6 +370,8 @@ type PageConfig struct {
type FrontMatterHandler struct {
fmConfig FrontmatterConfig
+ contentAdapterDatesHandler func(d *FrontMatterDescriptor) error
+
dateHandler frontMatterFieldHandler
lastModHandler frontMatterFieldHandler
publishDateHandler frontMatterFieldHandler
@@ -116,6 +390,9 @@ type FrontMatterDescriptor struct {
// if page is a leaf bundle, the bundle folder name (ContentBaseName).
BaseFilename string
+ // The Page's path if the page is backed by a file, else its title.
+ PathOrTitle string
+
// The content file's mod time.
ModTime time.Time
@@ -144,6 +421,13 @@ func (f FrontMatterHandler) HandleDates(d *FrontMatterDescriptor) error {
panic("missing pageConfig")
}
+ if d.PageConfig.IsFromContentAdapter {
+ if f.contentAdapterDatesHandler == nil {
+ panic("missing content adapter date handler")
+ }
+ return f.contentAdapterDatesHandler(d)
+ }
+
if f.dateHandler == nil {
panic("missing date handler")
}
@@ -312,7 +596,7 @@ func expandDefaultValues(values []string, defaults []string) []string {
func toLowerSlice(in any) []string {
out := cast.ToStringSlice(in)
- for i := 0; i < len(out); i++ {
+ for i := range out {
out[i] = strings.ToLower(out[i])
}
@@ -352,9 +636,13 @@ func NewFrontmatterHandler(logger loggers.Logger, frontMatterConfig FrontmatterC
func (f *FrontMatterHandler) createHandlers() error {
var err error
+ if f.contentAdapterDatesHandler, err = f.createContentAdapterDatesHandler(f.fmConfig); err != nil {
+ return err
+ }
+
if f.dateHandler, err = f.createDateHandler(f.fmConfig.Date,
func(d *FrontMatterDescriptor, t time.Time) {
- d.PageConfig.Date = t
+ d.PageConfig.Dates.Date = t
setParamIfNotSet(fmDate, t, d)
}); err != nil {
return err
@@ -363,7 +651,7 @@ func (f *FrontMatterHandler) createHandlers() error {
if f.lastModHandler, err = f.createDateHandler(f.fmConfig.Lastmod,
func(d *FrontMatterDescriptor, t time.Time) {
setParamIfNotSet(fmLastmod, t, d)
- d.PageConfig.Lastmod = t
+ d.PageConfig.Dates.Lastmod = t
}); err != nil {
return err
}
@@ -371,7 +659,7 @@ func (f *FrontMatterHandler) createHandlers() error {
if f.publishDateHandler, err = f.createDateHandler(f.fmConfig.PublishDate,
func(d *FrontMatterDescriptor, t time.Time) {
setParamIfNotSet(fmPubDate, t, d)
- d.PageConfig.PublishDate = t
+ d.PageConfig.Dates.PublishDate = t
}); err != nil {
return err
}
@@ -379,7 +667,7 @@ func (f *FrontMatterHandler) createHandlers() error {
if f.expiryDateHandler, err = f.createDateHandler(f.fmConfig.ExpiryDate,
func(d *FrontMatterDescriptor, t time.Time) {
setParamIfNotSet(fmExpiryDate, t, d)
- d.PageConfig.ExpiryDate = t
+ d.PageConfig.Dates.ExpiryDate = t
}); err != nil {
return err
}
@@ -394,6 +682,86 @@ func setParamIfNotSet(key string, value any, d *FrontMatterDescriptor) {
d.PageConfig.Params[key] = value
}
+func (f FrontMatterHandler) createContentAdapterDatesHandler(fmcfg FrontmatterConfig) (func(d *FrontMatterDescriptor) error, error) {
+ setTime := func(key string, value time.Time, in *PageConfig) {
+ switch key {
+ case fmDate:
+ in.Dates.Date = value
+ case fmLastmod:
+ in.Dates.Lastmod = value
+ case fmPubDate:
+ in.Dates.PublishDate = value
+ case fmExpiryDate:
+ in.Dates.ExpiryDate = value
+ }
+ }
+
+ getTime := func(key string, in *PageConfig) time.Time {
+ switch key {
+ case fmDate:
+ return in.Dates.Date
+ case fmLastmod:
+ return in.Dates.Lastmod
+ case fmPubDate:
+ return in.Dates.PublishDate
+ case fmExpiryDate:
+ return in.Dates.ExpiryDate
+ }
+ return time.Time{}
+ }
+
+ createSetter := func(identifiers []string, date string) func(pcfg *PageConfig) {
+ var getTimes []func(in *PageConfig) time.Time
+ for _, identifier := range identifiers {
+ if strings.HasPrefix(identifier, ":") {
+ continue
+ }
+ switch identifier {
+ case fmDate:
+ getTimes = append(getTimes, func(in *PageConfig) time.Time {
+ return getTime(fmDate, in)
+ })
+ case fmLastmod:
+ getTimes = append(getTimes, func(in *PageConfig) time.Time {
+ return getTime(fmLastmod, in)
+ })
+ case fmPubDate:
+ getTimes = append(getTimes, func(in *PageConfig) time.Time {
+ return getTime(fmPubDate, in)
+ })
+ case fmExpiryDate:
+ getTimes = append(getTimes, func(in *PageConfig) time.Time {
+ return getTime(fmExpiryDate, in)
+ })
+ }
+ }
+
+ return func(pcfg *PageConfig) {
+ for _, get := range getTimes {
+ if t := get(pcfg); !t.IsZero() {
+ setTime(date, t, pcfg)
+ return
+ }
+ }
+ }
+ }
+
+ setDate := createSetter(fmcfg.Date, fmDate)
+ setLastmod := createSetter(fmcfg.Lastmod, fmLastmod)
+ setPublishDate := createSetter(fmcfg.PublishDate, fmPubDate)
+ setExpiryDate := createSetter(fmcfg.ExpiryDate, fmExpiryDate)
+
+ fn := func(d *FrontMatterDescriptor) error {
+ pcfg := d.PageConfig
+ setDate(pcfg)
+ setLastmod(pcfg)
+ setPublishDate(pcfg)
+ setExpiryDate(pcfg)
+ return nil
+ }
+ return fn, nil
+}
+
func (f FrontMatterHandler) createDateHandler(identifiers []string, setter func(d *FrontMatterDescriptor, t time.Time)) (frontMatterFieldHandler, error) {
var h *frontmatterFieldHandlers
var handlers []frontMatterFieldHandler
@@ -420,7 +788,7 @@ func (f *frontmatterFieldHandlers) newDateFieldHandler(key string, setter func(d
return func(d *FrontMatterDescriptor) (bool, error) {
v, found := d.PageConfig.Params[key]
- if !found {
+ if !found || v == "" || v == nil {
return false, nil
}
@@ -431,7 +799,7 @@ func (f *frontmatterFieldHandlers) newDateFieldHandler(key string, setter func(d
var err error
date, err = htime.ToTimeInDefaultLocationE(v, d.Location)
if err != nil {
- return false, nil
+ return false, fmt.Errorf("the %q front matter field is not a parsable date: see %s", key, d.PathOrTitle)
}
d.PageConfig.Params[key] = date
}
diff --git a/resources/page/pagemeta/page_frontmatter_test.go b/resources/page/pagemeta/page_frontmatter_test.go
index 9e1151f22..8d50f9b57 100644
--- a/resources/page/pagemeta/page_frontmatter_test.go
+++ b/resources/page/pagemeta/page_frontmatter_test.go
@@ -18,8 +18,11 @@ import (
"testing"
"time"
+ "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/testconfig"
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources/page/pagemeta"
@@ -29,7 +32,7 @@ import (
func newTestFd() *pagemeta.FrontMatterDescriptor {
return &pagemeta.FrontMatterDescriptor{
PageConfig: &pagemeta.PageConfig{
- Params: make(map[string]interface{}),
+ Params: make(map[string]any),
},
Location: time.UTC,
}
@@ -148,3 +151,32 @@ func TestFrontMatterDatesDefaultKeyword(t *testing.T) {
c.Assert(d.PageConfig.Dates.PublishDate.Day(), qt.Equals, 4)
c.Assert(d.PageConfig.Dates.ExpiryDate.IsZero(), qt.Equals, true)
}
+
+func TestContentMediaTypeFromMarkup(t *testing.T) {
+ c := qt.New(t)
+ logger := loggers.NewDefault()
+
+ for _, test := range []struct {
+ in string
+ expected string
+ }{
+ {"", "text/markdown"},
+ {"md", "text/markdown"},
+ {"markdown", "text/markdown"},
+ {"mdown", "text/markdown"},
+ {"goldmark", "text/markdown"},
+ {"html", "text/html"},
+ {"htm", "text/html"},
+ {"asciidoc", "text/asciidoc"},
+ {"asciidocext", "text/asciidoc"},
+ {"adoc", "text/asciidoc"},
+ {"pandoc", "text/pandoc"},
+ {"pdc", "text/pandoc"},
+ {"rst", "text/rst"},
+ } {
+ var pc pagemeta.PageConfig
+ pc.Content.Markup = test.in
+ c.Assert(pc.Compile("", logger, output.DefaultFormats, media.DefaultTypes), qt.IsNil)
+ c.Assert(pc.ContentMediaType.Type, qt.Equals, test.expected)
+ }
+}
diff --git a/resources/page/pagemeta/pagemeta.go b/resources/page/pagemeta/pagemeta.go
index f5b6380bc..b6b953231 100644
--- a/resources/page/pagemeta/pagemeta.go
+++ b/resources/page/pagemeta/pagemeta.go
@@ -24,7 +24,7 @@ const (
Link = "link"
)
-var defaultBuildConfig = BuildConfig{
+var DefaultBuildConfig = BuildConfig{
List: Always,
Render: Always,
PublishResources: true,
@@ -69,7 +69,7 @@ func (b BuildConfig) IsZero() bool {
}
func DecodeBuildConfig(m any) (BuildConfig, error) {
- b := defaultBuildConfig
+ b := DefaultBuildConfig
if m == nil {
return b, nil
}
diff --git a/resources/page/pagemeta/pagemeta_integration_test.go b/resources/page/pagemeta/pagemeta_integration_test.go
index 4d195b7f0..d0c550b2e 100644
--- a/resources/page/pagemeta/pagemeta_integration_test.go
+++ b/resources/page/pagemeta/pagemeta_integration_test.go
@@ -14,6 +14,7 @@
package pagemeta_test
import (
+ "strings"
"testing"
"github.com/gohugoio/hugo/hugolib"
@@ -43,3 +44,99 @@ Lastmod: 2024-03-13 06:00:00 +0000 GMT
Eq: true
`)
}
+
+func TestDateValidation(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- content/_index.md --
+FRONT_MATTER
+-- layouts/index.html --
+{{ .Date.UTC.Format "2006-01-02" }}
+--
+`
+ errorMsg := `ERROR the "date" front matter field is not a parsable date`
+
+ // TOML: unquoted date/time (valid)
+ f := strings.ReplaceAll(files, "FRONT_MATTER", `
++++
+date = 2024-10-01
++++
+ `)
+ b := hugolib.Test(t, f)
+ b.AssertFileContent("public/index.html", "2024-10-01")
+
+ // TOML: string (valid)
+ f = strings.ReplaceAll(files, "FRONT_MATTER", `
++++
+date = "2024-10-01"
++++
+ `)
+ b = hugolib.Test(t, f)
+ b.AssertFileContent("public/index.html", "2024-10-01")
+
+ // TOML: empty string (valid)
+ f = strings.ReplaceAll(files, "FRONT_MATTER", `
++++
+date = ""
++++
+ `)
+ b = hugolib.Test(t, f)
+ b.AssertFileContent("public/index.html", "0001-01-01")
+
+ // TOML: int (valid)
+ f = strings.ReplaceAll(files, "FRONT_MATTER", `
++++
+date = 0
++++
+ `)
+ b = hugolib.Test(t, f)
+ b.AssertFileContent("public/index.html", "1970-01-01")
+
+ // TOML: string (invalid)
+ f = strings.ReplaceAll(files, "FRONT_MATTER", `
++++
+date = "2024-42-42"
++++
+ `)
+ b, _ = hugolib.TestE(t, f)
+ b.AssertLogContains(errorMsg)
+
+ // TOML: bool (invalid)
+ f = strings.ReplaceAll(files, "FRONT_MATTER", `
++++
+date = true
++++
+ `)
+ b, _ = hugolib.TestE(t, f)
+ b.AssertLogContains(errorMsg)
+
+ // TOML: float (invalid)
+ f = strings.ReplaceAll(files, "FRONT_MATTER", `
++++
+date = 6.7
++++
+ `)
+ b, _ = hugolib.TestE(t, f)
+ b.AssertLogContains(errorMsg)
+
+ // JSON: null (valid)
+ f = strings.ReplaceAll(files, "FRONT_MATTER", `
+{
+ "date": null
+}
+ `)
+ b = hugolib.Test(t, f)
+ b.AssertFileContent("public/index.html", "0001-01-01")
+
+ // YAML: null (valid)
+ f = strings.ReplaceAll(files, "FRONT_MATTER", `
+---
+date:
+---
+ `)
+ b = hugolib.Test(t, f)
+ b.AssertFileContent("public/index.html", "0001-01-01")
+}
diff --git a/resources/page/pagemeta/pagemeta_test.go b/resources/page/pagemeta/pagemeta_test.go
index eef16ef03..f205c74f8 100644
--- a/resources/page/pagemeta/pagemeta_test.go
+++ b/resources/page/pagemeta/pagemeta_test.go
@@ -31,7 +31,7 @@ func TestDecodeBuildConfig(t *testing.T) {
c := qt.New(t)
configTempl := `
-[_build]
+[build]
render = %s
list = %s
publishResources = true`
@@ -82,7 +82,7 @@ publishResources = true`
} {
cfg, err := config.FromConfigString(fmt.Sprintf(configTempl, test.args...), "toml")
c.Assert(err, qt.IsNil)
- bcfg, err := DecodeBuildConfig(cfg.Get("_build"))
+ bcfg, err := DecodeBuildConfig(cfg.Get("build"))
c.Assert(err, qt.IsNil)
eq := qt.CmpEquals(hqt.DeepAllowUnexported(BuildConfig{}))
diff --git a/resources/page/pages_cache.go b/resources/page/pages_cache.go
index 9435cb308..5300d5521 100644
--- a/resources/page/pages_cache.go
+++ b/resources/page/pages_cache.go
@@ -14,6 +14,7 @@
package page
import (
+ "slices"
"sync"
)
@@ -92,7 +93,7 @@ func (c *pageCache) getP(key string, apply func(p *Pages), pageLists ...Pages) (
}
p := pageLists[0]
- pagesCopy := append(Pages(nil), p...)
+ pagesCopy := slices.Clone(p)
if apply != nil {
apply(&pagesCopy)
@@ -126,7 +127,7 @@ func pagesEqual(p1, p2 Pages) bool {
return true
}
- for i := 0; i < len(p1); i++ {
+ for i := range p1 {
if p1[i] != p2[i] {
return false
}
diff --git a/resources/page/pages_cache_test.go b/resources/page/pages_cache_test.go
index 825bdc31f..9e6af1c28 100644
--- a/resources/page/pages_cache_test.go
+++ b/resources/page/pages_cache_test.go
@@ -41,11 +41,11 @@ func TestPageCache(t *testing.T) {
var testPageSets []Pages
- for i := 0; i < 50; i++ {
+ for i := range 50 {
testPageSets = append(testPageSets, createSortTestPages(i+1))
}
- for j := 0; j < 100; j++ {
+ for range 100 {
wg.Add(1)
go func() {
defer wg.Done()
@@ -75,7 +75,7 @@ func TestPageCache(t *testing.T) {
func BenchmarkPageCache(b *testing.B) {
cache := newPageCache()
pages := make(Pages, 30)
- for i := 0; i < 30; i++ {
+ for i := range 30 {
pages[i] = &testPage{title: "p" + strconv.Itoa(i)}
}
key := "key"
diff --git a/resources/page/pages_prev_next_integration_test.go b/resources/page/pages_prev_next_integration_test.go
new file mode 100644
index 000000000..d61d23cf0
--- /dev/null
+++ b/resources/page/pages_prev_next_integration_test.go
@@ -0,0 +1,82 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package page_test
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/gohugoio/hugo/hugolib"
+)
+
+func TestNextPrevConfig(t *testing.T) {
+ filesTemplate := `
+-- hugo.toml --
+-- content/mysection/_index.md --
+-- content/mysection/p1.md --
+---
+title: "Page 1"
+weight: 10
+---
+-- content/mysection/p2.md --
+---
+title: "Page 2"
+weight: 20
+---
+-- content/mysection/p3.md --
+---
+title: "Page 3"
+weight: 30
+---
+-- layouts/_default/single.html --
+{{ .Title }}|Next: {{ with .Next}}{{ .Title}}{{ end }}|Prev: {{ with .Prev}}{{ .Title}}{{ end }}|NextInSection: {{ with .NextInSection}}{{ .Title}}{{ end }}|PrevInSection: {{ with .PrevInSection}}{{ .Title}}{{ end }}|
+
+`
+ b := hugolib.Test(t, filesTemplate)
+
+ b.AssertFileContent("public/mysection/p1/index.html", "Page 1|Next: |Prev: Page 2|NextInSection: |PrevInSection: Page 2|")
+ b.AssertFileContent("public/mysection/p2/index.html", "Page 2|Next: Page 1|Prev: Page 3|NextInSection: Page 1|PrevInSection: Page 3|")
+ b.AssertFileContent("public/mysection/p3/index.html", "Page 3|Next: Page 2|Prev: |NextInSection: Page 2|PrevInSection: |")
+
+ files := strings.ReplaceAll(filesTemplate, "-- hugo.toml --", `-- hugo.toml --
+[page]
+nextPrevSortOrder="aSc"
+nextPrevInSectionSortOrder="asC"
+`)
+
+ b = hugolib.Test(t, files)
+
+ b.AssertFileContent("public/mysection/p1/index.html", "Page 1|Next: Page 2|Prev: |NextInSection: Page 2|PrevInSection: |")
+ b.AssertFileContent("public/mysection/p2/index.html", "Page 2|Next: Page 3|Prev: Page 1|NextInSection: Page 3|PrevInSection: Page 1|")
+ b.AssertFileContent("public/mysection/p3/index.html", "Page 3|Next: |Prev: Page 2|NextInSection: |PrevInSection: Page 2|")
+
+ files = strings.ReplaceAll(filesTemplate, "-- hugo.toml --", `-- hugo.toml --
+[page]
+nextPrevSortOrder="aSc"
+`)
+
+ b = hugolib.Test(t, files)
+
+ b.AssertFileContent("public/mysection/p1/index.html", "Page 1|Next: Page 2|Prev: |NextInSection: |PrevInSection: Page 2|")
+ b.AssertFileContent("public/mysection/p2/index.html", "Page 2|Next: Page 3|Prev: Page 1|NextInSection: Page 1|PrevInSection: Page 3|")
+ b.AssertFileContent("public/mysection/p3/index.html", "Page 3|Next: |Prev: Page 2|NextInSection: Page 2|PrevInSection: |")
+
+ files = strings.ReplaceAll(filesTemplate, "-- hugo.toml --", `-- hugo.toml --
+[page]
+nextPrevInSectionSortOrder="aSc"
+`)
+
+ b = hugolib.Test(t, files)
+
+ b.AssertFileContent("public/mysection/p1/index.html", "Page 1|Next: |Prev: Page 2|NextInSection: Page 2|PrevInSection: |")
+}
diff --git a/resources/page/pages_related.go b/resources/page/pages_related.go
index 3322a4fbf..402ed905f 100644
--- a/resources/page/pages_related.go
+++ b/resources/page/pages_related.go
@@ -124,12 +124,12 @@ func (p Pages) withInvertedIndex(ctx context.Context, search func(idx *related.I
return nil, nil
}
- d, ok := p[0].(InternalDependencies)
+ d, ok := p[0].(RelatedDocsHandlerProvider)
if !ok {
return nil, fmt.Errorf("invalid type %T in related search", p[0])
}
- cache := d.GetRelatedDocsHandler()
+ cache := d.GetInternalRelatedDocsHandler()
searchIndex, err := cache.getOrCreateIndex(ctx, p)
if err != nil {
diff --git a/resources/page/pages_sort.go b/resources/page/pages_sort.go
index 3f4875702..e77bb7e7c 100644
--- a/resources/page/pages_sort.go
+++ b/resources/page/pages_sort.go
@@ -90,14 +90,14 @@ var (
if w01 != w02 && w01 != -1 && w02 != -1 {
return w01 < w02
}
+
if p1.Weight() == p2.Weight() {
if p1.Date().Unix() == p2.Date().Unix() {
c := collatorStringCompare(func(p Page) string { return p.LinkTitle() }, p1, p2)
if c == 0 {
- if p1.File() == nil || p2.File() == nil {
- return p1.File() == nil
- }
- return compare.LessStrings(p1.File().Filename(), p2.File().Filename())
+ // This is the full normalized path, which will contain extension and any language code preserved,
+ // which is what we want for sorting.
+ return compare.LessStrings(p1.PathInfo().Path(), p2.PathInfo().Path())
}
return c < 0
}
diff --git a/resources/page/pages_sort_test.go b/resources/page/pages_sort_test.go
index 12fa4a1e1..70c7bc8a8 100644
--- a/resources/page/pages_sort_test.go
+++ b/resources/page/pages_sort_test.go
@@ -139,7 +139,7 @@ func TestLimit(t *testing.T) {
p := createSortTestPages(10)
firstFive := p.Limit(5)
c.Assert(len(firstFive), qt.Equals, 5)
- for i := 0; i < 5; i++ {
+ for i := range 5 {
c.Assert(firstFive[i], qt.Equals, p[i])
}
c.Assert(p.Limit(10), eq, p)
@@ -197,7 +197,7 @@ func TestPageSortByParamNumeric(t *testing.T) {
n := 10
unsorted := createSortTestPages(n)
- for i := 0; i < n; i++ {
+ for i := range n {
v := 100 - i
if i%2 == 0 {
v = 100.0 - i
@@ -269,7 +269,7 @@ func setSortVals(dates [4]time.Time, titles [4]string, weights [4]int, pages Pag
func createSortTestPages(num int) Pages {
pages := make(Pages, num)
- for i := 0; i < num; i++ {
+ for i := range num {
p := newTestPage()
p.path = fmt.Sprintf("/x/y/p%d.md", i)
p.title = fmt.Sprintf("Title %d", i%((num+1)/2))
diff --git a/resources/page/pagination.go b/resources/page/pagination.go
index 4beb96e50..ea49d62f6 100644
--- a/resources/page/pagination.go
+++ b/resources/page/pagination.go
@@ -19,6 +19,7 @@ import (
"math"
"reflect"
+ "github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/config"
"github.com/spf13/cast"
@@ -194,7 +195,14 @@ func (p *Paginator) Pagers() pagers {
}
// PageSize returns the size of each paginator page.
+// Deprecated: Use PagerSize instead.
func (p *Paginator) PageSize() int {
+ hugo.Deprecate("PageSize", "Use PagerSize instead.", "v0.128.0")
+ return p.size
+}
+
+// PagerSize returns the size of each paginator page.
+func (p *Paginator) PagerSize() int {
return p.size
}
@@ -263,7 +271,7 @@ func splitPageGroups(pageGroups PagesGroup, size int) []paginatedElement {
func ResolvePagerSize(conf config.AllProvider, options ...any) (int, error) {
if len(options) == 0 {
- return conf.Paginate(), nil
+ return conf.Pagination().PagerSize, nil
}
if len(options) > 1 {
@@ -400,7 +408,7 @@ func newPaginationURLFactory(d TargetPathDescriptor) paginationURLFactory {
pathDescriptor := d
var rel string
if pageNumber > 1 {
- rel = fmt.Sprintf("/%s/%d/", d.PathSpec.Cfg.PaginatePath(), pageNumber)
+ rel = fmt.Sprintf("/%s/%d/", d.PathSpec.Cfg.Pagination().Path, pageNumber)
pathDescriptor.Addends = rel
}
diff --git a/resources/page/pagination_test.go b/resources/page/pagination_test.go
index 487b36adb..64ee9a998 100644
--- a/resources/page/pagination_test.go
+++ b/resources/page/pagination_test.go
@@ -28,7 +28,7 @@ func TestSplitPages(t *testing.T) {
chunks := splitPages(pages, 5)
c.Assert(len(chunks), qt.Equals, 5)
- for i := 0; i < 4; i++ {
+ for i := range 4 {
c.Assert(chunks[i].Len(), qt.Equals, 5)
}
@@ -114,7 +114,7 @@ func doTestPages(t *testing.T, paginator *Paginator) {
c.Assert(len(paginatorPages), qt.Equals, 5)
c.Assert(paginator.TotalNumberOfElements(), qt.Equals, 21)
- c.Assert(paginator.PageSize(), qt.Equals, 5)
+ c.Assert(paginator.PagerSize(), qt.Equals, 5)
c.Assert(paginator.TotalPages(), qt.Equals, 5)
first := paginatorPages[0]
@@ -172,7 +172,7 @@ func doTestPagerNoPages(t *testing.T, paginator *Paginator) {
c := qt.New(t)
c.Assert(len(paginatorPages), qt.Equals, 1)
c.Assert(paginator.TotalNumberOfElements(), qt.Equals, 0)
- c.Assert(paginator.PageSize(), qt.Equals, 5)
+ c.Assert(paginator.PagerSize(), qt.Equals, 5)
c.Assert(paginator.TotalPages(), qt.Equals, 0)
// pageOne should be nothing but the first
@@ -187,7 +187,7 @@ func doTestPagerNoPages(t *testing.T, paginator *Paginator) {
c.Assert(pageOne.TotalNumberOfElements(), qt.Equals, 0)
c.Assert(pageOne.TotalPages(), qt.Equals, 0)
c.Assert(pageOne.PageNumber(), qt.Equals, 1)
- c.Assert(pageOne.PageSize(), qt.Equals, 5)
+ c.Assert(pageOne.PagerSize(), qt.Equals, 5)
}
func TestProbablyEqualPageLists(t *testing.T) {
diff --git a/resources/page/permalinks.go b/resources/page/permalinks.go
index 67c63c4b2..f8cbcd62c 100644
--- a/resources/page/permalinks.go
+++ b/resources/page/permalinks.go
@@ -40,6 +40,8 @@ type PermalinkExpander struct {
expanders map[string]map[string]func(Page) (string, error)
urlize func(uri string) string
+
+ patternCache *maps.Cache[string, func(Page) (string, error)]
}
// Time for checking date formats. Every field is different than the
@@ -71,22 +73,27 @@ func (p PermalinkExpander) callback(attr string) (pageToPermaAttribute, bool) {
// NewPermalinkExpander creates a new PermalinkExpander configured by the given
// urlize func.
func NewPermalinkExpander(urlize func(uri string) string, patterns map[string]map[string]string) (PermalinkExpander, error) {
- p := PermalinkExpander{urlize: urlize}
+ p := PermalinkExpander{
+ urlize: urlize,
+ patternCache: maps.NewCache[string, func(Page) (string, error)](),
+ }
p.knownPermalinkAttributes = map[string]pageToPermaAttribute{
- "year": p.pageToPermalinkDate,
- "month": p.pageToPermalinkDate,
- "monthname": p.pageToPermalinkDate,
- "day": p.pageToPermalinkDate,
- "weekday": p.pageToPermalinkDate,
- "weekdayname": p.pageToPermalinkDate,
- "yearday": p.pageToPermalinkDate,
- "section": p.pageToPermalinkSection,
- "sections": p.pageToPermalinkSections,
- "title": p.pageToPermalinkTitle,
- "slug": p.pageToPermalinkSlugElseTitle,
- "slugorfilename": p.pageToPermalinkSlugElseFilename,
- "filename": p.pageToPermalinkFilename,
+ "year": p.pageToPermalinkDate,
+ "month": p.pageToPermalinkDate,
+ "monthname": p.pageToPermalinkDate,
+ "day": p.pageToPermalinkDate,
+ "weekday": p.pageToPermalinkDate,
+ "weekdayname": p.pageToPermalinkDate,
+ "yearday": p.pageToPermalinkDate,
+ "section": p.pageToPermalinkSection,
+ "sections": p.pageToPermalinkSections,
+ "title": p.pageToPermalinkTitle,
+ "slug": p.pageToPermalinkSlugElseTitle,
+ "slugorfilename": p.pageToPermalinkSlugElseFilename,
+ "filename": p.pageToPermalinkFilename,
+ "contentbasename": p.pageToPermalinkContentBaseName,
+ "slugorcontentbasename": p.pageToPermalinkSlugOrContentBaseName,
}
p.expanders = make(map[string]map[string]func(Page) (string, error))
@@ -102,17 +109,37 @@ func NewPermalinkExpander(urlize func(uri string) string, patterns map[string]ma
return p, nil
}
+// Escape sequence for colons in permalink patterns.
+const escapePlaceholderColon = "\x00"
+
+func (l PermalinkExpander) normalizeEscapeSequencesIn(s string) (string, bool) {
+ s2 := strings.ReplaceAll(s, "\\:", escapePlaceholderColon)
+ return s2, s2 != s
+}
+
+func (l PermalinkExpander) normalizeEscapeSequencesOut(result string) string {
+ return strings.ReplaceAll(result, escapePlaceholderColon, ":")
+}
+
+// ExpandPattern expands the path in p with the specified expand pattern.
+func (l PermalinkExpander) ExpandPattern(pattern string, p Page) (string, error) {
+ expand, err := l.getOrParsePattern(pattern)
+ if err != nil {
+ return "", err
+ }
+
+ return expand(p)
+}
+
// Expand expands the path in p according to the rules defined for the given key.
// If no rules are found for the given key, an empty string is returned.
func (l PermalinkExpander) Expand(key string, p Page) (string, error) {
expanders, found := l.expanders[p.Kind()]
-
if !found {
return "", nil
}
expand, found := expanders[key]
-
if !found {
return "", nil
}
@@ -129,18 +156,21 @@ func init() {
}
}
-func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Page) (string, error), error) {
- expanders := make(map[string]func(Page) (string, error))
+func (l PermalinkExpander) getOrParsePattern(pattern string) (func(Page) (string, error), error) {
+ return l.patternCache.GetOrCreate(pattern, func() (func(Page) (string, error), error) {
+ var normalized bool
+ pattern, normalized = l.normalizeEscapeSequencesIn(pattern)
- for k, pattern := range patterns {
- k = strings.Trim(k, sectionCutSet)
-
- if !l.validate(pattern) {
- return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkIllFormed}
- }
-
- pattern := pattern
matches := attributeRegexp.FindAllStringSubmatch(pattern, -1)
+ if matches == nil {
+ result := pattern
+ if normalized {
+ result = l.normalizeEscapeSequencesOut(result)
+ }
+ return func(p Page) (string, error) {
+ return result, nil
+ }, nil
+ }
callbacks := make([]pageToPermaAttribute, len(matches))
replacements := make([]string, len(matches))
@@ -157,11 +187,7 @@ func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Pa
callbacks[i] = callback
}
- expanders[k] = func(p Page) (string, error) {
- if matches == nil {
- return pattern, nil
- }
-
+ return func(p Page) (string, error) {
newField := pattern
for i, replacement := range replacements {
@@ -173,12 +199,29 @@ func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Pa
}
newField = strings.Replace(newField, replacement, newAttr, 1)
+ }
+ if normalized {
+ newField = l.normalizeEscapeSequencesOut(newField)
}
return newField, nil
+ }, nil
+ })
+}
+
+func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Page) (string, error), error) {
+ expanders := make(map[string]func(Page) (string, error))
+
+ for k, pattern := range patterns {
+ k = strings.Trim(k, sectionCutSet)
+
+ expander, err := l.getOrParsePattern(pattern)
+ if err != nil {
+ return nil, err
}
+ expanders[k] = expander
}
return expanders, nil
@@ -190,37 +233,6 @@ type pageToPermaAttribute func(Page, string) (string, error)
var attributeRegexp = regexp.MustCompile(`:\w+(\[.+?\])?`)
-// validate determines if a PathPattern is well-formed
-func (l PermalinkExpander) validate(pp string) bool {
- if len(pp) == 0 {
- return false
- }
- fragments := strings.Split(pp[1:], "/")
- bail := false
- for i := range fragments {
- if bail {
- return false
- }
- if len(fragments[i]) == 0 {
- bail = true
- continue
- }
-
- matches := attributeRegexp.FindAllStringSubmatch(fragments[i], -1)
- if matches == nil {
- continue
- }
-
- for _, match := range matches {
- k := match[0][1:]
- if _, ok := l.callback(k); !ok {
- return false
- }
- }
- }
- return true
-}
-
type permalinkExpandError struct {
pattern string
err error
@@ -230,10 +242,7 @@ func (pee *permalinkExpandError) Error() string {
return fmt.Sprintf("error expanding %q: %s", pee.pattern, pee.err)
}
-var (
- errPermalinkIllFormed = errors.New("permalink ill-formed")
- errPermalinkAttributeUnknown = errors.New("permalink attribute not recognised")
-)
+var errPermalinkAttributeUnknown = errors.New("permalink attribute not recognised")
func (l PermalinkExpander) pageToPermalinkDate(p Page, dateField string) (string, error) {
// a Page contains a Node which provides a field Date, time.Time
@@ -300,6 +309,23 @@ func (l PermalinkExpander) pageToPermalinkSections(p Page, _ string) (string, er
return p.CurrentSection().SectionsPath(), nil
}
+// pageToPermalinkContentBaseName returns the URL-safe form of the content base name.
+func (l PermalinkExpander) pageToPermalinkContentBaseName(p Page, _ string) (string, error) {
+ return l.urlize(p.PathInfo().Unnormalized().BaseNameNoIdentifier()), nil
+}
+
+// pageToPermalinkSlugOrContentBaseName returns the URL-safe form of the slug, content base name.
+func (l PermalinkExpander) pageToPermalinkSlugOrContentBaseName(p Page, a string) (string, error) {
+ if p.Slug() != "" {
+ return l.urlize(p.Slug()), nil
+ }
+ name, err := l.pageToPermalinkContentBaseName(p, a)
+ if err != nil {
+ return "", nil
+ }
+ return name, nil
+}
+
func (l PermalinkExpander) translationBaseName(p Page) string {
if p.File() == nil {
return ""
diff --git a/resources/page/permalinks_integration_test.go b/resources/page/permalinks_integration_test.go
index 9a76ac602..c865e2704 100644
--- a/resources/page/permalinks_integration_test.go
+++ b/resources/page/permalinks_integration_test.go
@@ -14,10 +14,12 @@
package page_test
import (
+ "strings"
"testing"
"github.com/bep/logg"
qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/hugolib"
)
@@ -193,3 +195,178 @@ List.
b.AssertFileContent("public/libros/fiction/index.html", "List.")
b.AssertFileContent("public/libros/fiction/2023/book1/index.html", "Single.")
}
+
+func TestPermalinksUrlCascade(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- layouts/_default/list.html --
+List|{{ .Kind }}|{{ .RelPermalink }}|
+-- layouts/_default/single.html --
+Single|{{ .Kind }}|{{ .RelPermalink }}|
+-- hugo.toml --
+-- content/cooking/delicious-recipes/_index.md --
+---
+url: /delicious-recipe/
+cascade:
+ url: /delicious-recipe/:slug/
+---
+-- content/cooking/delicious-recipes/example1.md --
+---
+title: Recipe 1
+---
+-- content/cooking/delicious-recipes/example2.md --
+---
+title: Recipe 2
+slug: custom-recipe-2
+---
+`
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ TxtarString: files,
+ LogLevel: logg.LevelWarn,
+ }).Build()
+
+ t.Log(b.LogString())
+ b.Assert(b.H.Log.LoggCount(logg.LevelWarn), qt.Equals, 0)
+ b.AssertFileContent("public/delicious-recipe/index.html", "List|section|/delicious-recipe/")
+ b.AssertFileContent("public/delicious-recipe/recipe-1/index.html", "Single|page|/delicious-recipe/recipe-1/")
+ b.AssertFileContent("public/delicious-recipe/custom-recipe-2/index.html", "Single|page|/delicious-recipe/custom-recipe-2/")
+}
+
+// Issue 12948.
+// Issue 12954.
+func TestPermalinksWithEscapedColons(t *testing.T) {
+ t.Parallel()
+
+ if htesting.IsWindows() {
+ t.Skip("Windows does not support colons in paths")
+ }
+
+ files := `
+-- hugo.toml --
+disableKinds = ['home','rss','sitemap','taxonomy','term']
+[permalinks.page]
+s2 = "/c\\:d/:slug/"
+-- content/s1/_index.md --
+---
+title: s1
+url: "/a\\:b/:slug/"
+---
+-- content/s1/p1.md --
+---
+title: p1
+url: "/a\\:b/:slug/"
+---
+-- content/s2/p2.md --
+---
+title: p2
+---
+-- layouts/_default/single.html --
+{{ .Title }}
+-- layouts/_default/list.html --
+{{ .Title }}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileExists("public/a:b/p1/index.html", true)
+ b.AssertFileExists("public/a:b/s1/index.html", true)
+
+ // The above URLs come from the URL front matter field where everything is allowed.
+ // We strip colons from paths constructed by Hugo (they are not supported on Windows).
+ b.AssertFileExists("public/cd/p2/index.html", true)
+}
+
+func TestPermalinksContentbasenameContentAdapter(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+[permalinks]
+[permalinks.page]
+a = "/:slugorcontentbasename/"
+b = "/:sections/:contentbasename/"
+-- content/_content.gotmpl --
+{{ $.AddPage (dict "kind" "page" "path" "a/b/contentbasename1" "title" "My A Page No Slug") }}
+{{ $.AddPage (dict "kind" "page" "path" "a/b/contentbasename2" "slug" "myslug" "title" "My A Page With Slug") }}
+ {{ $.AddPage (dict "kind" "section" "path" "b/c" "title" "My B Section") }}
+{{ $.AddPage (dict "kind" "page" "path" "b/c/contentbasename3" "title" "My B Page No Slug") }}
+-- layouts/_default/single.html --
+{{ .Title }}|{{ .RelPermalink }}|{{ .Kind }}|
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/contentbasename1/index.html", "My A Page No Slug|/contentbasename1/|page|")
+ b.AssertFileContent("public/myslug/index.html", "My A Page With Slug|/myslug/|page|")
+}
+
+func TestPermalinksContentbasenameWithAndWithoutFile(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+[permalinks.section]
+a = "/mya/:contentbasename/"
+[permalinks.page]
+a = "/myapage/:contentbasename/"
+[permalinks.term]
+categories = "/myc/:slugorcontentbasename/"
+-- content/b/c/_index.md --
+---
+title: "C section"
+---
+-- content/a/b/index.md --
+---
+title: "My Title"
+categories: ["c1", "c2"]
+---
+-- content/categories/c2/_index.md --
+---
+title: "C2"
+slug: "c2slug"
+---
+-- layouts/_default/single.html --
+{{ .Title }}|{{ .RelPermalink }}|{{ .Kind }}|
+-- layouts/_default/list.html --
+{{ .Title }}|{{ .RelPermalink }}|{{ .Kind }}|
+`
+ b := hugolib.Test(t, files)
+
+ // Sections.
+ b.AssertFileContent("public/mya/a/index.html", "As|/mya/a/|section|")
+
+ // Pages.
+ b.AssertFileContent("public/myapage/b/index.html", "My Title|/myapage/b/|page|")
+
+ // Taxonomies.
+ b.AssertFileContent("public/myc/c1/index.html", "C1|/myc/c1/|term|")
+ b.AssertFileContent("public/myc/c2slug/index.html", "C2|/myc/c2slug/|term|")
+}
+
+func TestIssue13755(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['home','rss','section','sitemap','taxonomy','term']
+disablePathToLower = false
+[permalinks.page]
+s1 = "/:contentbasename"
+-- content/s1/aBc.md --
+---
+title: aBc
+---
+-- layouts/all.html --
+{{ .Title }}
+`
+
+ b := hugolib.Test(t, files)
+ b.AssertFileExists("public/abc/index.html", true)
+
+ files = strings.ReplaceAll(files, "disablePathToLower = false", "disablePathToLower = true")
+
+ b = hugolib.Test(t, files)
+ b.AssertFileExists("public/aBc/index.html", true)
+}
diff --git a/resources/page/permalinks_test.go b/resources/page/permalinks_test.go
index a3a45bb88..191259252 100644
--- a/resources/page/permalinks_test.go
+++ b/resources/page/permalinks_test.go
@@ -22,6 +22,7 @@ import (
"time"
qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/source"
)
// testdataPermalinks is used by a couple of tests; the expandsTo content is
@@ -29,28 +30,44 @@ import (
var testdataPermalinks = []struct {
spec string
valid bool
+ withPage func(p *testPage)
expandsTo string
}{
- {":title", true, "spf13-vim-3.0-release-and-new-website"},
- {"/:year-:month-:title", true, "/2012-04-spf13-vim-3.0-release-and-new-website"},
- {"/:year/:yearday/:month/:monthname/:day/:weekday/:weekdayname/", true, "/2012/97/04/April/06/5/Friday/"}, // Dates
- {"/:section/", true, "/blue/"}, // Section
- {"/:title/", true, "/spf13-vim-3.0-release-and-new-website/"}, // Title
- {"/:slug/", true, "/the-slug/"}, // Slug
- {"/:slugorfilename/", true, "/the-slug/"}, // Slug or filename
- {"/:filename/", true, "/test-page/"}, // Filename
- {"/:06-:1-:2-:Monday", true, "/12-4-6-Friday"}, // Dates with Go formatting
- {"/:2006_01_02_15_04_05.000", true, "/2012_04_06_03_01_59.000"}, // Complicated custom date format
- {"/:sections/", true, "/a/b/c/"}, // Sections
- {"/:sections[last]/", true, "/c/"}, // Sections
- {"/:sections[0]/:sections[last]/", true, "/a/c/"}, // Sections
-
+ {":title", true, nil, "spf13-vim-3.0-release-and-new-website"},
+ {"/:year-:month-:title", true, nil, "/2012-04-spf13-vim-3.0-release-and-new-website"},
+ {"/:year/:yearday/:month/:monthname/:day/:weekday/:weekdayname/", true, nil, "/2012/97/04/April/06/5/Friday/"}, // Dates
+ {"/:section/", true, nil, "/blue/"}, // Section
+ {"/:title/", true, nil, "/spf13-vim-3.0-release-and-new-website/"}, // Title
+ {"/:slug/", true, nil, "/the-slug/"}, // Slug
+ {"/:slugorfilename/", true, nil, "/the-slug/"}, // Slug or filename
+ {"/:filename/", true, nil, "/test-page/"}, // Filename
+ {"/:06-:1-:2-:Monday", true, nil, "/12-4-6-Friday"}, // Dates with Go formatting
+ {"/:2006_01_02_15_04_05.000", true, nil, "/2012_04_06_03_01_59.000"}, // Complicated custom date format
+ {"/:sections/", true, nil, "/a/b/c/"}, // Sections
+ {"/:sections[last]/", true, nil, "/c/"}, // Sections
+ {"/:sections[0]/:sections[last]/", true, nil, "/a/c/"}, // Sections
+ {"/\\:filename", true, nil, "/:filename"}, // Escape sequence
+ {"/special\\::slug/", true, nil, "/special:the-slug/"},
+ // contentbasename. // Escape sequence
+ {"/:contentbasename/", true, nil, "/test-page/"},
+ // slug, contentbasename. // Content base name
+ {"/:slugorcontentbasename/", true, func(p *testPage) {
+ p.slug = ""
+ }, "/test-page/"},
+ {"/:slugorcontentbasename/", true, func(p *testPage) {
+ p.slug = "myslug"
+ }, "/myslug/"},
+ {"/:slugorcontentbasename/", true, func(p *testPage) {
+ p.slug = ""
+ p.title = "mytitle"
+ p.file = source.NewContentFileInfoFrom("/", "_index.md")
+ }, "/test-page/"},
// Failures
- {"/blog/:fred", false, ""},
- {"/:year//:title", false, ""},
- {"/:TITLE", false, ""}, // case is not normalized
- {"/:2017", false, ""}, // invalid date format
- {"/:2006-01-02", false, ""}, // valid date format but invalid attribute name
+ {"/blog/:fred", false, nil, ""},
+ {"/:year//:title", false, nil, ""},
+ {"/:TITLE", false, nil, ""}, // case is not normalized
+ {"/:2017", false, nil, ""}, // invalid date format
+ {"/:2006-01-02", false, nil, ""}, // valid date format but invalid attribute name
}
func urlize(uri string) string {
@@ -63,21 +80,30 @@ func TestPermalinkExpansion(t *testing.T) {
c := qt.New(t)
- page := newTestPageWithFile("/test-page/index.md")
- page.title = "Spf13 Vim 3.0 Release and new website"
- d, _ := time.Parse("2006-01-02 15:04:05", "2012-04-06 03:01:59")
- page.date = d
- page.section = "blue"
- page.slug = "The Slug"
- page.kind = "page"
+ newPage := func() *testPage {
+ page := newTestPageWithFile("/test-page/index.md")
+ page.title = "Spf13 Vim 3.0 Release and new website"
+ d, _ := time.Parse("2006-01-02 15:04:05", "2012-04-06 03:01:59")
+ page.date = d
+ page.section = "blue"
+ page.slug = "The Slug"
+ page.kind = "page"
+ // page.pathInfo
+ return page
+ }
- for _, item := range testdataPermalinks {
+ for i, item := range testdataPermalinks {
if !item.valid {
continue
}
+ page := newPage()
+ if item.withPage != nil {
+ item.withPage(page)
+ }
+
specNameCleaner := regexp.MustCompile(`[\:\/\[\]]`)
- name := specNameCleaner.ReplaceAllString(item.spec, "")
+ name := fmt.Sprintf("[%d] %s", i, specNameCleaner.ReplaceAllString(item.spec, "_"))
c.Run(name, func(c *qt.C) {
patterns := map[string]map[string]string{
@@ -90,6 +116,10 @@ func TestPermalinkExpansion(t *testing.T) {
expanded, err := expander.Expand("posts", page)
c.Assert(err, qt.IsNil)
c.Assert(expanded, qt.Equals, item.expandsTo)
+
+ expanded, err = expander.ExpandPattern(item.spec, page)
+ c.Assert(err, qt.IsNil)
+ c.Assert(expanded, qt.Equals, item.expandsTo)
})
}
@@ -117,6 +147,7 @@ func TestPermalinkExpansionMultiSection(t *testing.T) {
"posts": "/:slug",
"blog": "/:section/:year",
"recipes": "/:slugorfilename",
+ "special": "/special\\::slug",
},
}
expander, err := NewPermalinkExpander(urlize, permalinksConfig)
@@ -137,6 +168,10 @@ func TestPermalinkExpansionMultiSection(t *testing.T) {
expanded, err = expander.Expand("recipes", page_slug_fallback)
c.Assert(err, qt.IsNil)
c.Assert(expanded, qt.Equals, "/page-filename")
+
+ expanded, err = expander.Expand("special", page)
+ c.Assert(err, qt.IsNil)
+ c.Assert(expanded, qt.Equals, "/special:the-slug")
}
func TestPermalinkExpansionConcurrent(t *testing.T) {
diff --git a/resources/page/site.go b/resources/page/site.go
index df33485eb..3c9e9e78c 100644
--- a/resources/page/site.go
+++ b/resources/page/site.go
@@ -14,7 +14,6 @@
package page
import (
- "html/template"
"time"
"github.com/gohugoio/hugo/common/maps"
@@ -54,9 +53,6 @@ type Site interface {
// A shortcut to the home
Home() Page
- // Deprecated: Use hugo.IsServer instead.
- IsServer() bool
-
// Returns the server port.
ServerPort() int
@@ -109,7 +105,7 @@ type Site interface {
Config() SiteConfig
// Deprecated: Use taxonomies instead.
- Author() map[string]interface{}
+ Author() map[string]any
// Deprecated: Use taxonomies instead.
Authors() AuthorList
@@ -117,12 +113,6 @@ type Site interface {
// Deprecated: Use .Site.Params instead.
Social() map[string]string
- // Deprecated: Use Config().Services.GoogleAnalytics instead.
- GoogleAnalytics() string
-
- // Deprecated: Use Config().Privacy.Disqus instead.
- DisqusShortname() string
-
// BuildDrafts is deprecated and will be removed in a future release.
BuildDrafts() bool
@@ -132,15 +122,27 @@ type Site interface {
// LanguagePrefix returns the language prefix for this site.
LanguagePrefix() string
- // Deprecated: Use .Site.Home.OutputFormats.Get "rss" instead.
- RSSLink() template.URL
+ maps.StoreProvider
+
+ // For internal use only.
+ // This will panic if the site is not fully initialized.
+ // This is typically used to inform the user in the content adapter templates,
+ // as these are executed before all the page collections etc. are ready to use.
+ CheckReady()
}
// Sites represents an ordered list of sites (languages).
type Sites []Site
-// First is a convenience method to get the first Site, i.e. the main language.
+// Deprecated: Use .Sites.Default instead.
func (s Sites) First() Site {
+ hugo.Deprecate(".Sites.First", "Use .Sites.Default instead.", "v0.127.0")
+ return s.Default()
+}
+
+// Default is a convenience method to get the site corresponding to the default
+// content language.
+func (s Sites) Default() Site {
if len(s) == 0 {
return nil
}
@@ -165,13 +167,13 @@ func (s *siteWrapper) Key() string {
return s.s.Language().Lang
}
-// // Deprecated: Use .Site.Params instead.
+// Deprecated: Use .Site.Params instead.
func (s *siteWrapper) Social() map[string]string {
return s.s.Social()
}
// Deprecated: Use taxonomies instead.
-func (s *siteWrapper) Author() map[string]interface{} {
+func (s *siteWrapper) Author() map[string]any {
return s.s.Author()
}
@@ -180,11 +182,6 @@ func (s *siteWrapper) Authors() AuthorList {
return s.s.Authors()
}
-// Deprecated: Use .Site.Config.Services.GoogleAnalytics.ID instead.
-func (s *siteWrapper) GoogleAnalytics() string {
- return s.s.GoogleAnalytics()
-}
-
func (s *siteWrapper) GetPage(ref ...string) (Page, error) {
return s.s.GetPage(ref...)
}
@@ -217,11 +214,6 @@ func (s *siteWrapper) Home() Page {
return s.s.Home()
}
-// Deprecated: Use hugo.IsServer instead.
-func (s *siteWrapper) IsServer() bool {
- return s.s.IsServer()
-}
-
func (s *siteWrapper) ServerPort() int {
return s.s.ServerPort()
}
@@ -300,18 +292,12 @@ func (s *siteWrapper) IsMultiLingual() bool {
return s.s.IsMultiLingual()
}
-// Deprecated: Use .Site.Config.Services.Disqus.Shortname instead.
-func (s *siteWrapper) DisqusShortname() string {
- return s.s.DisqusShortname()
-}
-
func (s *siteWrapper) LanguagePrefix() string {
return s.s.LanguagePrefix()
}
-// Deprecated: Use .Site.Home.OutputFormats.Get "rss" instead.
-func (s *siteWrapper) RSSLink() template.URL {
- return s.s.RSSLink()
+func (s *siteWrapper) Store() *maps.Scratch {
+ return s.s.Store()
}
// For internal use only.
@@ -319,13 +305,18 @@ func (s *siteWrapper) ForEeachIdentityByName(name string, f func(identity.Identi
s.s.(identity.ForEeachIdentityByNameProvider).ForEeachIdentityByName(name, f)
}
+// For internal use only.
+func (s *siteWrapper) CheckReady() {
+ s.s.CheckReady()
+}
+
type testSite struct {
h hugo.HugoInfo
l *langs.Language
}
// Deprecated: Use taxonomies instead.
-func (s testSite) Author() map[string]interface{} {
+func (s testSite) Author() map[string]any {
return nil
}
@@ -392,20 +383,10 @@ func (t testSite) Languages() langs.Languages {
return nil
}
-// Deprecated: Use .Site.Config.Services.GoogleAnalytics.ID instead.
-func (t testSite) GoogleAnalytics() string {
- return ""
-}
-
func (t testSite) MainSections() []string {
return nil
}
-// Deprecated: Use hugo.IsServer instead.
-func (t testSite) IsServer() bool {
- return false
-}
-
func (t testSite) Language() *langs.Language {
return t.l
}
@@ -450,11 +431,6 @@ func (s testSite) Config() SiteConfig {
return SiteConfig{}
}
-// Deprecated: Use .Site.Config.Services.Disqus.Shortname instead.
-func (testSite) DisqusShortname() string {
- return ""
-}
-
func (s testSite) BuildDrafts() bool {
return false
}
@@ -468,9 +444,11 @@ func (s testSite) Param(key any) (any, error) {
return nil, nil
}
-// Deprecated: Use .Site.Home.OutputFormats.Get "rss" instead.
-func (s testSite) RSSLink() template.URL {
- return ""
+func (s testSite) Store() *maps.Scratch {
+ return maps.NewScratch()
+}
+
+func (s testSite) CheckReady() {
}
// NewDummyHugoSite creates a new minimal test site.
diff --git a/resources/page/site_integration_test.go b/resources/page/site_integration_test.go
new file mode 100644
index 000000000..60064df3a
--- /dev/null
+++ b/resources/page/site_integration_test.go
@@ -0,0 +1,44 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page_test
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/hugolib"
+)
+
+// Issue 12513
+func TestPageSiteSitesDefault(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+defaultContentLanguage = 'de'
+defaultContentLanguageInSubdir = true
+[languages.en]
+languageName = 'English'
+weight = 1
+[languages.de]
+languageName = 'Deutsch'
+weight = 2
+-- layouts/index.html --
+{{ .Site.Sites.Default.Language.LanguageName }}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/de/index.html", "Deutsch")
+}
diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go
index cedbc74e9..1d2ee6223 100644
--- a/resources/page/testhelpers_test.go
+++ b/resources/page/testhelpers_test.go
@@ -52,7 +52,7 @@ func newTestPage() *testPage {
func newTestPageWithFile(filename string) *testPage {
filename = filepath.FromSlash(filename)
- file := source.NewFileInfoFrom(filename, filename)
+ file := source.NewContentFileInfoFrom(filename, filename)
l, err := langs.NewLanguage(
"en",
@@ -67,9 +67,10 @@ func newTestPageWithFile(filename string) *testPage {
}
return &testPage{
- params: make(map[string]any),
- data: make(map[string]any),
- file: file,
+ params: make(map[string]any),
+ data: make(map[string]any),
+ file: file,
+ pathInfo: file.FileInfo().Meta().PathInfo,
currentSection: &testPage{
sectionEntries: []string{"a", "b", "c"},
},
@@ -90,7 +91,8 @@ type testPage struct {
fuzzyWordCount int
- path string
+ path string
+ pathInfo *paths.Path
slug string
@@ -111,10 +113,6 @@ type testPage struct {
sectionEntries []string
}
-func (p *testPage) Err() resource.ResourceError {
- return nil
-}
-
func (p *testPage) Aliases() []string {
panic("testpage: not implemented")
}
@@ -127,16 +125,6 @@ func (p *testPage) AlternativeOutputFormats() OutputFormats {
panic("testpage: not implemented")
}
-// Deprecated: Use taxonomies instead.
-func (p *testPage) Author() Author {
- return Author{}
-}
-
-// Deprecated: Use taxonomies instead.
-func (p *testPage) Authors() AuthorList {
- return nil
-}
-
func (p *testPage) BaseFileName() string {
panic("testpage: not implemented")
}
@@ -149,6 +137,10 @@ func (p *testPage) Content(context.Context) (any, error) {
panic("testpage: not implemented")
}
+func (p *testPage) Markup(...any) Markup {
+ panic("testpage: not implemented")
+}
+
func (p *testPage) ContentBaseName() string {
panic("testpage: not implemented")
}
@@ -177,6 +169,10 @@ func (p *testPage) Description() string {
return ""
}
+func (p *testPage) ContentWithoutSummary(ctx context.Context) (template.HTML, error) {
+ return "", nil
+}
+
func (p *testPage) Dir() string {
panic("testpage: not implemented")
}
@@ -193,14 +189,6 @@ func (p *testPage) ExpiryDate() time.Time {
return p.expiryDate
}
-func (p *testPage) Ext() string {
- panic("testpage: not implemented")
-}
-
-func (p *testPage) Extension() string {
- panic("testpage: not implemented")
-}
-
func (p *testPage) File() *source.File {
return p.file
}
@@ -233,7 +221,7 @@ func (p *testPage) GetTerms(taxonomy string) Pages {
panic("testpage: not implemented")
}
-func (p *testPage) GetRelatedDocsHandler() *RelatedDocsHandler {
+func (p *testPage) GetInternalRelatedDocsHandler() *RelatedDocsHandler {
return relatedDocsHandler
}
@@ -420,7 +408,7 @@ func (p *testPage) Path() string {
}
func (p *testPage) PathInfo() *paths.Path {
- panic("testpage: not implemented")
+ return p.pathInfo
}
func (p *testPage) Permalink() string {
@@ -451,10 +439,6 @@ func (p *testPage) PublishDate() time.Time {
return p.pubDate
}
-func (p *testPage) RSSLink() template.URL {
- return ""
-}
-
func (p *testPage) RawContent() string {
panic("testpage: not implemented")
}
@@ -603,7 +587,7 @@ func (p *testPage) WordCount(context.Context) int {
func createTestPages(num int) Pages {
pages := make(Pages, num)
- for i := 0; i < num; i++ {
+ for i := range num {
m := &testPage{
path: fmt.Sprintf("/x/y/z/p%d.md", i),
weight: 5,
diff --git a/resources/postpub/fields.go b/resources/postpub/fields.go
index 13b2963ce..12b3be2eb 100644
--- a/resources/postpub/fields.go
+++ b/resources/postpub/fields.go
@@ -31,7 +31,7 @@ func structToMap(s any) map[string]any {
m := make(map[string]any)
t := reflect.TypeOf(s)
- for i := 0; i < t.NumMethod(); i++ {
+ for i := range t.NumMethod() {
method := t.Method(i)
if method.PkgPath != "" {
continue
@@ -41,7 +41,7 @@ func structToMap(s any) map[string]any {
}
}
- for i := 0; i < t.NumField(); i++ {
+ for i := range t.NumField() {
field := t.Field(i)
if field.PkgPath != "" {
continue
diff --git a/resources/postpub/fields_test.go b/resources/postpub/fields_test.go
index 336da1f0e..53875cb34 100644
--- a/resources/postpub/fields_test.go
+++ b/resources/postpub/fields_test.go
@@ -36,6 +36,8 @@ func TestCreatePlaceholders(t *testing.T) {
"SuffixesCSV": "pre_foo.SuffixesCSV_post",
"Delimiter": "pre_foo.Delimiter_post",
"FirstSuffix": "pre_foo.FirstSuffix_post",
+ "IsHTML": "pre_foo.IsHTML_post",
+ "IsMarkdown": "pre_foo.IsMarkdown_post",
"IsText": "pre_foo.IsText_post",
"String": "pre_foo.String_post",
"Type": "pre_foo.Type_post",
diff --git a/resources/resource.go b/resources/resource.go
index 0fee69cdd..f6e5b9d73 100644
--- a/resources/resource.go
+++ b/resources/resource.go
@@ -24,8 +24,10 @@ import (
"sync/atomic"
"github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/lazy"
"github.com/gohugoio/hugo/resources/internal"
+ "github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/paths"
@@ -46,10 +48,14 @@ var (
_ resource.Cloner = (*genericResource)(nil)
_ resource.ResourcesLanguageMerger = (*resource.Resources)(nil)
_ resource.Identifier = (*genericResource)(nil)
+ _ resource.TransientIdentifier = (*genericResource)(nil)
+ _ targetPathProvider = (*genericResource)(nil)
+ _ sourcePathProvider = (*genericResource)(nil)
_ identity.IdentityGroupProvider = (*genericResource)(nil)
_ identity.DependencyManagerProvider = (*genericResource)(nil)
_ identity.Identity = (*genericResource)(nil)
_ fileInfo = (*genericResource)(nil)
+ _ isPublishedProvider = (*genericResource)(nil)
)
type ResourceSourceDescriptor struct {
@@ -65,6 +71,9 @@ type ResourceSourceDescriptor struct {
// The name of the resource as it was read from the source.
NameOriginal string
+ // The title of the resource.
+ Title string
+
// Any base paths prepended to the target path. This will also typically be the
// language code, but setting it here means that it should not have any effect on
// the permalink.
@@ -75,10 +84,14 @@ type ResourceSourceDescriptor struct {
TargetPath string
BasePathRelPermalink string
BasePathTargetPath string
+ SourceFilenameOrPath string // Used for error logging.
// The Data to associate with this resource.
Data map[string]any
+ // The Params to associate with this resource.
+ Params maps.Params
+
// Delay publishing until either Permalink or RelPermalink is called. Maybe never.
LazyPublish bool
@@ -107,8 +120,12 @@ func (fd *ResourceSourceDescriptor) init(r *Spec) error {
panic(errors.New("RelPath is empty"))
}
+ if fd.Params == nil {
+ fd.Params = make(maps.Params)
+ }
+
if fd.Path == nil {
- fd.Path = paths.Parse("", fd.TargetPath)
+ fd.Path = r.Cfg.PathParser().Parse("", fd.TargetPath)
}
if fd.TargetPath == "" {
@@ -127,13 +144,6 @@ func (fd *ResourceSourceDescriptor) init(r *Spec) error {
}
fd.TargetPath = paths.ToSlashPreserveLeading(fd.TargetPath)
- for i, base := range fd.TargetBasePaths {
- dir := paths.ToSlashPreserveLeading(base)
- if dir == "/" {
- dir = ""
- }
- fd.TargetBasePaths[i] = dir
- }
if fd.NameNormalized == "" {
fd.NameNormalized = fd.TargetPath
@@ -143,6 +153,10 @@ func (fd *ResourceSourceDescriptor) init(r *Spec) error {
fd.NameOriginal = fd.NameNormalized
}
+ if fd.Title == "" {
+ fd.Title = fd.NameOriginal
+ }
+
mediaType := fd.MediaType
if mediaType.IsZero() {
ext := fd.Path.Ext()
@@ -212,9 +226,6 @@ type resourceCopier interface {
// Copy copies r to the targetPath given.
func Copy(r resource.Resource, targetPath string) resource.Resource {
- if r.Err() != nil {
- panic(fmt.Sprintf("Resource has an .Err: %s", r.Err()))
- }
return r.(resourceCopier).cloneTo(targetPath)
}
@@ -233,6 +244,7 @@ type baseResourceInternal interface {
fileInfo
mediaTypeAssigner
targetPather
+ isPublishedProvider
ReadSeekCloser() (hugio.ReadSeekCloser, error)
@@ -293,7 +305,7 @@ type fileInfo interface {
}
type hashProvider interface {
- hash() string
+ hash() uint64
}
var _ resource.StaleInfo = (*StaleValue[any])(nil)
@@ -346,11 +358,15 @@ func GetTestInfoForResource(r resource.Resource) GenericResourceTestInfo {
// genericResource represents a generic linkable resource.
type genericResource struct {
- publishInit *sync.Once
+ publishInit *lazy.OnceMore
+
+ key string
+ keyInit *sync.Once
sd ResourceSourceDescriptor
paths internal.ResourcePaths
+ includeHashInKey bool
sourceFilenameIsHash bool
h *resourceHash // A hash of the source content. Is only calculated in caching situations.
@@ -389,7 +405,7 @@ func (l *genericResource) size() int64 {
return l.h.size
}
-func (l *genericResource) hash() string {
+func (l *genericResource) hash() uint64 {
if err := l.h.init(l); err != nil {
panic(err)
}
@@ -424,28 +440,44 @@ func (l *genericResource) Content(context.Context) (any, error) {
return hugio.ReadString(r)
}
-func (r *genericResource) Err() resource.ResourceError {
- return nil
-}
-
func (l *genericResource) Data() any {
return l.sd.Data
}
func (l *genericResource) Key() string {
- basePath := l.spec.Cfg.BaseURL().BasePathNoTrailingSlash
- var key string
- if basePath == "" {
- key = l.RelPermalink()
- } else {
- key = strings.TrimPrefix(l.RelPermalink(), basePath)
- }
+ l.keyInit.Do(func() {
+ basePath := l.spec.Cfg.BaseURL().BasePathNoTrailingSlash
+ if basePath == "" {
+ l.key = l.RelPermalink()
+ } else {
+ l.key = strings.TrimPrefix(l.RelPermalink(), basePath)
+ }
- if l.spec.Cfg.IsMultihost() {
- key = l.spec.Lang() + key
- }
+ if l.spec.Cfg.IsMultihost() {
+ l.key = l.spec.Lang() + l.key
+ }
- return key
+ if l.includeHashInKey && !l.sourceFilenameIsHash {
+ l.key += fmt.Sprintf("_%d", l.hash())
+ }
+ })
+
+ return l.key
+}
+
+func (l *genericResource) TransientKey() string {
+ return l.Key()
+}
+
+func (l *genericResource) targetPath() string {
+ return l.paths.TargetPath()
+}
+
+func (l *genericResource) sourcePath() string {
+ if p := l.sd.SourceFilenameOrPath; p != "" {
+ return p
+ }
+ return ""
}
func (l *genericResource) MediaType() media.Type {
@@ -507,6 +539,10 @@ func (l *genericResource) Publish() error {
return err
}
+func (l *genericResource) isPublished() bool {
+ return l.publishInit.Done()
+}
+
func (l *genericResource) RelPermalink() string {
return l.spec.PathSpec.GetBasePath(false) + paths.PathEscape(l.paths.TargetLink())
}
@@ -600,7 +636,8 @@ func (rc *genericResource) cloneWithUpdates(u *transformationUpdate) (baseResour
}
func (l genericResource) clone() *genericResource {
- l.publishInit = &sync.Once{}
+ l.publishInit = &lazy.OnceMore{}
+ l.keyInit = &sync.Once{}
return &l
}
@@ -613,8 +650,12 @@ type targetPather interface {
TargetPath() string
}
+type isPublishedProvider interface {
+ isPublished() bool
+}
+
type resourceHash struct {
- value string
+ value uint64
size int64
initOnce sync.Once
}
@@ -622,7 +663,7 @@ type resourceHash struct {
func (r *resourceHash) init(l hugio.ReadSeekCloserProvider) error {
var initErr error
r.initOnce.Do(func() {
- var hash string
+ var hash uint64
var size int64
f, err := l.ReadSeekCloser()
if err != nil {
@@ -630,7 +671,7 @@ func (r *resourceHash) init(l hugio.ReadSeekCloserProvider) error {
return
}
defer f.Close()
- hash, size, err = helpers.MD5FromReaderFast(f)
+ hash, size, err = hashImage(f)
if err != nil {
initErr = fmt.Errorf("failed to calculate hash: %w", err)
return
@@ -641,3 +682,52 @@ func (r *resourceHash) init(l hugio.ReadSeekCloserProvider) error {
return initErr
}
+
+func hashImage(r io.ReadSeeker) (uint64, int64, error) {
+ return hashing.XXHashFromReader(r)
+}
+
+// InternalResourceTargetPath is used internally to get the target path for a Resource.
+func InternalResourceTargetPath(r resource.Resource) string {
+ return r.(targetPathProvider).targetPath()
+}
+
+// InternalResourceSourcePathBestEffort is used internally to get the source path for a Resource.
+// It returns an empty string if the source path is not available.
+func InternalResourceSourcePath(r resource.Resource) string {
+ if sp, ok := r.(sourcePathProvider); ok {
+ if p := sp.sourcePath(); p != "" {
+ return p
+ }
+ }
+ return ""
+}
+
+// InternalResourceSourcePathBestEffort is used internally to get the source path for a Resource.
+// Used for error messages etc.
+// It will fall back to the target path if the source path is not available.
+func InternalResourceSourcePathBestEffort(r resource.Resource) string {
+ if s := InternalResourceSourcePath(r); s != "" {
+ return s
+ }
+ return InternalResourceTargetPath(r)
+}
+
+// isPublished returns true if the resource is published.
+func IsPublished(r resource.Resource) bool {
+ return r.(isPublishedProvider).isPublished()
+}
+
+type targetPathProvider interface {
+ // targetPath is the relative path to this resource.
+ // In most cases this will be the same as the RelPermalink(),
+ // but it will not trigger any lazy publishing.
+ targetPath() string
+}
+
+// Optional interface implemented by resources that can provide the source path.
+type sourcePathProvider interface {
+ // sourcePath is the source path to this resource's source.
+ // This is used in error messages etc.
+ sourcePath() string
+}
diff --git a/resources/resource/resource_helpers.go b/resources/resource/resource_helpers.go
index 90075f983..8575ae79e 100644
--- a/resources/resource/resource_helpers.go
+++ b/resources/resource/resource_helpers.go
@@ -17,6 +17,8 @@ import (
"strings"
"time"
+ "github.com/gohugoio/hugo/common/maps"
+
"github.com/gohugoio/hugo/helpers"
"github.com/pelletier/go-toml/v2"
@@ -36,9 +38,9 @@ func GetParamToLower(r Resource, key string) any {
}
func getParam(r Resource, key string, stringToLower bool) any {
- v := r.Params()[strings.ToLower(key)]
+ v, err := maps.GetNestedParam(key, ".", r.Params())
- if v == nil {
+ if v == nil || err != nil {
return nil
}
diff --git a/resources/resource/resources.go b/resources/resource/resources.go
index 32bcdbb08..6b7311bad 100644
--- a/resources/resource/resources.go
+++ b/resources/resource/resources.go
@@ -16,11 +16,15 @@ package resource
import (
"fmt"
+ "path"
"strings"
+ "github.com/gohugoio/hugo/common/hreflect"
+ "github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/hugofs/glob"
"github.com/spf13/cast"
+ "slices"
)
var _ ResourceFinder = (*Resources)(nil)
@@ -29,6 +33,51 @@ var _ ResourceFinder = (*Resources)(nil)
// I.e. both pages and images etc.
type Resources []Resource
+// Mount mounts the given resources from base to the given target path.
+// Note that leading slashes in target marks an absolute path.
+// This method is currently only useful in js.Batch.
+func (r Resources) Mount(base, target string) ResourceGetter {
+ return resourceGetterFunc(func(namev any) Resource {
+ name1, err := cast.ToStringE(namev)
+ if err != nil {
+ panic(err)
+ }
+
+ isTargetAbs := strings.HasPrefix(target, "/")
+
+ if target != "" {
+ name1 = strings.TrimPrefix(name1, target)
+ if !isTargetAbs {
+ name1 = paths.TrimLeading(name1)
+ }
+ }
+
+ if base != "" && isTargetAbs {
+ name1 = path.Join(base, name1)
+ }
+
+ for _, res := range r {
+ name2 := res.Name()
+
+ if base != "" && !isTargetAbs {
+ name2 = paths.TrimLeading(strings.TrimPrefix(name2, base))
+ }
+
+ if strings.EqualFold(name1, name2) {
+ return res
+ }
+
+ }
+
+ return nil
+ })
+}
+
+type ResourcesProvider interface {
+ // Resources returns a list of all resources.
+ Resources() Resources
+}
+
// var _ resource.ResourceFinder = (*Namespace)(nil)
// ResourcesConverter converts a given slice of Resource objects to Resources.
type ResourcesConverter interface {
@@ -63,13 +112,25 @@ func (r Resources) Get(name any) Resource {
panic(err)
}
- namestr = paths.AddLeadingSlash(namestr)
+ isDotCurrent := strings.HasPrefix(namestr, "./")
+ if isDotCurrent {
+ namestr = strings.TrimPrefix(namestr, "./")
+ } else {
+ namestr = paths.AddLeadingSlash(namestr)
+ }
+
+ check := func(name string) bool {
+ if !isDotCurrent {
+ name = paths.AddLeadingSlash(name)
+ }
+ return strings.EqualFold(namestr, name)
+ }
// First check the Name.
// Note that this can be modified by the user in the front matter,
// also, it does not contain any language code.
for _, resource := range r {
- if strings.EqualFold(namestr, paths.AddLeadingSlash(resource.Name())) {
+ if check(resource.Name()) {
return resource
}
}
@@ -77,7 +138,7 @@ func (r Resources) Get(name any) Resource {
// Finally, check the normalized name.
for _, resource := range r {
if nop, ok := resource.(NameNormalizedProvider); ok {
- if strings.EqualFold(namestr, paths.AddLeadingSlash(nop.NameNormalized())) {
+ if check(nop.NameNormalized()) {
return resource
}
}
@@ -162,7 +223,7 @@ type translatedResource interface {
// MergeByLanguage adds missing translations in r1 from r2.
func (r Resources) MergeByLanguage(r2 Resources) Resources {
- result := append(Resources(nil), r...)
+ result := slices.Clone(r)
m := make(map[string]bool)
for _, rr := range r {
if translated, ok := rr.(translatedResource); ok {
@@ -197,14 +258,35 @@ type Source interface {
Publish() error
}
-// ResourceFinder provides methods to find Resources.
-// Note that GetRemote (as found in resources.GetRemote) is
-// not covered by this interface, as this is only available as a global template function.
-type ResourceFinder interface {
+type ResourceGetter interface {
// Get locates the Resource with the given name in the current context (e.g. in .Page.Resources).
//
// It returns nil if no Resource could found, panics if name is invalid.
Get(name any) Resource
+}
+
+type IsProbablySameResourceGetter interface {
+ IsProbablySameResourceGetter(other ResourceGetter) bool
+}
+
+// StaleInfoResourceGetter is a ResourceGetter that also provides information about
+// whether the underlying resources are stale.
+type StaleInfoResourceGetter interface {
+ StaleInfo
+ ResourceGetter
+}
+
+type resourceGetterFunc func(name any) Resource
+
+func (f resourceGetterFunc) Get(name any) Resource {
+ return f(name)
+}
+
+// ResourceFinder provides methods to find Resources.
+// Note that GetRemote (as found in resources.GetRemote) is
+// not covered by this interface, as this is only available as a global template function.
+type ResourceFinder interface {
+ ResourceGetter
// GetMatch finds the first Resource matching the given pattern, or nil if none found.
//
@@ -235,3 +317,92 @@ type ResourceFinder interface {
// It returns nil if no Resources could found, panics if typ is invalid.
ByType(typ any) Resources
}
+
+// NewCachedResourceGetter creates a new ResourceGetter from the given objects.
+// If multiple objects are provided, they are merged into one where
+// the first match wins.
+func NewCachedResourceGetter(os ...any) *cachedResourceGetter {
+ var getters multiResourceGetter
+ for _, o := range os {
+ if g, ok := unwrapResourceGetter(o); ok {
+ getters = append(getters, g)
+ }
+ }
+
+ return &cachedResourceGetter{
+ cache: maps.NewCache[string, Resource](),
+ delegate: getters,
+ }
+}
+
+type multiResourceGetter []ResourceGetter
+
+func (m multiResourceGetter) Get(name any) Resource {
+ for _, g := range m {
+ if res := g.Get(name); res != nil {
+ return res
+ }
+ }
+ return nil
+}
+
+var (
+ _ ResourceGetter = (*cachedResourceGetter)(nil)
+ _ IsProbablySameResourceGetter = (*cachedResourceGetter)(nil)
+)
+
+type cachedResourceGetter struct {
+ cache *maps.Cache[string, Resource]
+ delegate ResourceGetter
+}
+
+func (c *cachedResourceGetter) Get(name any) Resource {
+ namestr, err := cast.ToStringE(name)
+ if err != nil {
+ panic(err)
+ }
+ v, _ := c.cache.GetOrCreate(namestr, func() (Resource, error) {
+ v := c.delegate.Get(name)
+ return v, nil
+ })
+ return v
+}
+
+func (c *cachedResourceGetter) IsProbablySameResourceGetter(other ResourceGetter) bool {
+ isProbablyEq := true
+ c.cache.ForEeach(func(k string, v Resource) bool {
+ if v != other.Get(k) {
+ isProbablyEq = false
+ return false
+ }
+ return true
+ })
+
+ return isProbablyEq
+}
+
+func unwrapResourceGetter(v any) (ResourceGetter, bool) {
+ if v == nil {
+ return nil, false
+ }
+ switch vv := v.(type) {
+ case ResourceGetter:
+ return vv, true
+ case ResourcesProvider:
+ return vv.Resources(), true
+ case func(name any) Resource:
+ return resourceGetterFunc(vv), true
+ default:
+ vvv, ok := hreflect.ToSliceAny(v)
+ if !ok {
+ return nil, false
+ }
+ var getters multiResourceGetter
+ for _, vv := range vvv {
+ if g, ok := unwrapResourceGetter(vv); ok {
+ getters = append(getters, g)
+ }
+ }
+ return getters, len(getters) > 0
+ }
+}
diff --git a/resources/resource/resources_integration_test.go b/resources/resource/resources_integration_test.go
new file mode 100644
index 000000000..920fc7397
--- /dev/null
+++ b/resources/resource/resources_integration_test.go
@@ -0,0 +1,105 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resource_test
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/hugolib"
+)
+
+func TestResourcesMount(t *testing.T) {
+ files := `
+-- hugo.toml --
+-- assets/text/txt1.txt --
+Text 1.
+-- assets/text/txt2.txt --
+Text 2.
+-- assets/text/sub/txt3.txt --
+Text 3.
+-- assets/text/sub/txt4.txt --
+Text 4.
+-- content/mybundle/index.md --
+---
+title: "My Bundle"
+---
+-- content/mybundle/txt1.txt --
+Text 1.
+-- content/mybundle/sub/txt2.txt --
+Text 1.
+-- layouts/index.html --
+{{ $mybundle := site.GetPage "mybundle" }}
+{{ $subResources := resources.Match "/text/sub/*.*" }}
+{{ $subResourcesMount := $subResources.Mount "/text/sub" "/newroot" }}
+resources:text/txt1.txt:{{ with resources.Get "text/txt1.txt" }}{{ .Name }}{{ end }}|
+resources:text/txt2.txt:{{ with resources.Get "text/txt2.txt" }}{{ .Name }}{{ end }}|
+resources:text/sub/txt3.txt:{{ with resources.Get "text/sub/txt3.txt" }}{{ .Name }}{{ end }}|
+subResources.range:{{ range $subResources }}{{ .Name }}|{{ end }}|
+subResources:"text/sub/txt3.txt:{{ with $subResources.Get "text/sub/txt3.txt" }}{{ .Name }}{{ end }}|
+subResourcesMount:/newroot/txt3.txt:{{ with $subResourcesMount.Get "/newroot/txt3.txt" }}{{ .Name }}{{ end }}|
+page:txt1.txt:{{ with $mybundle.Resources.Get "txt1.txt" }}{{ .Name }}{{ end }}|
+page:./txt1.txt:{{ with $mybundle.Resources.Get "./txt1.txt" }}{{ .Name }}{{ end }}|
+page:sub/txt2.txt:{{ with $mybundle.Resources.Get "sub/txt2.txt" }}{{ .Name }}{{ end }}|
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", `
+resources:text/txt1.txt:/text/txt1.txt|
+resources:text/txt2.txt:/text/txt2.txt|
+resources:text/sub/txt3.txt:/text/sub/txt3.txt|
+subResources:"text/sub/txt3.txt:/text/sub/txt3.txt|
+subResourcesMount:/newroot/txt3.txt:/text/sub/txt3.txt|
+page:txt1.txt:txt1.txt|
+page:./txt1.txt:txt1.txt|
+page:sub/txt2.txt:sub/txt2.txt|
+`)
+}
+
+func TestResourcesMountOnRename(t *testing.T) {
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "home", "sitemap"]
+-- content/mybundle/index.md --
+---
+title: "My Bundle"
+resources:
+- name: /foo/bars.txt
+ src: foo/txt1.txt
+- name: foo/bars2.txt
+ src: foo/txt2.txt
+---
+-- content/mybundle/foo/txt1.txt --
+Text 1.
+-- content/mybundle/foo/txt2.txt --
+Text 2.
+-- layouts/_default/single.html --
+Single.
+{{ $mybundle := site.GetPage "mybundle" }}
+Resources:{{ range $mybundle.Resources }}Name: {{ .Name }}|{{ end }}$
+{{ $subResourcesMount := $mybundle.Resources.Mount "/foo" "/newroot" }}
+ {{ $subResourcesMount2 := $mybundle.Resources.Mount "foo" "/newroot" }}
+{{ $subResourcesMount3 := $mybundle.Resources.Mount "foo" "." }}
+subResourcesMount:/newroot/bars.txt:{{ with $subResourcesMount.Get "/newroot/bars.txt" }}{{ .Name }}{{ end }}|
+subResourcesMount:/newroot/bars2.txt:{{ with $subResourcesMount.Get "/newroot/bars2.txt" }}{{ .Name }}{{ end }}|
+subResourcesMount2:/newroot/bars2.txt:{{ with $subResourcesMount2.Get "/newroot/bars2.txt" }}{{ .Name }}{{ end }}|
+subResourcesMount3:bars2.txt:{{ with $subResourcesMount3.Get "bars2.txt" }}{{ .Name }}{{ end }}|
+`
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/mybundle/index.html",
+ "Resources:Name: foo/bars.txt|Name: foo/bars2.txt|$",
+ "subResourcesMount:/newroot/bars.txt:|\nsubResourcesMount:/newroot/bars2.txt:|",
+ "subResourcesMount2:/newroot/bars2.txt:foo/bars2.txt|",
+ "subResourcesMount3:bars2.txt:foo/bars2.txt|",
+ )
+}
diff --git a/resources/resource/resources_test.go b/resources/resource/resources_test.go
new file mode 100644
index 000000000..ebadbb312
--- /dev/null
+++ b/resources/resource/resources_test.go
@@ -0,0 +1,122 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resource
+
+import (
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestResourcesMount(t *testing.T) {
+ c := qt.New(t)
+ c.Assert(true, qt.IsTrue)
+
+ var m ResourceGetter
+ var r Resources
+
+ check := func(in, expect string) {
+ c.Helper()
+ r := m.Get(in)
+ c.Assert(r, qt.Not(qt.IsNil))
+ c.Assert(r.Name(), qt.Equals, expect)
+ }
+
+ checkNil := func(in string) {
+ c.Helper()
+ r := m.Get(in)
+ c.Assert(r, qt.IsNil)
+ }
+
+ // Misc tests.
+ r = Resources{
+ testResource{name: "/foo/theme.css"},
+ }
+
+ m = r.Mount("/foo", ".")
+ check("./theme.css", "/foo/theme.css")
+
+ // Relative target.
+ r = Resources{
+ testResource{name: "/a/b/c/d.txt"},
+ testResource{name: "/a/b/c/e/f.txt"},
+ testResource{name: "/a/b/d.txt"},
+ testResource{name: "/a/b/e.txt"},
+ }
+
+ m = r.Mount("/a/b/c", "z")
+ check("z/d.txt", "/a/b/c/d.txt")
+ check("z/e/f.txt", "/a/b/c/e/f.txt")
+
+ m = r.Mount("/a/b", "")
+ check("d.txt", "/a/b/d.txt")
+ m = r.Mount("/a/b", ".")
+ check("d.txt", "/a/b/d.txt")
+ m = r.Mount("/a/b", "./")
+ check("d.txt", "/a/b/d.txt")
+ check("./d.txt", "/a/b/d.txt")
+
+ m = r.Mount("/a/b", ".")
+ check("./d.txt", "/a/b/d.txt")
+
+ // Absolute target.
+ m = r.Mount("/a/b/c", "/z")
+ check("/z/d.txt", "/a/b/c/d.txt")
+ check("/z/e/f.txt", "/a/b/c/e/f.txt")
+ checkNil("/z/f.txt")
+
+ m = r.Mount("/a/b", "/z")
+ check("/z/c/d.txt", "/a/b/c/d.txt")
+ check("/z/c/e/f.txt", "/a/b/c/e/f.txt")
+ check("/z/d.txt", "/a/b/d.txt")
+ checkNil("/z/f.txt")
+
+ m = r.Mount("", "")
+ check("/a/b/c/d.txt", "/a/b/c/d.txt")
+ check("/a/b/c/e/f.txt", "/a/b/c/e/f.txt")
+ check("/a/b/d.txt", "/a/b/d.txt")
+ checkNil("/a/b/f.txt")
+
+ m = r.Mount("/a/b", "/a/b")
+ check("/a/b/c/d.txt", "/a/b/c/d.txt")
+ check("/a/b/c/e/f.txt", "/a/b/c/e/f.txt")
+ check("/a/b/d.txt", "/a/b/d.txt")
+ checkNil("/a/b/f.txt")
+
+ // Resources with relative paths.
+ r = Resources{
+ testResource{name: "a/b/c/d.txt"},
+ testResource{name: "a/b/c/e/f.txt"},
+ testResource{name: "a/b/d.txt"},
+ testResource{name: "a/b/e.txt"},
+ testResource{name: "n.txt"},
+ }
+
+ m = r.Mount("a/b", "z")
+ check("z/d.txt", "a/b/d.txt")
+ checkNil("/z/d.txt")
+}
+
+type testResource struct {
+ Resource
+ name string
+}
+
+func (r testResource) Name() string {
+ return r.name
+}
+
+func (r testResource) NameNormalized() string {
+ return r.name
+}
diff --git a/resources/resource/resourcetypes.go b/resources/resource/resourcetypes.go
index 5d9533223..51255c612 100644
--- a/resources/resource/resourcetypes.go
+++ b/resources/resource/resourcetypes.go
@@ -43,6 +43,9 @@ type OriginProvider interface {
// NewResourceError creates a new ResourceError.
func NewResourceError(err error, data any) ResourceError {
+ if data == nil {
+ data = map[string]any{}
+ }
return &resourceError{
error: err,
data: data,
@@ -65,22 +68,17 @@ type ResourceError interface {
ResourceDataProvider
}
-// ErrProvider provides an Err.
-type ErrProvider interface {
- // Err returns an error if this resource is in an error state.
- // This will currently only be set for resources obtained from resources.GetRemote.
- Err() ResourceError
-}
-
// Resource represents a linkable resource, i.e. a content page, image etc.
type Resource interface {
+ ResourceWithoutMeta
+ ResourceMetaProvider
+}
+
+type ResourceWithoutMeta interface {
ResourceTypeProvider
MediaTypeProvider
ResourceLinksProvider
- ResourceNameTitleProvider
- ResourceParamsProvider
ResourceDataProvider
- ErrProvider
}
type ResourceTypeProvider interface {
@@ -162,11 +160,19 @@ type ResourcesLanguageMerger interface {
// Identifier identifies a resource.
type Identifier interface {
- // Key is is mostly for internal use and should be considered opaque.
+ // Key is mostly for internal use and should be considered opaque.
// This value may change between Hugo versions.
Key() string
}
+// TransientIdentifier identifies a transient resource.
+type TransientIdentifier interface {
+ // TransientKey is mostly for internal use and should be considered opaque.
+ // This value is implemented by transient resources where pointers may be short lived and
+ // not suitable for use as a map keys.
+ TransientKey() string
+}
+
// WeightProvider provides a weight.
type WeightProvider interface {
Weight() int
@@ -290,3 +296,11 @@ func (r resourceTypesHolder) ResourceType() string {
func NewResourceTypesProvider(mediaType media.Type, resourceType string) ResourceTypesProvider {
return resourceTypesHolder{mediaType: mediaType, resourceType: resourceType}
}
+
+// NameNormalizedOrName returns the normalized name if available, otherwise the name.
+func NameNormalizedOrName(r Resource) string {
+ if nn, ok := r.(NameNormalizedProvider); ok {
+ return nn.NameNormalized()
+ }
+ return r.Name()
+}
diff --git a/resources/resource_cache.go b/resources/resource_cache.go
index bf930c71d..898cd4c31 100644
--- a/resources/resource_cache.go
+++ b/resources/resource_cache.go
@@ -36,6 +36,16 @@ func newResourceCache(rs *Spec, memCache *dynacache.Cache) *ResourceCache {
"/res1",
dynacache.OptionsPartition{ClearWhen: dynacache.ClearOnChange, Weight: 40},
),
+ cacheResourceFile: dynacache.GetOrCreatePartition[string, resource.Resource](
+ memCache,
+ "/res2",
+ dynacache.OptionsPartition{ClearWhen: dynacache.ClearOnChange, Weight: 40},
+ ),
+ CacheResourceRemote: dynacache.GetOrCreatePartition[string, resource.Resource](
+ memCache,
+ "/resr",
+ dynacache.OptionsPartition{ClearWhen: dynacache.ClearOnChange, Weight: 40},
+ ),
cacheResources: dynacache.GetOrCreatePartition[string, resource.Resources](
memCache,
"/ress",
@@ -53,6 +63,8 @@ type ResourceCache struct {
sync.RWMutex
cacheResource *dynacache.Partition[string, resource.Resource]
+ cacheResourceFile *dynacache.Partition[string, resource.Resource]
+ CacheResourceRemote *dynacache.Partition[string, resource.Resource]
cacheResources *dynacache.Partition[string, resource.Resources]
cacheResourceTransformation *dynacache.Partition[string, *resourceAdapterInner]
@@ -73,6 +85,12 @@ func (c *ResourceCache) GetOrCreate(key string, f func() (resource.Resource, err
})
}
+func (c *ResourceCache) GetOrCreateFile(key string, f func() (resource.Resource, error)) (resource.Resource, error) {
+ return c.cacheResourceFile.GetOrCreate(key, func(key string) (resource.Resource, error) {
+ return f()
+ })
+}
+
func (c *ResourceCache) GetOrCreateResources(key string, f func() (resource.Resources, error)) (resource.Resources, error) {
return c.cacheResources.GetOrCreate(key, func(key string) (resource.Resources, error) {
return f()
diff --git a/resources/resource_factories/bundler/bundler.go b/resources/resource_factories/bundler/bundler.go
index dd0f1a4e1..aef644b7f 100644
--- a/resources/resource_factories/bundler/bundler.go
+++ b/resources/resource_factories/bundler/bundler.go
@@ -95,6 +95,10 @@ func (c *Client) Concat(targetPath string, r resource.Resources) (resource.Resou
}
idm := c.rs.Cfg.NewIdentityManager("concat")
+
+ // Re-create on structural changes.
+ idm.AddIdentity(identity.StructuralChangeAdd, identity.StructuralChangeRemove)
+
// Add the concatenated resources as dependencies to the composite resource
// so that we can track changes to the individual resources.
idm.AddIdentityForEach(identity.ForEeachIdentityProviderFunc(
@@ -137,7 +141,7 @@ func (c *Client) Concat(targetPath string, r resource.Resources) (resource.Resou
if resolvedm.MainType == media.Builtin.JavascriptType.MainType && resolvedm.SubType == media.Builtin.JavascriptType.SubType {
readers := make([]hugio.ReadSeekCloser, 2*len(rcsources)-1)
j := 0
- for i := 0; i < len(rcsources); i++ {
+ for i := range rcsources {
if i > 0 {
readers[j] = hugio.NewReadSeekerNoOpCloserFromString("\n;\n")
j++
diff --git a/resources/resource_factories/bundler/bundler_test.go b/resources/resource_factories/bundler/bundler_test.go
index 17a74cc88..66f0c2340 100644
--- a/resources/resource_factories/bundler/bundler_test.go
+++ b/resources/resource_factories/bundler/bundler_test.go
@@ -31,7 +31,7 @@ func TestMultiReadSeekCloser(t *testing.T) {
hugio.NewReadSeekerNoOpCloserFromString("C"),
)
- for i := 0; i < 3; i++ {
+ for range 3 {
s1 := helpers.ReaderToString(rc)
c.Assert(s1, qt.Equals, "ABC")
_, err := rc.Seek(0, 0)
diff --git a/resources/resource_factories/create/create.go b/resources/resource_factories/create/create.go
index 4725cf390..2aecb5a93 100644
--- a/resources/resource_factories/create/create.go
+++ b/resources/resource_factories/create/create.go
@@ -23,7 +23,9 @@ import (
"strings"
"time"
- "github.com/gohugoio/hugo/helpers"
+ "github.com/bep/logg"
+ "github.com/gohugoio/httpcache"
+ hhttpcache "github.com/gohugoio/hugo/cache/httpcache"
"github.com/gohugoio/hugo/hugofs/glob"
"github.com/gohugoio/hugo/identity"
@@ -31,7 +33,10 @@ import (
"github.com/gohugoio/hugo/cache/dynacache"
"github.com/gohugoio/hugo/cache/filecache"
+ "github.com/gohugoio/hugo/common/hashing"
+ "github.com/gohugoio/hugo/common/hcontext"
"github.com/gohugoio/hugo/common/hugio"
+ "github.com/gohugoio/hugo/common/tasks"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource"
)
@@ -39,19 +44,80 @@ import (
// Client contains methods to create Resource objects.
// tasks to Resource objects.
type Client struct {
- rs *resources.Spec
- httpClient *http.Client
- cacheGetResource *filecache.Cache
+ rs *resources.Spec
+ httpClient *http.Client
+ httpCacheConfig hhttpcache.ConfigCompiled
+ cacheGetResource *filecache.Cache
+ resourceIDDispatcher hcontext.ContextDispatcher[string]
+
+ // Set when watching.
+ remoteResourceChecker *tasks.RunEvery
+ remoteResourceLogger logg.LevelLogger
}
+type contextKey uint8
+
+const (
+ contextKeyResourceID contextKey = iota
+)
+
// New creates a new Client with the given specification.
func New(rs *resources.Spec) *Client {
+ fileCache := rs.FileCaches.GetResourceCache()
+ resourceIDDispatcher := hcontext.NewContextDispatcher[string](contextKeyResourceID)
+ httpCacheConfig := rs.Cfg.GetConfigSection("httpCacheCompiled").(hhttpcache.ConfigCompiled)
+ var remoteResourceChecker *tasks.RunEvery
+ if rs.Cfg.Watching() && !httpCacheConfig.IsPollingDisabled() {
+ remoteResourceChecker = &tasks.RunEvery{
+ HandleError: func(name string, err error) {
+ rs.Logger.Warnf("Failed to check remote resource: %s", err)
+ },
+ RunImmediately: false,
+ }
+
+ if err := remoteResourceChecker.Start(); err != nil {
+ panic(err)
+ }
+
+ rs.BuildClosers.Add(remoteResourceChecker)
+ }
+
+ httpTimeout := 2 * time.Minute // Need to cover retries.
+ if httpTimeout < (rs.Cfg.Timeout() + 30*time.Second) {
+ httpTimeout = rs.Cfg.Timeout() + 30*time.Second
+ }
+
return &Client{
- rs: rs,
+ rs: rs,
+ httpCacheConfig: httpCacheConfig,
+ resourceIDDispatcher: resourceIDDispatcher,
+ remoteResourceChecker: remoteResourceChecker,
+ remoteResourceLogger: rs.Logger.InfoCommand("remote"),
httpClient: &http.Client{
- Timeout: time.Minute,
+ Timeout: httpTimeout,
+ Transport: &httpcache.Transport{
+ Cache: fileCache.AsHTTPCache(),
+ CacheKey: func(req *http.Request) string {
+ return resourceIDDispatcher.Get(req.Context())
+ },
+ Around: func(req *http.Request, key string) func() {
+ return fileCache.NamedLock(key)
+ },
+ AlwaysUseCachedResponse: func(req *http.Request, key string) bool {
+ return !httpCacheConfig.For(req.URL.String())
+ },
+ ShouldCache: func(req *http.Request, resp *http.Response, key string) bool {
+ return shouldCache(resp.StatusCode)
+ },
+ MarkCachedResponses: true,
+ EnableETagPair: true,
+ Transport: &transport{
+ Cfg: rs.Cfg,
+ Logger: rs.Logger,
+ },
+ },
},
- cacheGetResource: rs.FileCaches.GetResourceCache(),
+ cacheGetResource: fileCache,
}
}
@@ -81,17 +147,7 @@ func (c *Client) Get(pathname string) (resource.Resource, error) {
return nil, err
}
- pi := fi.(hugofs.FileMetaInfo).Meta().PathInfo
-
- return c.rs.NewResource(resources.ResourceSourceDescriptor{
- LazyPublish: true,
- OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) {
- return c.rs.BaseFs.Assets.Fs.Open(filename)
- },
- Path: pi,
- GroupIdentity: pi,
- TargetPath: pathname,
- })
+ return c.getOrCreateFileResource(fi.(hugofs.FileMetaInfo))
})
}
@@ -117,6 +173,23 @@ func (c *Client) GetMatch(pattern string) (resource.Resource, error) {
return res[0], err
}
+func (c *Client) getOrCreateFileResource(info hugofs.FileMetaInfo) (resource.Resource, error) {
+ meta := info.Meta()
+ return c.rs.ResourceCache.GetOrCreateFile(filepath.ToSlash(meta.Filename), func() (resource.Resource, error) {
+ return c.rs.NewResource(resources.ResourceSourceDescriptor{
+ LazyPublish: true,
+ OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) {
+ return meta.Open()
+ },
+ NameNormalized: meta.PathInfo.Path(),
+ NameOriginal: meta.PathInfo.Unnormalized().Path(),
+ GroupIdentity: meta.PathInfo,
+ TargetPath: meta.PathInfo.Unnormalized().Path(),
+ SourceFilenameOrPath: meta.Filename,
+ })
+ })
+}
+
func (c *Client) match(name, pattern string, matchFunc func(r resource.Resource) bool, firstOnly bool) (resource.Resources, error) {
pattern = glob.NormalizePath(pattern)
partitions := glob.FilterGlobParts(strings.Split(pattern, "/"))
@@ -127,18 +200,7 @@ func (c *Client) match(name, pattern string, matchFunc func(r resource.Resource)
var res resource.Resources
handle := func(info hugofs.FileMetaInfo) (bool, error) {
- meta := info.Meta()
-
- r, err := c.rs.NewResource(resources.ResourceSourceDescriptor{
- LazyPublish: true,
- OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) {
- return meta.Open()
- },
- NameNormalized: meta.PathInfo.Path(),
- NameOriginal: meta.PathInfo.Unnormalized().Path(),
- GroupIdentity: meta.PathInfo,
- TargetPath: meta.PathInfo.Unnormalized().Path(),
- })
+ r, err := c.getOrCreateFileResource(info)
if err != nil {
return true, err
}
@@ -160,22 +222,83 @@ func (c *Client) match(name, pattern string, matchFunc func(r resource.Resource)
})
}
-// FromString creates a new Resource from a string with the given relative target path.
-// TODO(bep) see #10912; we currently emit a warning for this config scenario.
-func (c *Client) FromString(targetPath, content string) (resource.Resource, error) {
- targetPath = path.Clean(targetPath)
- key := dynacache.CleanKey(targetPath) + helpers.MD5String(content)
+type Options struct {
+ // The target path relative to the publish directory.
+ // Unix style path, i.e. "images/logo.png".
+ TargetPath string
+
+ // Whether the TargetPath has a hash in it which will change if the resource changes.
+ // If not, we will calculate a hash from the content.
+ TargetPathHasHash bool
+
+ // The content to create the Resource from.
+ CreateContent func() (func() (hugio.ReadSeekCloser, error), error)
+}
+
+// FromOpts creates a new Resource from the given Options.
+// Make sure to set optis.TargetPathHasHash if the TargetPath already contains a hash,
+// as this avoids the need to calculate it.
+// To create a new ReadSeekCloser from a string, use hugio.NewReadSeekerNoOpCloserFromString,
+// or hugio.NewReadSeekerNoOpCloserFromBytes for a byte slice.
+// See FromString.
+func (c *Client) FromOpts(opts Options) (resource.Resource, error) {
+ opts.TargetPath = path.Clean(opts.TargetPath)
+ var hash string
+ var newReadSeeker func() (hugio.ReadSeekCloser, error) = nil
+ if !opts.TargetPathHasHash {
+ var err error
+ newReadSeeker, err = opts.CreateContent()
+ if err != nil {
+ return nil, err
+ }
+ if err := func() error {
+ r, err := newReadSeeker()
+ if err != nil {
+ return err
+ }
+ defer r.Close()
+
+ hash, err = hashing.XxHashFromReaderHexEncoded(r)
+ if err != nil {
+ return err
+ }
+ return nil
+ }(); err != nil {
+ return nil, err
+ }
+ }
+
+ key := dynacache.CleanKey(opts.TargetPath) + hash
r, err := c.rs.ResourceCache.GetOrCreate(key, func() (resource.Resource, error) {
+ if newReadSeeker == nil {
+ var err error
+ newReadSeeker, err = opts.CreateContent()
+ if err != nil {
+ return nil, err
+ }
+ }
return c.rs.NewResource(
resources.ResourceSourceDescriptor{
LazyPublish: true,
GroupIdentity: identity.Anonymous, // All usage of this resource are tracked via its string content.
OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) {
- return hugio.NewReadSeekerNoOpCloserFromString(content), nil
+ return newReadSeeker()
},
- TargetPath: targetPath,
+ TargetPath: opts.TargetPath,
})
})
return r, err
}
+
+// FromString creates a new Resource from a string with the given relative target path.
+func (c *Client) FromString(targetPath, content string) (resource.Resource, error) {
+ return c.FromOpts(Options{
+ TargetPath: targetPath,
+ CreateContent: func() (func() (hugio.ReadSeekCloser, error), error) {
+ return func() (hugio.ReadSeekCloser, error) {
+ return hugio.NewReadSeekerNoOpCloserFromString(content), nil
+ }, nil
+ },
+ })
+}
diff --git a/resources/resource_factories/create/create_integration_test.go b/resources/resource_factories/create/create_integration_test.go
index 61bc17adb..0ed43721c 100644
--- a/resources/resource_factories/create/create_integration_test.go
+++ b/resources/resource_factories/create/create_integration_test.go
@@ -31,18 +31,17 @@ func TestGetRemoteHead(t *testing.T) {
[security.http]
methods = ['(?i)GET|POST|HEAD']
urls = ['.*gohugo\.io.*']
-
-- layouts/index.html --
{{ $url := "https://gohugo.io/img/hugo.png" }}
{{ $opts := dict "method" "head" }}
-{{ with resources.GetRemote $url $opts }}
+{{ with try (resources.GetRemote $url $opts) }}
{{ with .Err }}
{{ errorf "Unable to get remote resource: %s" . }}
- {{ else }}
+ {{ else with .Value }}
Head Content: {{ .Content }}. Head Data: {{ .Data }}
- {{ end }}
-{{ else }}
+ {{ else }}
{{ errorf "Unable to get remote resource: %s" $url }}
+ {{ end }}
{{ end }}
`
@@ -57,7 +56,42 @@ func TestGetRemoteHead(t *testing.T) {
b.AssertFileContent("public/index.html",
"Head Content: .",
- "Head Data: map[ContentLength:18210 ContentType:image/png Status:200 OK StatusCode:200 TransferEncoding:[]]",
+ "Head Data: map[ContentLength:18210 ContentType:image/png Headers:map[] Status:200 OK StatusCode:200 TransferEncoding:[]]",
+ )
+}
+
+func TestGetRemoteResponseHeaders(t *testing.T) {
+ files := `
+-- config.toml --
+[security]
+ [security.http]
+ methods = ['(?i)GET|POST|HEAD']
+ urls = ['.*gohugo\.io.*']
+-- layouts/index.html --
+{{ $url := "https://gohugo.io/img/hugo.png" }}
+{{ $opts := dict "method" "head" "responseHeaders" (slice "X-Frame-Options" "Server") }}
+{{ with try (resources.GetRemote $url $opts) }}
+ {{ with .Err }}
+ {{ errorf "Unable to get remote resource: %s" . }}
+ {{ else with .Value }}
+ Response Headers: {{ .Data.Headers }}
+ {{ else }}
+ {{ errorf "Unable to get remote resource: %s" $url }}
+ {{ end }}
+{{ end }}
+`
+
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ TxtarString: files,
+ },
+ )
+
+ b.Build()
+
+ b.AssertFileContent("public/index.html",
+ "Response Headers: map[Server:[Netlify] X-Frame-Options:[DENY]]",
)
}
@@ -90,18 +124,19 @@ mediaTypes = ['text/plain']
-- layouts/_default/single.html --
{{ $url := printf "%s%s" "URL" .RelPermalink}}
{{ $opts := dict }}
-{{ with resources.GetRemote $url $opts }}
+{{ with try (resources.GetRemote $url $opts) }}
{{ with .Err }}
- {{ errorf "Got Err: %s. Data: %v" . .Data }}
- {{ else }}
+ {{ errorf "Got Err: %s" . }}
+ {{ with .Cause }}{{ errorf "Data: %s" .Data }}{{ end }}
+ {{ else with .Value }}
Content: {{ .Content }}
+ {{ else }}
+ {{ errorf "Unable to get remote resource: %s" $url }}
{{ end }}
-{{ else }}
- {{ errorf "Unable to get remote resource: %s" $url }}
{{ end }}
`
- for i := 0; i < numPages; i++ {
+ for i := range numPages {
filesTemplate += fmt.Sprintf("-- content/post/p%d.md --\n", i)
}
@@ -118,7 +153,7 @@ mediaTypes = ['text/plain']
b.Build()
- for i := 0; i < numPages; i++ {
+ for i := range numPages {
b.AssertFileContent(fmt.Sprintf("public/post/p%d/index.html", i), fmt.Sprintf("Content: Response for /post/p%d/.", i))
}
})
@@ -134,8 +169,7 @@ mediaTypes = ['text/plain']
// This is hard to get stable on GitHub Actions, it sometimes succeeds due to timing issues.
if err != nil {
b.AssertLogContains("Got Err")
- b.AssertLogContains("Retry timeout")
- b.AssertLogContains("ContentLength:0")
+ b.AssertLogContains("retry timeout")
}
})
}
diff --git a/resources/resource_factories/create/remote.go b/resources/resource_factories/create/remote.go
index c2d17e7a5..51fd0bf8e 100644
--- a/resources/resource_factories/create/remote.go
+++ b/resources/resource_factories/create/remote.go
@@ -14,22 +14,29 @@
package create
import (
- "bufio"
"bytes"
+ "context"
"fmt"
"io"
"math/rand"
"mime"
"net/http"
- "net/http/httputil"
"net/url"
"path"
"strings"
"time"
+ gmaps "maps"
+
+ "github.com/gohugoio/httpcache"
+ "github.com/gohugoio/hugo/common/hashing"
+ "github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/hugio"
+ "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/tasks"
"github.com/gohugoio/hugo/common/types"
+ "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources"
@@ -45,18 +52,28 @@ type HTTPError struct {
Body string
}
-func responseToData(res *http.Response, readBody bool) map[string]any {
+func responseToData(res *http.Response, readBody bool, includeHeaders []string) map[string]any {
var body []byte
if readBody {
body, _ = io.ReadAll(res.Body)
}
+ responseHeaders := make(map[string][]string)
+ if len(includeHeaders) > 0 {
+ for k, v := range res.Header {
+ if hstrings.InSlicEqualFold(includeHeaders, k) {
+ responseHeaders[k] = v
+ }
+ }
+ }
+
m := map[string]any{
"StatusCode": res.StatusCode,
"Status": res.Status,
"TransferEncoding": res.TransferEncoding,
"ContentLength": res.ContentLength,
"ContentType": res.Header.Get("Content-Type"),
+ "Headers": responseHeaders,
}
if readBody {
@@ -66,7 +83,7 @@ func responseToData(res *http.Response, readBody bool) map[string]any {
return m
}
-func toHTTPError(err error, res *http.Response, readBody bool) *HTTPError {
+func toHTTPError(err error, res *http.Response, readBody bool, responseHeaders []string) *HTTPError {
if err == nil {
panic("err is nil")
}
@@ -79,7 +96,7 @@ func toHTTPError(err error, res *http.Response, readBody bool) *HTTPError {
return &HTTPError{
error: err,
- Data: responseToData(res, readBody),
+ Data: responseToData(res, readBody, responseHeaders),
}
}
@@ -92,6 +109,60 @@ var temporaryHTTPStatusCodes = map[int]bool{
504: true,
}
+func (c *Client) configurePollingIfEnabled(uri, optionsKey string, getRes func() (*http.Response, error)) {
+ if c.remoteResourceChecker == nil {
+ return
+ }
+
+ // Set up polling for changes to this resource.
+ pollingConfig := c.httpCacheConfig.PollConfigFor(uri)
+ if pollingConfig.IsZero() || pollingConfig.Config.Disable {
+ return
+ }
+
+ if c.remoteResourceChecker.Has(optionsKey) {
+ return
+ }
+
+ var lastChange time.Time
+ c.remoteResourceChecker.Add(optionsKey,
+ tasks.Func{
+ IntervalLow: pollingConfig.Config.Low,
+ IntervalHigh: pollingConfig.Config.High,
+ F: func(interval time.Duration) (time.Duration, error) {
+ start := time.Now()
+ defer func() {
+ duration := time.Since(start)
+ c.rs.Logger.Debugf("Polled remote resource for changes in %13s. Interval: %4s (low: %4s high: %4s) resource: %q ", duration, interval, pollingConfig.Config.Low, pollingConfig.Config.High, uri)
+ }()
+ // TODO(bep) figure out a ways to remove unused tasks.
+ res, err := getRes()
+ if err != nil {
+ return pollingConfig.Config.High, err
+ }
+ // The caching is delayed until the body is read.
+ io.Copy(io.Discard, res.Body)
+ res.Body.Close()
+ x1, x2 := res.Header.Get(httpcache.XETag1), res.Header.Get(httpcache.XETag2)
+ if x1 != x2 {
+ lastChange = time.Now()
+ c.remoteResourceLogger.Logf("detected change in remote resource %q", uri)
+ c.rs.Rebuilder.SignalRebuild(identity.StringIdentity(optionsKey))
+ }
+
+ if time.Since(lastChange) < 10*time.Second {
+ // The user is typing, check more often.
+ return 0, nil
+ }
+
+ // Increase the interval to avoid hammering the server.
+ interval += 1 * time.Second
+
+ return interval, nil
+ },
+ })
+}
+
// FromRemote expects one or n-parts of a URL to a resource
// If you provide multiple parts they will be joined together to the final URL.
func (c *Client) FromRemote(uri string, optionsm map[string]any) (resource.Resource, error) {
@@ -101,168 +172,139 @@ func (c *Client) FromRemote(uri string, optionsm map[string]any) (resource.Resou
}
method := "GET"
- if s, ok := maps.LookupEqualFold(optionsm, "method"); ok {
+ if s, _, ok := maps.LookupEqualFold(optionsm, "method"); ok {
method = strings.ToUpper(s.(string))
}
isHeadMethod := method == "HEAD"
- resourceID := calculateResourceID(uri, optionsm)
+ optionsm = gmaps.Clone(optionsm)
+ userKey, optionsKey := remoteResourceKeys(uri, optionsm)
- _, httpResponse, err := c.cacheGetResource.GetOrCreate(resourceID, func() (io.ReadCloser, error) {
+ // A common pattern is to use the key in the options map as
+ // a way to control cache eviction,
+ // so make sure we use any user provided key as the file cache key,
+ // but the auto generated and more stable key for everything else.
+ filecacheKey := userKey
+
+ return c.rs.ResourceCache.CacheResourceRemote.GetOrCreate(optionsKey, func(key string) (resource.Resource, error) {
options, err := decodeRemoteOptions(optionsm)
if err != nil {
return nil, fmt.Errorf("failed to decode options for resource %s: %w", uri, err)
}
+
if err := c.validateFromRemoteArgs(uri, options); err != nil {
return nil, err
}
- var (
- start time.Time
- nextSleep = time.Duration((rand.Intn(1000) + 100)) * time.Millisecond
- nextSleepLimit = time.Duration(5) * time.Second
- )
+ getRes := func() (*http.Response, error) {
+ ctx := context.Background()
+ ctx = c.resourceIDDispatcher.Set(ctx, filecacheKey)
- for {
- b, retry, err := func() ([]byte, bool, error) {
- req, err := options.NewRequest(uri)
- if err != nil {
- return nil, false, fmt.Errorf("failed to create request for resource %s: %w", uri, err)
- }
-
- res, err := c.httpClient.Do(req)
- if err != nil {
- return nil, false, err
- }
- defer res.Body.Close()
-
- if res.StatusCode != http.StatusNotFound {
- if res.StatusCode < 200 || res.StatusCode > 299 {
- return nil, temporaryHTTPStatusCodes[res.StatusCode], toHTTPError(fmt.Errorf("failed to fetch remote resource: %s", http.StatusText(res.StatusCode)), res, !isHeadMethod)
- }
- }
-
- b, err := httputil.DumpResponse(res, true)
- if err != nil {
- return nil, false, toHTTPError(err, res, !isHeadMethod)
- }
-
- return b, false, nil
- }()
+ req, err := options.NewRequest(uri)
if err != nil {
- if retry {
- if start.IsZero() {
- start = time.Now()
- } else if d := time.Since(start) + nextSleep; d >= c.rs.Cfg.Timeout() {
- c.rs.Logger.Errorf("Retry timeout (configured to %s) fetching remote resource.", c.rs.Cfg.Timeout())
- return nil, err
- }
- time.Sleep(nextSleep)
- if nextSleep < nextSleepLimit {
- nextSleep *= 2
- }
- continue
- }
- return nil, err
+ return nil, fmt.Errorf("failed to create request for resource %s: %w", uri, err)
}
- return hugio.ToReadCloser(bytes.NewReader(b)), nil
+ req = req.WithContext(ctx)
+ return c.httpClient.Do(req)
}
- })
- if err != nil {
- return nil, err
- }
- defer httpResponse.Close()
- res, err := http.ReadResponse(bufio.NewReader(httpResponse), nil)
- if err != nil {
- return nil, err
- }
- defer res.Body.Close()
-
- if res.StatusCode == http.StatusNotFound {
- // Not found. This matches how looksup for local resources work.
- return nil, nil
- }
-
- var (
- body []byte
- mediaType media.Type
- )
- // A response to a HEAD method should not have a body. If it has one anyway, that body must be ignored.
- // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD
- if !isHeadMethod && res.Body != nil {
- body, err = io.ReadAll(res.Body)
+ res, err := getRes()
if err != nil {
- return nil, fmt.Errorf("failed to read remote resource %q: %w", uri, err)
+ return nil, err
}
- }
+ defer res.Body.Close()
- filename := path.Base(rURL.Path)
- if _, params, _ := mime.ParseMediaType(res.Header.Get("Content-Disposition")); params != nil {
- if _, ok := params["filename"]; ok {
- filename = params["filename"]
+ c.configurePollingIfEnabled(uri, optionsKey, getRes)
+
+ if res.StatusCode == http.StatusNotFound {
+ // Not found. This matches how lookups for local resources work.
+ return nil, nil
}
- }
- contentType := res.Header.Get("Content-Type")
-
- // For HEAD requests we have no body to work with, so we need to use the Content-Type header.
- if isHeadMethod || c.rs.ExecHelper.Sec().HTTP.MediaTypes.Accept(contentType) {
- var found bool
- mediaType, found = c.rs.MediaTypes().GetByType(contentType)
- if !found {
- // A media type not configured in Hugo, just create one from the content type string.
- mediaType, _ = media.FromString(contentType)
+ if res.StatusCode < 200 || res.StatusCode > 299 {
+ return nil, toHTTPError(fmt.Errorf("failed to fetch remote resource from '%s': %s", uri, http.StatusText(res.StatusCode)), res, !isHeadMethod, options.ResponseHeaders)
}
- }
- if mediaType.IsZero() {
-
- var extensionHints []string
-
- // mime.ExtensionsByType gives a long list of extensions for text/plain,
- // just use ".txt".
- if strings.HasPrefix(contentType, "text/plain") {
- extensionHints = []string{".txt"}
- } else {
- exts, _ := mime.ExtensionsByType(contentType)
- if exts != nil {
- extensionHints = exts
+ var (
+ body []byte
+ mediaType media.Type
+ )
+ // A response to a HEAD method should not have a body. If it has one anyway, that body must be ignored.
+ // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD
+ if !isHeadMethod && res.Body != nil {
+ body, err = io.ReadAll(res.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read remote resource %q: %w", uri, err)
}
}
- // Look for a file extension. If it's .txt, look for a more specific.
- if extensionHints == nil || extensionHints[0] == ".txt" {
- if ext := path.Ext(filename); ext != "" {
- extensionHints = []string{ext}
+ filename := path.Base(rURL.Path)
+ if _, params, _ := mime.ParseMediaType(res.Header.Get("Content-Disposition")); params != nil {
+ if _, ok := params["filename"]; ok {
+ filename = params["filename"]
}
}
- // Now resolve the media type primarily using the content.
- mediaType = media.FromContent(c.rs.MediaTypes(), extensionHints, body)
+ contentType := res.Header.Get("Content-Type")
- }
+ // For HEAD requests we have no body to work with, so we need to use the Content-Type header.
+ if isHeadMethod || c.rs.ExecHelper.Sec().HTTP.MediaTypes.Accept(contentType) {
+ var found bool
+ mediaType, found = c.rs.MediaTypes().GetByType(contentType)
+ if !found {
+ // A media type not configured in Hugo, just create one from the content type string.
+ mediaType, _ = media.FromString(contentType)
+ }
+ }
- if mediaType.IsZero() {
- return nil, fmt.Errorf("failed to resolve media type for remote resource %q", uri)
- }
+ if mediaType.IsZero() {
- resourceID = filename[:len(filename)-len(path.Ext(filename))] + "_" + resourceID + mediaType.FirstSuffix.FullSuffix
- data := responseToData(res, false)
+ var extensionHints []string
- return c.rs.NewResource(
- resources.ResourceSourceDescriptor{
- MediaType: mediaType,
- Data: data,
- GroupIdentity: identity.StringIdentity(resourceID),
- LazyPublish: true,
- OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) {
- return hugio.NewReadSeekerNoOpCloser(bytes.NewReader(body)), nil
- },
- TargetPath: resourceID,
- })
+ // mime.ExtensionsByType gives a long list of extensions for text/plain,
+ // just use ".txt".
+ if strings.HasPrefix(contentType, "text/plain") {
+ extensionHints = []string{".txt"}
+ } else {
+ exts, _ := mime.ExtensionsByType(contentType)
+ if exts != nil {
+ extensionHints = exts
+ }
+ }
+
+ // Look for a file extension. If it's .txt, look for a more specific.
+ if extensionHints == nil || extensionHints[0] == ".txt" {
+ if ext := path.Ext(filename); ext != "" {
+ extensionHints = []string{ext}
+ }
+ }
+
+ // Now resolve the media type primarily using the content.
+ mediaType = media.FromContent(c.rs.MediaTypes(), extensionHints, body)
+
+ }
+
+ if mediaType.IsZero() {
+ return nil, fmt.Errorf("failed to resolve media type for remote resource %q", uri)
+ }
+
+ userKey = filename[:len(filename)-len(path.Ext(filename))] + "_" + userKey + mediaType.FirstSuffix.FullSuffix
+ data := responseToData(res, false, options.ResponseHeaders)
+
+ return c.rs.NewResource(
+ resources.ResourceSourceDescriptor{
+ MediaType: mediaType,
+ Data: data,
+ GroupIdentity: identity.StringIdentity(optionsKey),
+ LazyPublish: true,
+ OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) {
+ return hugio.NewReadSeekerNoOpCloser(bytes.NewReader(body)), nil
+ },
+ TargetPath: userKey,
+ })
+ })
}
func (c *Client) validateFromRemoteArgs(uri string, options fromRemoteOptions) error {
@@ -277,11 +319,17 @@ func (c *Client) validateFromRemoteArgs(uri string, options fromRemoteOptions) e
return nil
}
-func calculateResourceID(uri string, optionsm map[string]any) string {
- if key, found := maps.LookupEqualFold(optionsm, "key"); found {
- return identity.HashString(key)
+func remoteResourceKeys(uri string, optionsm map[string]any) (string, string) {
+ var userKey string
+ if key, k, found := maps.LookupEqualFold(optionsm, "key"); found {
+ userKey = hashing.HashString(key)
+ delete(optionsm, k)
}
- return identity.HashString(uri, optionsm)
+ optionsKey := hashing.HashString(uri, optionsm)
+ if userKey == "" {
+ userKey = optionsKey
+ }
+ return userKey, optionsKey
}
func addDefaultHeaders(req *http.Request) {
@@ -308,9 +356,10 @@ func hasHeaderKey(m http.Header, key string) bool {
}
type fromRemoteOptions struct {
- Method string
- Headers map[string]any
- Body []byte
+ Method string
+ Headers map[string]any
+ Body []byte
+ ResponseHeaders []string
}
func (o fromRemoteOptions) BodyReader() io.Reader {
@@ -350,3 +399,71 @@ func decodeRemoteOptions(optionsm map[string]any) (fromRemoteOptions, error) {
return options, nil
}
+
+var _ http.RoundTripper = (*transport)(nil)
+
+type transport struct {
+ Cfg config.AllProvider
+ Logger loggers.Logger
+}
+
+func (t *transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
+ defer func() {
+ if resp != nil && resp.StatusCode != http.StatusNotFound && resp.StatusCode != http.StatusNotModified {
+ t.Logger.Debugf("Fetched remote resource: %s", req.URL.String())
+ }
+ }()
+
+ var (
+ start time.Time
+ nextSleep = time.Duration((rand.Intn(1000) + 100)) * time.Millisecond
+ nextSleepLimit = time.Duration(5) * time.Second
+ retry bool
+ )
+
+ for {
+ resp, retry, err = func() (*http.Response, bool, error) {
+ resp2, err := http.DefaultTransport.RoundTrip(req)
+ if err != nil {
+ return resp2, false, err
+ }
+
+ if resp2.StatusCode != http.StatusNotFound && resp2.StatusCode != http.StatusNotModified {
+ if resp2.StatusCode < 200 || resp2.StatusCode > 299 {
+ return resp2, temporaryHTTPStatusCodes[resp2.StatusCode], nil
+ }
+ }
+ return resp2, false, nil
+ }()
+
+ if retry {
+ if start.IsZero() {
+ start = time.Now()
+ } else if d := time.Since(start) + nextSleep; d >= t.Cfg.Timeout() {
+ msg := ""
+ if resp != nil {
+ msg = resp.Status
+ }
+ err := toHTTPError(fmt.Errorf("retry timeout (configured to %s) fetching remote resource: %s", t.Cfg.Timeout(), msg), resp, req.Method != "HEAD", nil)
+ return resp, err
+ }
+ time.Sleep(nextSleep)
+ if nextSleep < nextSleepLimit {
+ nextSleep *= 2
+ }
+ continue
+ }
+
+ return
+ }
+}
+
+// We need to send the redirect responses back to the HTTP client from RoundTrip,
+// but we don't want to cache them.
+func shouldCache(statusCode int) bool {
+ switch statusCode {
+ case http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect, http.StatusPermanentRedirect:
+ return false
+ }
+ return true
+}
diff --git a/resources/resource_factories/create/remote_test.go b/resources/resource_factories/create/remote_test.go
index 21314ad34..293845107 100644
--- a/resources/resource_factories/create/remote_test.go
+++ b/resources/resource_factories/create/remote_test.go
@@ -115,15 +115,22 @@ func TestOptionsNewRequest(t *testing.T) {
c.Assert(req.Header["User-Agent"], qt.DeepEquals, []string{"foo"})
}
-func TestCalculateResourceID(t *testing.T) {
+func TestRemoteResourceKeys(t *testing.T) {
t.Parallel()
c := qt.New(t)
- c.Assert(calculateResourceID("foo", nil), qt.Equals, "5917621528921068675")
- c.Assert(calculateResourceID("foo", map[string]any{"bar": "baz"}), qt.Equals, "7294498335241413323")
+ check := func(uri string, optionsm map[string]any, expect1, expect2 string) {
+ c.Helper()
+ got1, got2 := remoteResourceKeys(uri, optionsm)
+ c.Assert(got1, qt.Equals, expect1)
+ c.Assert(got2, qt.Equals, expect2)
+ }
- c.Assert(calculateResourceID("foo", map[string]any{"key": "1234", "bar": "baz"}), qt.Equals, "14904296279238663669")
- c.Assert(calculateResourceID("asdf", map[string]any{"key": "1234", "bar": "asdf"}), qt.Equals, "14904296279238663669")
- c.Assert(calculateResourceID("asdf", map[string]any{"key": "12345", "bar": "asdf"}), qt.Equals, "12191037851845371770")
+ check("foo", nil, "7763396052142361238", "7763396052142361238")
+ check("foo", map[string]any{"bar": "baz"}, "5783339285578751849", "5783339285578751849")
+ check("foo", map[string]any{"key": "1234", "bar": "baz"}, "15578353952571222948", "5783339285578751849")
+ check("foo", map[string]any{"key": "12345", "bar": "baz"}, "14335752410685132726", "5783339285578751849")
+ check("asdf", map[string]any{"key": "1234", "bar": "asdf"}, "15578353952571222948", "15615023578599429261")
+ check("asdf", map[string]any{"key": "12345", "bar": "asdf"}, "14335752410685132726", "15615023578599429261")
}
diff --git a/resources/resource_metadata.go b/resources/resource_metadata.go
index 659ce81f8..2a4faa315 100644
--- a/resources/resource_metadata.go
+++ b/resources/resource_metadata.go
@@ -15,16 +15,20 @@ package resources
import (
"fmt"
+ "path/filepath"
"strconv"
"strings"
"github.com/gohugoio/hugo/hugofs/glob"
"github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/gohugoio/hugo/resources/resource"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/paths"
+ maps0 "maps"
)
var (
@@ -82,15 +86,39 @@ func (r *metaResource) setName(name string) {
func (r *metaResource) updateParams(params map[string]any) {
if r.params == nil {
- r.params = make(map[string]interface{})
- }
- for k, v := range params {
- r.params[k] = v
+ r.params = make(map[string]any)
}
+ maps0.Copy(r.params, params)
r.changed = true
}
-func CloneWithMetadataIfNeeded(m []map[string]any, r resource.Resource) resource.Resource {
+// cloneWithMetadataFromResourceConfigIfNeeded clones the given resource with the given metadata if the resource supports it.
+func cloneWithMetadataFromResourceConfigIfNeeded(rc *pagemeta.ResourceConfig, r resource.Resource) resource.Resource {
+ wmp, ok := r.(resource.WithResourceMetaProvider)
+ if !ok {
+ return r
+ }
+
+ if rc.Name == "" && rc.Title == "" && len(rc.Params) == 0 {
+ // No metadata.
+ return r
+ }
+
+ if rc.Title == "" {
+ rc.Title = rc.Name
+ }
+
+ wrapped := &metaResource{
+ name: rc.Name,
+ title: rc.Title,
+ params: rc.Params,
+ }
+
+ return wmp.WithResourceMeta(wrapped)
+}
+
+// CloneWithMetadataFromMapIfNeeded clones the given resource with the given metadata if the resource supports it.
+func CloneWithMetadataFromMapIfNeeded(m []map[string]any, r resource.Resource) resource.Resource {
wmp, ok := r.(resource.WithResourceMetaProvider)
if !ok {
return r
@@ -145,6 +173,8 @@ func assignMetadata(metadata []map[string]any, ma *metaResource) error {
name, found := meta["name"]
if found {
name := cast.ToString(name)
+ // Bundled resources in sub folders are relative paths with forward slashes. Make sure any renames also matches that format:
+ name = paths.TrimLeading(filepath.ToSlash(name))
if !nameCounterFound {
nameCounterFound = strings.Contains(name, counterPlaceHolder)
}
diff --git a/resources/resource_spec.go b/resources/resource_spec.go
index bd04846ed..806a5ff8d 100644
--- a/resources/resource_spec.go
+++ b/resources/resource_spec.go
@@ -14,19 +14,23 @@
package resources
import (
+ "fmt"
"path"
"sync"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/allconfig"
+ "github.com/gohugoio/hugo/lazy"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources/internal"
"github.com/gohugoio/hugo/resources/jsconfig"
+ "github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/paths"
+ "github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/identity"
@@ -39,7 +43,6 @@ import (
"github.com/gohugoio/hugo/resources/images"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
- "github.com/gohugoio/hugo/tpl"
)
func NewSpec(
@@ -51,11 +54,15 @@ func NewSpec(
logger loggers.Logger,
errorHandler herrors.ErrorSender,
execHelper *hexec.Exec,
+ buildClosers types.CloseAdder,
+ rebuilder identity.SignalRebuilder,
) (*Spec, error) {
conf := s.Cfg.GetConfig().(*allconfig.Config)
imgConfig := conf.Imaging
- imaging, err := images.NewImageProcessor(imgConfig)
+ imagesWarnl := logger.WarnCommand("images")
+
+ imaging, err := images.NewImageProcessor(imagesWarnl, imgConfig)
if err != nil {
return nil, err
}
@@ -85,10 +92,12 @@ func NewSpec(
}
rs := &Spec{
- PathSpec: s,
- Logger: logger,
- ErrorSender: errorHandler,
- imaging: imaging,
+ PathSpec: s,
+ Logger: logger,
+ ErrorSender: errorHandler,
+ BuildClosers: buildClosers,
+ Rebuilder: rebuilder,
+ imaging: imaging,
ImageCache: newImageCache(
fileCaches.ImageCache(),
memCache,
@@ -109,10 +118,10 @@ func NewSpec(
type Spec struct {
*helpers.PathSpec
- Logger loggers.Logger
- ErrorSender herrors.ErrorSender
-
- TextTemplates tpl.TemplateParseFinder
+ Logger loggers.Logger
+ ErrorSender herrors.ErrorSender
+ BuildClosers types.CloseAdder
+ Rebuilder identity.SignalRebuilder
Permalinks page.PermalinkExpander
@@ -143,6 +152,16 @@ type PostBuildAssets struct {
JSConfigBuilder *jsconfig.Builder
}
+func (r *Spec) NewResourceWrapperFromResourceConfig(rc *pagemeta.ResourceConfig) (resource.Resource, error) {
+ content := rc.Content
+ switch r := content.Value.(type) {
+ case resource.Resource:
+ return cloneWithMetadataFromResourceConfigIfNeeded(rc, r), nil
+ default:
+ return nil, fmt.Errorf("failed to create resource for path %q, expected a resource.Resource, got %T", rc.PathInfo.Path(), content.Value)
+ }
+}
+
// NewResource creates a new Resource from the given ResourceSourceDescriptor.
func (r *Spec) NewResource(rd ResourceSourceDescriptor) (resource.Resource, error) {
if err := rd.init(r); err != nil {
@@ -162,28 +181,33 @@ func (r *Spec) NewResource(rd ResourceSourceDescriptor) (resource.Resource, erro
TargetBasePaths: rd.TargetBasePaths,
}
- gr := &genericResource{
- Staler: &AtomicStaler{},
- h: &resourceHash{},
- publishInit: &sync.Once{},
- paths: rp,
- spec: r,
- sd: rd,
- params: make(map[string]any),
- name: rd.NameOriginal,
- title: rd.NameOriginal,
+ isImage := rd.MediaType.MainType == "image"
+ var imgFormat images.Format
+ if isImage {
+ imgFormat, isImage = images.ImageFormatFromMediaSubType(rd.MediaType.SubType)
}
- if rd.MediaType.MainType == "image" {
- imgFormat, ok := images.ImageFormatFromMediaSubType(rd.MediaType.SubType)
- if ok {
- ir := &imageResource{
- Image: images.NewImage(imgFormat, r.imaging, nil, gr),
- baseResource: gr,
- }
- ir.root = ir
- return newResourceAdapter(gr.spec, rd.LazyPublish, ir), nil
+ gr := &genericResource{
+ Staler: &AtomicStaler{},
+ h: &resourceHash{},
+ publishInit: &lazy.OnceMore{},
+ keyInit: &sync.Once{},
+ includeHashInKey: isImage,
+ paths: rp,
+ spec: r,
+ sd: rd,
+ params: rd.Params,
+ name: rd.NameOriginal,
+ title: rd.Title,
+ }
+
+ if isImage {
+ ir := &imageResource{
+ Image: images.NewImage(imgFormat, r.imaging, nil, gr),
+ baseResource: gr,
}
+ ir.root = ir
+ return newResourceAdapter(gr.spec, rd.LazyPublish, ir), nil
}
diff --git a/identity/identityhash_test.go b/resources/resource_test.go
similarity index 55%
rename from identity/identityhash_test.go
rename to resources/resource_test.go
index 1ecaf7612..d07770456 100644
--- a/identity/identityhash_test.go
+++ b/resources/resource_test.go
@@ -11,34 +11,44 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package identity
+package resources
import (
+ "os"
"testing"
qt "github.com/frankban/quicktest"
)
-func TestHashString(t *testing.T) {
+func TestAtomicStaler(t *testing.T) {
c := qt.New(t)
- c.Assert(HashString("a", "b"), qt.Equals, "2712570657419664240")
- c.Assert(HashString("ab"), qt.Equals, "590647783936702392")
+ type test struct {
+ AtomicStaler
+ }
- var vals []any = []any{"a", "b", tstKeyer{"c"}}
+ var v test
- c.Assert(HashString(vals...), qt.Equals, "12599484872364427450")
- c.Assert(vals[2], qt.Equals, tstKeyer{"c"})
+ c.Assert(v.StaleVersion(), qt.Equals, uint32(0))
+ v.MarkStale()
+ c.Assert(v.StaleVersion(), qt.Equals, uint32(1))
+ v.MarkStale()
+ c.Assert(v.StaleVersion(), qt.Equals, uint32(2))
}
-type tstKeyer struct {
- key string
-}
+func BenchmarkHashImage(b *testing.B) {
+ f, err := os.Open("testdata/sunset.jpg")
+ if err != nil {
+ b.Fatal(err)
+ }
+ defer f.Close()
-func (t tstKeyer) Key() string {
- return t.key
-}
-
-func (t tstKeyer) String() string {
- return "key: " + t.key
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, _, err := hashImage(f)
+ if err != nil {
+ b.Fatal(err)
+ }
+ f.Seek(0, 0)
+ }
}
diff --git a/resources/resource_transformers/cssjs/inline_imports.go b/resources/resource_transformers/cssjs/inline_imports.go
new file mode 100644
index 000000000..98e3292cd
--- /dev/null
+++ b/resources/resource_transformers/cssjs/inline_imports.go
@@ -0,0 +1,247 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cssjs
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "path"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/common/text"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/identity"
+ "github.com/spf13/afero"
+)
+
+const importIdentifier = "@import"
+
+var (
+ cssSyntaxErrorRe = regexp.MustCompile(`> (\d+) \|`)
+ shouldImportRe = regexp.MustCompile(`^@import ["'](.*?)["'];?\s*(/\*.*\*/)?$`)
+)
+
+type fileOffset struct {
+ Filename string
+ Offset int
+}
+
+type importResolver struct {
+ r io.Reader
+ inPath string
+ opts InlineImports
+
+ contentSeen map[string]bool
+ dependencyManager identity.Manager
+ linemap map[int]fileOffset
+ fs afero.Fs
+ logger loggers.Logger
+}
+
+func newImportResolver(r io.Reader, inPath string, opts InlineImports, fs afero.Fs, logger loggers.Logger, dependencyManager identity.Manager) *importResolver {
+ return &importResolver{
+ r: r,
+ dependencyManager: dependencyManager,
+ inPath: inPath,
+ fs: fs, logger: logger,
+ linemap: make(map[int]fileOffset), contentSeen: make(map[string]bool),
+ opts: opts,
+ }
+}
+
+func (imp *importResolver) contentHash(filename string) ([]byte, string) {
+ b, err := afero.ReadFile(imp.fs, filename)
+ if err != nil {
+ return nil, ""
+ }
+ h := sha256.New()
+ h.Write(b)
+ return b, hex.EncodeToString(h.Sum(nil))
+}
+
+func (imp *importResolver) importRecursive(
+ lineNum int,
+ content string,
+ inPath string,
+) (int, string, error) {
+ basePath := path.Dir(inPath)
+
+ var replacements []string
+ lines := strings.Split(content, "\n")
+
+ trackLine := func(i, offset int, line string) {
+ // TODO(bep) this is not very efficient.
+ imp.linemap[i+lineNum] = fileOffset{Filename: inPath, Offset: offset}
+ }
+
+ i := 0
+ for offset, line := range lines {
+ i++
+ lineTrimmed := strings.TrimSpace(line)
+ column := strings.Index(line, lineTrimmed)
+ line = lineTrimmed
+
+ if !imp.shouldImport(line) {
+ trackLine(i, offset, line)
+ } else {
+ path := strings.Trim(strings.TrimPrefix(line, importIdentifier), " \"';")
+ filename := filepath.Join(basePath, path)
+ imp.dependencyManager.AddIdentity(identity.CleanStringIdentity(filename))
+ importContent, hash := imp.contentHash(filename)
+
+ if importContent == nil {
+ if imp.opts.SkipInlineImportsNotFound {
+ trackLine(i, offset, line)
+ continue
+ }
+ pos := text.Position{
+ Filename: inPath,
+ LineNumber: offset + 1,
+ ColumnNumber: column + 1,
+ }
+ return 0, "", herrors.NewFileErrorFromFileInPos(fmt.Errorf("failed to resolve CSS @import \"%s\"", filename), pos, imp.fs, nil)
+ }
+
+ i--
+
+ if imp.contentSeen[hash] {
+ i++
+ // Just replace the line with an empty string.
+ replacements = append(replacements, []string{line, ""}...)
+ trackLine(i, offset, "IMPORT")
+ continue
+ }
+
+ imp.contentSeen[hash] = true
+
+ // Handle recursive imports.
+ l, nested, err := imp.importRecursive(i+lineNum, string(importContent), filepath.ToSlash(filename))
+ if err != nil {
+ return 0, "", err
+ }
+
+ trackLine(i, offset, line)
+
+ i += l
+
+ importContent = []byte(nested)
+
+ replacements = append(replacements, []string{line, string(importContent)}...)
+ }
+ }
+
+ if len(replacements) > 0 {
+ repl := strings.NewReplacer(replacements...)
+ content = repl.Replace(content)
+ }
+
+ return i, content, nil
+}
+
+func (imp *importResolver) resolve() (io.Reader, error) {
+ content, err := io.ReadAll(imp.r)
+ if err != nil {
+ return nil, err
+ }
+
+ contents := string(content)
+
+ _, newContent, err := imp.importRecursive(0, contents, imp.inPath)
+ if err != nil {
+ return nil, err
+ }
+
+ return strings.NewReader(newContent), nil
+}
+
+// See https://www.w3schools.com/cssref/pr_import_rule.asp
+// We currently only support simple file imports, no urls, no media queries.
+// So this is OK:
+//
+// @import "navigation.css";
+//
+// This is not:
+//
+// @import url("navigation.css");
+// @import "mobstyle.css" screen and (max-width: 768px);
+func (imp *importResolver) shouldImport(s string) bool {
+ if !strings.HasPrefix(s, importIdentifier) {
+ return false
+ }
+ if strings.Contains(s, "url(") {
+ return false
+ }
+
+ m := shouldImportRe.FindStringSubmatch(s)
+ if m == nil {
+ return false
+ }
+
+ if len(m) != 3 {
+ return false
+ }
+
+ if tailwindImportExclude(m[1]) {
+ return false
+ }
+
+ return true
+}
+
+func (imp *importResolver) toFileError(output string) error {
+ inErr := errors.New(output)
+
+ match := cssSyntaxErrorRe.FindStringSubmatch(output)
+ if match == nil {
+ return inErr
+ }
+
+ lineNum, err := strconv.Atoi(match[1])
+ if err != nil {
+ return inErr
+ }
+
+ file, ok := imp.linemap[lineNum]
+ if !ok {
+ return inErr
+ }
+
+ fi, err := imp.fs.Stat(file.Filename)
+ if err != nil {
+ return inErr
+ }
+
+ meta := fi.(hugofs.FileMetaInfo).Meta()
+ realFilename := meta.Filename
+ f, err := meta.Open()
+ if err != nil {
+ return inErr
+ }
+ defer f.Close()
+
+ ferr := herrors.NewFileErrorFromName(inErr, realFilename)
+ pos := ferr.Position()
+ pos.LineNumber = file.Offset + 1
+ return ferr.UpdatePosition(pos).UpdateContent(f, nil)
+
+ // return herrors.NewFileErrorFromFile(inErr, file.Filename, realFilename, hugofs.Os, herrors.SimpleLineMatcher)
+}
diff --git a/resources/resource_transformers/postcss/postcss_test.go b/resources/resource_transformers/cssjs/inline_imports_test.go
similarity index 85%
rename from resources/resource_transformers/postcss/postcss_test.go
rename to resources/resource_transformers/cssjs/inline_imports_test.go
index 1edaaaaf5..9bcb7f9a3 100644
--- a/resources/resource_transformers/postcss/postcss_test.go
+++ b/resources/resource_transformers/cssjs/inline_imports_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Hugo Authors. All rights reserved.
+// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package postcss
+package cssjs
import (
"regexp"
@@ -32,14 +32,14 @@ import (
// Issue 6166
func TestDecodeOptions(t *testing.T) {
c := qt.New(t)
- opts1, err := decodeOptions(map[string]any{
+ opts1, err := decodePostCSSOptions(map[string]any{
"no-map": true,
})
c.Assert(err, qt.IsNil)
c.Assert(opts1.NoMap, qt.Equals, true)
- opts2, err := decodeOptions(map[string]any{
+ opts2, err := decodePostCSSOptions(map[string]any{
"noMap": true,
})
@@ -67,6 +67,16 @@ func TestShouldImport(t *testing.T) {
}
}
+func TestShouldImportExcludes(t *testing.T) {
+ c := qt.New(t)
+ var imp *importResolver
+
+ c.Assert(imp.shouldImport(`@import "navigation.css";`), qt.Equals, true)
+ c.Assert(imp.shouldImport(`@import "tailwindcss";`), qt.Equals, false)
+ c.Assert(imp.shouldImport(`@import "tailwindcss.css";`), qt.Equals, true)
+ c.Assert(imp.shouldImport(`@import "tailwindcss/preflight";`), qt.Equals, false)
+}
+
func TestImportResolver(t *testing.T) {
c := qt.New(t)
fs := afero.NewMemMapFs()
@@ -95,7 +105,7 @@ LOCAL_STYLE
imp := newImportResolver(
mainStyles,
"styles.css",
- Options{},
+ InlineImports{},
fs, loggers.NewDefault(),
identity.NopManager,
)
@@ -153,7 +163,7 @@ LOCAL_STYLE
imp := newImportResolver(
strings.NewReader(mainStyles),
"styles.css",
- Options{},
+ InlineImports{},
fs, logger,
identity.NopManager,
)
diff --git a/resources/resource_transformers/postcss/postcss.go b/resources/resource_transformers/cssjs/postcss.go
similarity index 50%
rename from resources/resource_transformers/postcss/postcss.go
rename to resources/resource_transformers/cssjs/postcss.go
index 9015e120d..98bdc9249 100644
--- a/resources/resource_transformers/postcss/postcss.go
+++ b/resources/resource_transformers/cssjs/postcss.go
@@ -11,32 +11,23 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package postcss
+// Package cssjs provides resource transformations backed by some popular JS based frameworks.
+package cssjs
import (
"bytes"
- "crypto/sha256"
- "encoding/hex"
- "errors"
"fmt"
"io"
- "path"
"path/filepath"
- "regexp"
- "strconv"
"strings"
"github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/loggers"
- "github.com/gohugoio/hugo/common/text"
- "github.com/gohugoio/hugo/hugofs"
- "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/resources/internal"
- "github.com/spf13/afero"
"github.com/spf13/cast"
"github.com/mitchellh/mapstructure"
@@ -46,19 +37,12 @@ import (
"github.com/gohugoio/hugo/resources/resource"
)
-const importIdentifier = "@import"
-
-var (
- cssSyntaxErrorRe = regexp.MustCompile(`> (\d+) \|`)
- shouldImportRe = regexp.MustCompile(`^@import ["'].*["'];?\s*(/\*.*\*/)?$`)
-)
-
-// New creates a new Client with the given specification.
-func New(rs *resources.Spec) *Client {
- return &Client{rs: rs}
+// NewPostCSSClient creates a new PostCSSClient with the given specification.
+func NewPostCSSClient(rs *resources.Spec) *PostCSSClient {
+ return &PostCSSClient{rs: rs}
}
-func decodeOptions(m map[string]any) (opts Options, err error) {
+func decodePostCSSOptions(m map[string]any) (opts PostCSSOptions, err error) {
if m == nil {
return
}
@@ -74,23 +58,17 @@ func decodeOptions(m map[string]any) (opts Options, err error) {
return
}
-// Client is the client used to do PostCSS transformations.
-type Client struct {
+// PostCSSClient is the client used to do PostCSS transformations.
+type PostCSSClient struct {
rs *resources.Spec
}
// Process transforms the given Resource with the PostCSS processor.
-func (c *Client) Process(res resources.ResourceTransformer, options map[string]any) (resource.Resource, error) {
+func (c *PostCSSClient) Process(res resources.ResourceTransformer, options map[string]any) (resource.Resource, error) {
return res.Transform(&postcssTransformation{rs: c.rs, optionsm: options})
}
-// Some of the options from https://github.com/postcss/postcss-cli
-type Options struct {
- // Set a custom path to look for a config file.
- Config string
-
- NoMap bool // Disable the default inline sourcemaps
-
+type InlineImports struct {
// Enable inlining of @import statements.
// Does so recursively, but currently once only per file;
// that is, it's not possible to import the same file in
@@ -99,11 +77,26 @@ type Options struct {
// so you can have @import anywhere in the file.
InlineImports bool
+ // See issue https://github.com/gohugoio/hugo/issues/13719
+ // Disable inlining of @import statements
+ // This is currenty only used for css.TailwindCSS.
+ DisableInlineImports bool
+
// When InlineImports is enabled, we fail the build if an import cannot be resolved.
// You can enable this to allow the build to continue and leave the import statement in place.
// Note that the inline importer does not process url location or imports with media queries,
// so those will be left as-is even without enabling this option.
SkipInlineImportsNotFound bool
+}
+
+// Some of the options from https://github.com/postcss/postcss-cli
+type PostCSSOptions struct {
+ // Set a custom path to look for a config file.
+ Config string
+
+ NoMap bool // Disable the default inline sourcemaps
+
+ InlineImports `mapstructure:",squash"`
// Options for when not using a config file
Use string // List of postcss plugins to use
@@ -112,7 +105,7 @@ type Options struct {
Syntax string // Custom postcss syntax
}
-func (opts Options) toArgs() []string {
+func (opts PostCSSOptions) toArgs() []string {
var args []string
if opts.NoMap {
args = append(args, "--no-map")
@@ -156,13 +149,9 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC
var configFile string
- var options Options
- if t.optionsm != nil {
- var err error
- options, err = decodeOptions(t.optionsm)
- if err != nil {
- return err
- }
+ options, err := decodePostCSSOptions(t.optionsm)
+ if err != nil {
+ return err
}
if options.Config != "" {
@@ -219,11 +208,11 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC
imp := newImportResolver(
ctx.From,
ctx.InPath,
- options,
+ options.InlineImports,
t.rs.Assets.Fs, t.rs.Logger, ctx.DependencyManager,
)
- if options.InlineImports {
+ if options.InlineImports.InlineImports {
var err error
src, err = imp.resolve()
if err != nil {
@@ -248,196 +237,3 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC
return nil
}
-
-type fileOffset struct {
- Filename string
- Offset int
-}
-
-type importResolver struct {
- r io.Reader
- inPath string
- opts Options
-
- contentSeen map[string]bool
- dependencyManager identity.Manager
- linemap map[int]fileOffset
- fs afero.Fs
- logger loggers.Logger
-}
-
-func newImportResolver(r io.Reader, inPath string, opts Options, fs afero.Fs, logger loggers.Logger, dependencyManager identity.Manager) *importResolver {
- return &importResolver{
- r: r,
- dependencyManager: dependencyManager,
- inPath: inPath,
- fs: fs, logger: logger,
- linemap: make(map[int]fileOffset), contentSeen: make(map[string]bool),
- opts: opts,
- }
-}
-
-func (imp *importResolver) contentHash(filename string) ([]byte, string) {
- b, err := afero.ReadFile(imp.fs, filename)
- if err != nil {
- return nil, ""
- }
- h := sha256.New()
- h.Write(b)
- return b, hex.EncodeToString(h.Sum(nil))
-}
-
-func (imp *importResolver) importRecursive(
- lineNum int,
- content string,
- inPath string,
-) (int, string, error) {
- basePath := path.Dir(inPath)
-
- var replacements []string
- lines := strings.Split(content, "\n")
-
- trackLine := func(i, offset int, line string) {
- // TODO(bep) this is not very efficient.
- imp.linemap[i+lineNum] = fileOffset{Filename: inPath, Offset: offset}
- }
-
- i := 0
- for offset, line := range lines {
- i++
- lineTrimmed := strings.TrimSpace(line)
- column := strings.Index(line, lineTrimmed)
- line = lineTrimmed
-
- if !imp.shouldImport(line) {
- trackLine(i, offset, line)
- } else {
- path := strings.Trim(strings.TrimPrefix(line, importIdentifier), " \"';")
- filename := filepath.Join(basePath, path)
- imp.dependencyManager.AddIdentity(identity.CleanStringIdentity(filename))
- importContent, hash := imp.contentHash(filename)
-
- if importContent == nil {
- if imp.opts.SkipInlineImportsNotFound {
- trackLine(i, offset, line)
- continue
- }
- pos := text.Position{
- Filename: inPath,
- LineNumber: offset + 1,
- ColumnNumber: column + 1,
- }
- return 0, "", herrors.NewFileErrorFromFileInPos(fmt.Errorf("failed to resolve CSS @import \"%s\"", filename), pos, imp.fs, nil)
- }
-
- i--
-
- if imp.contentSeen[hash] {
- i++
- // Just replace the line with an empty string.
- replacements = append(replacements, []string{line, ""}...)
- trackLine(i, offset, "IMPORT")
- continue
- }
-
- imp.contentSeen[hash] = true
-
- // Handle recursive imports.
- l, nested, err := imp.importRecursive(i+lineNum, string(importContent), filepath.ToSlash(filename))
- if err != nil {
- return 0, "", err
- }
-
- trackLine(i, offset, line)
-
- i += l
-
- importContent = []byte(nested)
-
- replacements = append(replacements, []string{line, string(importContent)}...)
- }
- }
-
- if len(replacements) > 0 {
- repl := strings.NewReplacer(replacements...)
- content = repl.Replace(content)
- }
-
- return i, content, nil
-}
-
-func (imp *importResolver) resolve() (io.Reader, error) {
- content, err := io.ReadAll(imp.r)
- if err != nil {
- return nil, err
- }
-
- contents := string(content)
-
- _, newContent, err := imp.importRecursive(0, contents, imp.inPath)
- if err != nil {
- return nil, err
- }
-
- return strings.NewReader(newContent), nil
-}
-
-// See https://www.w3schools.com/cssref/pr_import_rule.asp
-// We currently only support simple file imports, no urls, no media queries.
-// So this is OK:
-//
-// @import "navigation.css";
-//
-// This is not:
-//
-// @import url("navigation.css");
-// @import "mobstyle.css" screen and (max-width: 768px);
-func (imp *importResolver) shouldImport(s string) bool {
- if !strings.HasPrefix(s, importIdentifier) {
- return false
- }
- if strings.Contains(s, "url(") {
- return false
- }
-
- return shouldImportRe.MatchString(s)
-}
-
-func (imp *importResolver) toFileError(output string) error {
- inErr := errors.New(output)
-
- match := cssSyntaxErrorRe.FindStringSubmatch(output)
- if match == nil {
- return inErr
- }
-
- lineNum, err := strconv.Atoi(match[1])
- if err != nil {
- return inErr
- }
-
- file, ok := imp.linemap[lineNum]
- if !ok {
- return inErr
- }
-
- fi, err := imp.fs.Stat(file.Filename)
- if err != nil {
- return inErr
- }
-
- meta := fi.(hugofs.FileMetaInfo).Meta()
- realFilename := meta.Filename
- f, err := meta.Open()
- if err != nil {
- return inErr
- }
- defer f.Close()
-
- ferr := herrors.NewFileErrorFromName(inErr, realFilename)
- pos := ferr.Position()
- pos.LineNumber = file.Offset + 1
- return ferr.UpdatePosition(pos).UpdateContent(f, nil)
-
- // return herrors.NewFileErrorFromFile(inErr, file.Filename, realFilename, hugofs.Os, herrors.SimpleLineMatcher)
-}
diff --git a/resources/resource_transformers/postcss/postcss_integration_test.go b/resources/resource_transformers/cssjs/postcss_integration_test.go
similarity index 96%
rename from resources/resource_transformers/postcss/postcss_integration_test.go
rename to resources/resource_transformers/cssjs/postcss_integration_test.go
index 957e69403..a05f340fd 100644
--- a/resources/resource_transformers/postcss/postcss_integration_test.go
+++ b/resources/resource_transformers/cssjs/postcss_integration_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Hugo Authors. All rights reserved.
+// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package postcss_test
+package cssjs_test
import (
"fmt"
@@ -70,7 +70,7 @@ hello:
other: "Bonjour"
-- layouts/index.html --
{{ $options := dict "inlineImports" true }}
-{{ $styles := resources.Get "css/styles.css" | resources.PostCSS $options }}
+{{ $styles := resources.Get "css/styles.css" | css.PostCSS $options }}
Styles RelPermalink: {{ $styles.RelPermalink }}
{{ $cssContent := $styles.Content }}
Styles Content: Len: {{ len $styles.Content }}|
@@ -181,7 +181,7 @@ func TestTransformPostCSSNotInstalledError(t *testing.T) {
}).BuildE()
s.AssertIsFileError(err)
- c.Assert(err.Error(), qt.Contains, `binary with name "npx" not found`)
+ c.Assert(err.Error(), qt.Contains, `binary with name "postcss" not found using npx`)
}
// #9895
@@ -239,7 +239,7 @@ func TestTransformPostCSSResourceCacheWithPathInBaseURL(t *testing.T) {
c.Assert(err, qt.IsNil)
c.Cleanup(clean)
- for i := 0; i < 2; i++ {
+ for i := range 2 {
files := postCSSIntegrationTestFiles
if i == 1 {
diff --git a/resources/resource_transformers/cssjs/tailwindcss.go b/resources/resource_transformers/cssjs/tailwindcss.go
new file mode 100644
index 000000000..a60a16222
--- /dev/null
+++ b/resources/resource_transformers/cssjs/tailwindcss.go
@@ -0,0 +1,167 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cssjs
+
+import (
+ "bytes"
+ "io"
+ "regexp"
+ "strings"
+
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/hexec"
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/resources/internal"
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/mitchellh/mapstructure"
+)
+
+var (
+ tailwindcssImportRe = regexp.MustCompile(`^tailwindcss/?`)
+ tailwindImportExclude = func(s string) bool {
+ return tailwindcssImportRe.MatchString(s) && !strings.Contains(s, ".")
+ }
+)
+
+// NewTailwindCSSClient creates a new TailwindCSSClient with the given specification.
+func NewTailwindCSSClient(rs *resources.Spec) *TailwindCSSClient {
+ return &TailwindCSSClient{rs: rs}
+}
+
+// Client is the client used to do TailwindCSS transformations.
+type TailwindCSSClient struct {
+ rs *resources.Spec
+}
+
+// Process transforms the given Resource with the TailwindCSS processor.
+func (c *TailwindCSSClient) Process(res resources.ResourceTransformer, options map[string]any) (resource.Resource, error) {
+ return res.Transform(&tailwindcssTransformation{rs: c.rs, optionsm: options})
+}
+
+type tailwindcssTransformation struct {
+ optionsm map[string]any
+ rs *resources.Spec
+}
+
+func (t *tailwindcssTransformation) Key() internal.ResourceTransformationKey {
+ return internal.NewResourceTransformationKey("tailwindcss", t.optionsm)
+}
+
+type TailwindCSSOptions struct {
+ Minify bool // Optimize and minify the output
+ Optimize bool // Optimize the output without minifying
+ InlineImports `mapstructure:",squash"`
+}
+
+func (opts TailwindCSSOptions) toArgs() []any {
+ var args []any
+ if opts.Minify {
+ args = append(args, "--minify")
+ }
+ if opts.Optimize {
+ args = append(args, "--optimize")
+ }
+ return args
+}
+
+func (t *tailwindcssTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
+ const binaryName = "tailwindcss"
+
+ options, err := decodeTailwindCSSOptions(t.optionsm)
+ if err != nil {
+ return err
+ }
+
+ infol := t.rs.Logger.InfoCommand(binaryName)
+ infow := loggers.LevelLoggerToWriter(infol)
+
+ ex := t.rs.ExecHelper
+
+ workingDir := t.rs.Cfg.BaseConfig().WorkingDir
+
+ var cmdArgs []any = []any{
+ "--input=-", // Read from stdin.
+ "--cwd", workingDir,
+ }
+
+ cmdArgs = append(cmdArgs, options.toArgs()...)
+
+ var errBuf bytes.Buffer
+
+ stderr := io.MultiWriter(infow, &errBuf)
+ cmdArgs = append(cmdArgs, hexec.WithStderr(stderr))
+ cmdArgs = append(cmdArgs, hexec.WithStdout(ctx.To))
+ cmdArgs = append(cmdArgs, hexec.WithEnviron(hugo.GetExecEnviron(workingDir, t.rs.Cfg, t.rs.BaseFs.Assets.Fs)))
+
+ cmd, err := ex.Npx(binaryName, cmdArgs...)
+ if err != nil {
+ if hexec.IsNotFound(err) {
+ // This may be on a CI server etc. Will fall back to pre-built assets.
+ return &herrors.FeatureNotAvailableError{Cause: err}
+ }
+ return err
+ }
+
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ return err
+ }
+
+ src := ctx.From
+
+ imp := newImportResolver(
+ ctx.From,
+ ctx.InPath,
+ options.InlineImports,
+ t.rs.Assets.Fs, t.rs.Logger, ctx.DependencyManager,
+ )
+
+ if !options.InlineImports.DisableInlineImports {
+ src, err = imp.resolve()
+ if err != nil {
+ return err
+ }
+ }
+
+ go func() {
+ defer stdin.Close()
+ io.Copy(stdin, src)
+ }()
+
+ err = cmd.Run()
+ if err != nil {
+ if hexec.IsNotFound(err) {
+ return &herrors.FeatureNotAvailableError{
+ Cause: err,
+ }
+ }
+ s := errBuf.String()
+ if options.InlineImports.DisableInlineImports && strings.Contains(s, "Can't resolve") {
+ s += "You may want to set the 'disableInlineImports' option to false to inline imports, see https://gohugo.io/functions/css/tailwindcss/#disableinlineimports"
+ }
+ return imp.toFileError(s)
+ }
+
+ return nil
+}
+
+func decodeTailwindCSSOptions(m map[string]any) (opts TailwindCSSOptions, err error) {
+ if m == nil {
+ return
+ }
+ err = mapstructure.WeakDecode(m, &opts)
+ return
+}
diff --git a/resources/resource_transformers/cssjs/tailwindcss_integration_test.go b/resources/resource_transformers/cssjs/tailwindcss_integration_test.go
new file mode 100644
index 000000000..734ffe759
--- /dev/null
+++ b/resources/resource_transformers/cssjs/tailwindcss_integration_test.go
@@ -0,0 +1,136 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cssjs_test
+
+import (
+ "testing"
+
+ "github.com/bep/logg"
+ qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/htesting"
+ "github.com/gohugoio/hugo/hugolib"
+)
+
+func TestTailwindV4Basic(t *testing.T) {
+ if !htesting.IsCI() {
+ t.Skip("Skip long running test when running locally")
+ }
+
+ files := `
+-- hugo.toml --
+-- package.json --
+{
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/bep/hugo-starter-tailwind-basic.git"
+ },
+ "devDependencies": {
+ "@tailwindcss/cli": "^4.0.1",
+ "tailwindcss": "^4.0.1"
+ },
+ "name": "hugo-starter-tailwind-basic",
+ "version": "0.1.0"
+}
+-- assets/css/styles.css --
+@import "tailwindcss";
+
+@theme {
+ --font-family-display: "Satoshi", "sans-serif";
+
+ --breakpoint-3xl: 1920px;
+
+ --color-neon-pink: oklch(71.7% 0.25 360);
+ --color-neon-lime: oklch(91.5% 0.258 129);
+ --color-neon-cyan: oklch(91.3% 0.139 195.8);
+}
+-- layouts/index.html --
+{{ $css := resources.Get "css/styles.css" | css.TailwindCSS }}
+CSS: {{ $css.Content | safeCSS }}|
+`
+
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ TxtarString: files,
+ NeedsOsFS: true,
+ NeedsNpmInstall: true,
+ LogLevel: logg.LevelInfo,
+ }).Build()
+
+ b.AssertFileContent("public/index.html", "/*! tailwindcss v4.")
+}
+
+func TestTailwindCSSNoInlineImportsIssue13719(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+theme = 'my-theme'
+
+[[module.mounts]]
+source = 'assets'
+target = 'assets'
+
+[[module.mounts]]
+source = 'other'
+target = 'assets/css'
+-- assets/css/main.css --
+@import "tailwindcss";
+
+@import "colors/red.css";
+@import "colors/blue.css";
+@import "colors/purple.css";
+-- assets/css/colors/red.css --
+@import "green.css";
+
+.red {color: red;}
+-- assets/css/colors/green.css --
+.green {color: green;}
+-- themes/my-theme/assets/css/colors/blue.css --
+.blue {color: blue;}
+-- other/colors/purple.css --
+.purple {color: purple;}
+-- layouts/home.html --
+{{ with (templates.Defer (dict "key" "global")) }}
+ {{ with resources.Get "css/main.css" }}
+ {{ $opts := dict "disableInlineImports" true }}
+ {{ with . | css.TailwindCSS $opts }}
+
+ {{ end }}
+ {{ end }}
+{{ end }}
+-- package.json --
+{
+ "devDependencies": {
+ "@tailwindcss/cli": "^4.1.7",
+ "tailwindcss": "^4.1.7"
+ }
+}
+`
+
+ b, err := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ TxtarString: files,
+ NeedsOsFS: true,
+ NeedsNpmInstall: true,
+ LogLevel: logg.LevelInfo,
+ }).BuildE()
+
+ b.Assert(err, qt.IsNotNil)
+ b.Assert(err.Error(), qt.Contains, "Can't resolve 'colors/red.css'")
+ b.Assert(err.Error(), qt.Contains, "You may want to set the 'disableInlineImports' option to false")
+}
diff --git a/resources/resource_transformers/js/build.go b/resources/resource_transformers/js/build.go
index cc68d2253..bd943461f 100644
--- a/resources/resource_transformers/js/build.go
+++ b/resources/resource_transformers/js/build.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Hugo Authors. All rights reserved.
+// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,209 +14,69 @@
package js
import (
- "errors"
- "fmt"
- "io"
- "os"
"path"
- "path/filepath"
"regexp"
- "strings"
-
- "github.com/spf13/afero"
-
- "github.com/gohugoio/hugo/hugofs"
- "github.com/gohugoio/hugo/media"
-
- "github.com/gohugoio/hugo/common/herrors"
- "github.com/gohugoio/hugo/common/text"
-
- "github.com/gohugoio/hugo/hugolib/filesystems"
- "github.com/gohugoio/hugo/resources/internal"
"github.com/evanw/esbuild/pkg/api"
+ "github.com/gohugoio/hugo/hugolib/filesystems"
+ "github.com/gohugoio/hugo/internal/js/esbuild"
+
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource"
)
// Client context for ESBuild.
type Client struct {
- rs *resources.Spec
- sfs *filesystems.SourceFilesystem
+ c *esbuild.BuildClient
}
// New creates a new client context.
func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) *Client {
return &Client{
- rs: rs,
- sfs: fs,
+ c: esbuild.NewBuildClient(fs, rs),
}
}
-type buildTransformation struct {
- optsm map[string]any
- c *Client
-}
-
-func (t *buildTransformation) Key() internal.ResourceTransformationKey {
- return internal.NewResourceTransformationKey("jsbuild", t.optsm)
-}
-
-func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
- ctx.OutMediaType = media.Builtin.JavascriptType
-
- opts, err := decodeOptions(t.optsm)
- if err != nil {
- return err
- }
-
- if opts.TargetPath != "" {
- ctx.OutPath = opts.TargetPath
- } else {
- ctx.ReplaceOutPathExtension(".js")
- }
-
- src, err := io.ReadAll(ctx.From)
- if err != nil {
- return err
- }
-
- opts.sourceDir = filepath.FromSlash(path.Dir(ctx.SourcePath))
- opts.resolveDir = t.c.rs.Cfg.BaseConfig().WorkingDir // where node_modules gets resolved
- opts.contents = string(src)
- opts.mediaType = ctx.InMediaType
- opts.tsConfig = t.c.rs.ResolveJSConfigFile("tsconfig.json")
-
- buildOptions, err := toBuildOptions(opts)
- if err != nil {
- return err
- }
-
- buildOptions.Plugins, err = createBuildPlugins(ctx.DependencyManager, t.c, opts)
- if err != nil {
- return err
- }
-
- if buildOptions.Sourcemap == api.SourceMapExternal && buildOptions.Outdir == "" {
- buildOptions.Outdir, err = os.MkdirTemp(os.TempDir(), "compileOutput")
- if err != nil {
- return err
- }
- defer os.Remove(buildOptions.Outdir)
- }
-
- if opts.Inject != nil {
- // Resolve the absolute filenames.
- for i, ext := range opts.Inject {
- impPath := filepath.FromSlash(ext)
- if filepath.IsAbs(impPath) {
- return fmt.Errorf("inject: absolute paths not supported, must be relative to /assets")
- }
-
- m := resolveComponentInAssets(t.c.rs.Assets.Fs, impPath)
-
- if m == nil {
- return fmt.Errorf("inject: file %q not found", ext)
- }
-
- opts.Inject[i] = m.Filename
-
- }
-
- buildOptions.Inject = opts.Inject
-
- }
-
- result := api.Build(buildOptions)
-
- if len(result.Errors) > 0 {
-
- createErr := func(msg api.Message) error {
- loc := msg.Location
- if loc == nil {
- return errors.New(msg.Text)
- }
- path := loc.File
- if path == stdinImporter {
- path = ctx.SourcePath
- }
-
- errorMessage := msg.Text
- errorMessage = strings.ReplaceAll(errorMessage, nsImportHugo+":", "")
-
- var (
- f afero.File
- err error
- )
-
- if strings.HasPrefix(path, nsImportHugo) {
- path = strings.TrimPrefix(path, nsImportHugo+":")
- f, err = hugofs.Os.Open(path)
- } else {
- var fi os.FileInfo
- fi, err = t.c.sfs.Fs.Stat(path)
- if err == nil {
- m := fi.(hugofs.FileMetaInfo).Meta()
- path = m.Filename
- f, err = m.Open()
- }
-
- }
-
- if err == nil {
- fe := herrors.
- NewFileErrorFromName(errors.New(errorMessage), path).
- UpdatePosition(text.Position{Offset: -1, LineNumber: loc.Line, ColumnNumber: loc.Column}).
- UpdateContent(f, nil)
-
- f.Close()
- return fe
- }
-
- return fmt.Errorf("%s", errorMessage)
- }
-
- var errors []error
-
- for _, msg := range result.Errors {
- errors = append(errors, createErr(msg))
- }
-
- // Return 1, log the rest.
- for i, err := range errors {
- if i > 0 {
- t.c.rs.Logger.Errorf("js.Build failed: %s", err)
- }
- }
-
- return errors[0]
- }
-
- if buildOptions.Sourcemap == api.SourceMapExternal {
- content := string(result.OutputFiles[1].Contents)
- symPath := path.Base(ctx.OutPath) + ".map"
- re := regexp.MustCompile(`//# sourceMappingURL=.*\n?`)
- content = re.ReplaceAllString(content, "//# sourceMappingURL="+symPath+"\n")
-
- if err = ctx.PublishSourceMap(string(result.OutputFiles[0].Contents)); err != nil {
- return err
- }
- _, err := ctx.To.Write([]byte(content))
- if err != nil {
- return err
- }
- } else {
- _, err := ctx.To.Write(result.OutputFiles[0].Contents)
- if err != nil {
- return err
- }
- }
- return nil
-}
-
-// Process process esbuild transform
+// Process processes a resource with the user provided options.
func (c *Client) Process(res resources.ResourceTransformer, opts map[string]any) (resource.Resource, error) {
return res.Transform(
&buildTransformation{c: c, optsm: opts},
)
}
+
+func (c *Client) transform(opts esbuild.Options, transformCtx *resources.ResourceTransformationCtx) (api.BuildResult, error) {
+ if transformCtx.DependencyManager != nil {
+ opts.DependencyManager = transformCtx.DependencyManager
+ }
+
+ opts.StdinSourcePath = transformCtx.SourcePath
+
+ result, err := c.c.Build(opts)
+ if err != nil {
+ return result, err
+ }
+
+ if opts.ExternalOptions.SourceMap == "linked" || opts.ExternalOptions.SourceMap == "external" {
+ content := string(result.OutputFiles[1].Contents)
+ if opts.ExternalOptions.SourceMap == "linked" {
+ symPath := path.Base(transformCtx.OutPath) + ".map"
+ re := regexp.MustCompile(`//# sourceMappingURL=.*\n?`)
+ content = re.ReplaceAllString(content, "//# sourceMappingURL="+symPath+"\n")
+ }
+
+ if err = transformCtx.PublishSourceMap(string(result.OutputFiles[0].Contents)); err != nil {
+ return result, err
+ }
+ _, err := transformCtx.To.Write([]byte(content))
+ if err != nil {
+ return result, err
+ }
+ } else {
+ _, err := transformCtx.To.Write(result.OutputFiles[0].Contents)
+ if err != nil {
+ return result, err
+ }
+
+ }
+ return result, nil
+}
diff --git a/resources/resource_transformers/js/js_integration_test.go b/resources/resource_transformers/js/js_integration_test.go
index 304c51d33..9cee19a86 100644
--- a/resources/resource_transformers/js/js_integration_test.go
+++ b/resources/resource_transformers/js/js_integration_test.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Hugo Authors. All rights reserved.
+// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,13 +14,16 @@
package js_test
import (
+ "os"
"path/filepath"
"strings"
"testing"
qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/internal/js/esbuild"
)
func TestBuildVariants(t *testing.T) {
@@ -173,7 +176,7 @@ hello:
hello:
other: "Bonjour"
-- layouts/index.html --
-{{ $options := dict "minify" false "externals" (slice "react" "react-dom") }}
+{{ $options := dict "minify" false "externals" (slice "react" "react-dom") "sourcemap" "linked" }}
{{ $js := resources.Get "js/main.js" | js.Build $options }}
JS: {{ template "print" $js }}
{{ $jsx := resources.Get "js/myjsx.jsx" | js.Build $options }}
@@ -201,14 +204,31 @@ TS2: {{ template "print" $ts2 }}
TxtarString: files,
}).Build()
- b.AssertFileContent("public/js/myts.js", `//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJz`)
- b.AssertFileContent("public/js/myts2.js.map", `"version": 3,`)
+ b.AssertFileContent("public/js/main.js", `//# sourceMappingURL=main.js.map`)
+ b.AssertFileContent("public/js/main.js.map", `"version":3`, "! ns-hugo") // linked
+ b.AssertFileContent("public/js/myts.js", `//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJz`) // inline
b.AssertFileContent("public/index.html", `
console.log("included");
if (hasSpace.test(string))
var React = __toESM(__require("react"));
function greeter(person) {
`)
+
+ checkMap := func(p string, expectLen int) {
+ s := b.FileContent(p)
+ sources := esbuild.SourcesFromSourceMap(s)
+ b.Assert(sources, qt.HasLen, expectLen)
+
+ // Check that all source files exist.
+ for _, src := range sources {
+ filename, ok := paths.UrlStringToFilename(src)
+ b.Assert(ok, qt.IsTrue)
+ _, err := os.Stat(filename)
+ b.Assert(err, qt.IsNil, qt.Commentf("src: %q", src))
+ }
+ }
+
+ checkMap("public/js/main.js.map", 4)
}
func TestBuildError(t *testing.T) {
@@ -371,3 +391,32 @@ class A {}
}).Build()
b.AssertFileContent("public/js/main.js", "__decorateClass")
}
+
+// Issue 13183.
+func TestExternalsInAssets(t *testing.T) {
+ files := `
+-- assets/js/util1.js --
+export function hello1() {
+ return 'abcd';
+}
+-- assets/js/util2.js --
+export function hello2() {
+ return 'efgh';
+}
+-- assets/js/main.js --
+import { hello1 } from './util1.js';
+import { hello2 } from './util2.js';
+
+hello1();
+hello2();
+-- layouts/index.html --
+Home.
+{{ $js := resources.Get "js/main.js" | js.Build (dict "externals" (slice "./util1.js")) }}
+{{ $js.Publish }}
+`
+
+ b := hugolib.Test(t, files, hugolib.TestOptOsFs())
+
+ b.AssertFileContent("public/js/main.js", "efgh")
+ b.AssertFileContent("public/js/main.js", "! abcd")
+}
diff --git a/resources/resource_transformers/js/options.go b/resources/resource_transformers/js/options.go
deleted file mode 100644
index 7de88638c..000000000
--- a/resources/resource_transformers/js/options.go
+++ /dev/null
@@ -1,455 +0,0 @@
-// Copyright 2020 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package js
-
-import (
- "encoding/json"
- "fmt"
- "os"
- "path/filepath"
- "strings"
-
- "github.com/gohugoio/hugo/common/maps"
- "github.com/gohugoio/hugo/common/paths"
- "github.com/gohugoio/hugo/identity"
- "github.com/spf13/afero"
-
- "github.com/evanw/esbuild/pkg/api"
-
- "github.com/gohugoio/hugo/hugofs"
- "github.com/gohugoio/hugo/media"
- "github.com/mitchellh/mapstructure"
-)
-
-const (
- nsImportHugo = "ns-hugo"
- nsParams = "ns-params"
-
- stdinImporter = ""
-)
-
-// Options esbuild configuration
-type Options struct {
- // If not set, the source path will be used as the base target path.
- // Note that the target path's extension may change if the target MIME type
- // is different, e.g. when the source is TypeScript.
- TargetPath string
-
- // Whether to minify to output.
- Minify bool
-
- // Whether to write mapfiles
- SourceMap string
-
- // The language target.
- // One of: es2015, es2016, es2017, es2018, es2019, es2020 or esnext.
- // Default is esnext.
- Target string
-
- // The output format.
- // One of: iife, cjs, esm
- // Default is to esm.
- Format string
-
- // External dependencies, e.g. "react".
- Externals []string
-
- // This option allows you to automatically replace a global variable with an import from another file.
- // The filenames must be relative to /assets.
- // See https://esbuild.github.io/api/#inject
- Inject []string
-
- // User defined symbols.
- Defines map[string]any
-
- // Maps a component import to another.
- Shims map[string]string
-
- // User defined params. Will be marshaled to JSON and available as "@params", e.g.
- // import * as params from '@params';
- Params any
-
- // What to use instead of React.createElement.
- JSXFactory string
-
- // What to use instead of React.Fragment.
- JSXFragment string
-
- // What to do about JSX syntax.
- // See https://esbuild.github.io/api/#jsx
- JSX string
-
- // Which library to use to automatically import JSX helper functions from. Only works if JSX is set to automatic.
- // See https://esbuild.github.io/api/#jsx-import-source
- JSXImportSource string
-
- // There is/was a bug in WebKit with severe performance issue with the tracking
- // of TDZ checks in JavaScriptCore.
- //
- // Enabling this flag removes the TDZ and `const` assignment checks and
- // may improve performance of larger JS codebases until the WebKit fix
- // is in widespread use.
- //
- // See https://bugs.webkit.org/show_bug.cgi?id=199866
- // Deprecated: This no longer have any effect and will be removed.
- // TODO(bep) remove. See https://github.com/evanw/esbuild/commit/869e8117b499ca1dbfc5b3021938a53ffe934dba
- AvoidTDZ bool
-
- mediaType media.Type
- outDir string
- contents string
- sourceDir string
- resolveDir string
- tsConfig string
-}
-
-func decodeOptions(m map[string]any) (Options, error) {
- var opts Options
-
- if err := mapstructure.WeakDecode(m, &opts); err != nil {
- return opts, err
- }
-
- if opts.TargetPath != "" {
- opts.TargetPath = paths.ToSlashTrimLeading(opts.TargetPath)
- }
-
- opts.Target = strings.ToLower(opts.Target)
- opts.Format = strings.ToLower(opts.Format)
-
- return opts, nil
-}
-
-var extensionToLoaderMap = map[string]api.Loader{
- ".js": api.LoaderJS,
- ".mjs": api.LoaderJS,
- ".cjs": api.LoaderJS,
- ".jsx": api.LoaderJSX,
- ".ts": api.LoaderTS,
- ".tsx": api.LoaderTSX,
- ".css": api.LoaderCSS,
- ".json": api.LoaderJSON,
- ".txt": api.LoaderText,
-}
-
-func loaderFromFilename(filename string) api.Loader {
- l, found := extensionToLoaderMap[filepath.Ext(filename)]
- if found {
- return l
- }
- return api.LoaderJS
-}
-
-func resolveComponentInAssets(fs afero.Fs, impPath string) *hugofs.FileMeta {
- findFirst := func(base string) *hugofs.FileMeta {
- // This is the most common sub-set of ESBuild's default extensions.
- // We assume that imports of JSON, CSS etc. will be using their full
- // name with extension.
- for _, ext := range []string{".js", ".ts", ".tsx", ".jsx"} {
- if strings.HasSuffix(impPath, ext) {
- // Import of foo.js.js need the full name.
- continue
- }
- if fi, err := fs.Stat(base + ext); err == nil {
- return fi.(hugofs.FileMetaInfo).Meta()
- }
- }
-
- // Not found.
- return nil
- }
-
- var m *hugofs.FileMeta
-
- // We need to check if this is a regular file imported without an extension.
- // There may be ambiguous situations where both foo.js and foo/index.js exists.
- // This import order is in line with both how Node and ESBuild's native
- // import resolver works.
-
- // It may be a regular file imported without an extension, e.g.
- // foo or foo/index.
- m = findFirst(impPath)
- if m != nil {
- return m
- }
-
- base := filepath.Base(impPath)
- if base == "index" {
- // try index.esm.js etc.
- m = findFirst(impPath + ".esm")
- if m != nil {
- return m
- }
- }
-
- // Check the path as is.
- fi, err := fs.Stat(impPath)
-
- if err == nil {
- if fi.IsDir() {
- m = findFirst(filepath.Join(impPath, "index"))
- if m == nil {
- m = findFirst(filepath.Join(impPath, "index.esm"))
- }
- } else {
- m = fi.(hugofs.FileMetaInfo).Meta()
- }
- } else if strings.HasSuffix(base, ".js") {
- m = findFirst(strings.TrimSuffix(impPath, ".js"))
- }
-
- return m
-}
-
-func createBuildPlugins(depsManager identity.Manager, c *Client, opts Options) ([]api.Plugin, error) {
- fs := c.rs.Assets
-
- resolveImport := func(args api.OnResolveArgs) (api.OnResolveResult, error) {
- impPath := args.Path
- if opts.Shims != nil {
- override, found := opts.Shims[impPath]
- if found {
- impPath = override
- }
- }
- isStdin := args.Importer == stdinImporter
- var relDir string
- if !isStdin {
- rel, found := fs.MakePathRelative(args.Importer, true)
- if !found {
- // Not in any of the /assets folders.
- // This is an import from a node_modules, let
- // ESBuild resolve this.
- return api.OnResolveResult{}, nil
- }
-
- relDir = filepath.Dir(rel)
- } else {
- relDir = opts.sourceDir
- }
-
- // Imports not starting with a "." is assumed to live relative to /assets.
- // Hugo makes no assumptions about the directory structure below /assets.
- if relDir != "" && strings.HasPrefix(impPath, ".") {
- impPath = filepath.Join(relDir, impPath)
- }
-
- m := resolveComponentInAssets(fs.Fs, impPath)
-
- if m != nil {
- depsManager.AddIdentity(m.PathInfo)
-
- // Store the source root so we can create a jsconfig.json
- // to help IntelliSense when the build is done.
- // This should be a small number of elements, and when
- // in server mode, we may get stale entries on renames etc.,
- // but that shouldn't matter too much.
- c.rs.JSConfigBuilder.AddSourceRoot(m.SourceRoot)
- return api.OnResolveResult{Path: m.Filename, Namespace: nsImportHugo}, nil
- }
-
- // Fall back to ESBuild's resolve.
- return api.OnResolveResult{}, nil
- }
-
- importResolver := api.Plugin{
- Name: "hugo-import-resolver",
- Setup: func(build api.PluginBuild) {
- build.OnResolve(api.OnResolveOptions{Filter: `.*`},
- func(args api.OnResolveArgs) (api.OnResolveResult, error) {
- return resolveImport(args)
- })
- build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: nsImportHugo},
- func(args api.OnLoadArgs) (api.OnLoadResult, error) {
- b, err := os.ReadFile(args.Path)
- if err != nil {
- return api.OnLoadResult{}, fmt.Errorf("failed to read %q: %w", args.Path, err)
- }
- c := string(b)
- return api.OnLoadResult{
- // See https://github.com/evanw/esbuild/issues/502
- // This allows all modules to resolve dependencies
- // in the main project's node_modules.
- ResolveDir: opts.resolveDir,
- Contents: &c,
- Loader: loaderFromFilename(args.Path),
- }, nil
- })
- },
- }
-
- params := opts.Params
- if params == nil {
- // This way @params will always resolve to something.
- params = make(map[string]any)
- }
-
- b, err := json.Marshal(params)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal params: %w", err)
- }
- bs := string(b)
- paramsPlugin := api.Plugin{
- Name: "hugo-params-plugin",
- Setup: func(build api.PluginBuild) {
- build.OnResolve(api.OnResolveOptions{Filter: `^@params$`},
- func(args api.OnResolveArgs) (api.OnResolveResult, error) {
- return api.OnResolveResult{
- Path: args.Path,
- Namespace: nsParams,
- }, nil
- })
- build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: nsParams},
- func(args api.OnLoadArgs) (api.OnLoadResult, error) {
- return api.OnLoadResult{
- Contents: &bs,
- Loader: api.LoaderJSON,
- }, nil
- })
- },
- }
-
- return []api.Plugin{importResolver, paramsPlugin}, nil
-}
-
-func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
- var target api.Target
- switch opts.Target {
- case "", "esnext":
- target = api.ESNext
- case "es5":
- target = api.ES5
- case "es6", "es2015":
- target = api.ES2015
- case "es2016":
- target = api.ES2016
- case "es2017":
- target = api.ES2017
- case "es2018":
- target = api.ES2018
- case "es2019":
- target = api.ES2019
- case "es2020":
- target = api.ES2020
- default:
- err = fmt.Errorf("invalid target: %q", opts.Target)
- return
- }
-
- mediaType := opts.mediaType
- if mediaType.IsZero() {
- mediaType = media.Builtin.JavascriptType
- }
-
- var loader api.Loader
- switch mediaType.SubType {
- // TODO(bep) ESBuild support a set of other loaders, but I currently fail
- // to see the relevance. That may change as we start using this.
- case media.Builtin.JavascriptType.SubType:
- loader = api.LoaderJS
- case media.Builtin.TypeScriptType.SubType:
- loader = api.LoaderTS
- case media.Builtin.TSXType.SubType:
- loader = api.LoaderTSX
- case media.Builtin.JSXType.SubType:
- loader = api.LoaderJSX
- default:
- err = fmt.Errorf("unsupported Media Type: %q", opts.mediaType)
- return
- }
-
- var format api.Format
- // One of: iife, cjs, esm
- switch opts.Format {
- case "", "iife":
- format = api.FormatIIFE
- case "esm":
- format = api.FormatESModule
- case "cjs":
- format = api.FormatCommonJS
- default:
- err = fmt.Errorf("unsupported script output format: %q", opts.Format)
- return
- }
-
- var jsx api.JSX
- switch opts.JSX {
- case "", "transform":
- jsx = api.JSXTransform
- case "preserve":
- jsx = api.JSXPreserve
- case "automatic":
- jsx = api.JSXAutomatic
- default:
- err = fmt.Errorf("unsupported jsx type: %q", opts.JSX)
- return
- }
-
- var defines map[string]string
- if opts.Defines != nil {
- defines = maps.ToStringMapString(opts.Defines)
- }
-
- // By default we only need to specify outDir and no outFile
- outDir := opts.outDir
- outFile := ""
- var sourceMap api.SourceMap
- switch opts.SourceMap {
- case "inline":
- sourceMap = api.SourceMapInline
- case "external":
- sourceMap = api.SourceMapExternal
- case "":
- sourceMap = api.SourceMapNone
- default:
- err = fmt.Errorf("unsupported sourcemap type: %q", opts.SourceMap)
- return
- }
-
- buildOptions = api.BuildOptions{
- Outfile: outFile,
- Bundle: true,
-
- Target: target,
- Format: format,
- Sourcemap: sourceMap,
-
- MinifyWhitespace: opts.Minify,
- MinifyIdentifiers: opts.Minify,
- MinifySyntax: opts.Minify,
-
- Outdir: outDir,
- Define: defines,
-
- External: opts.Externals,
-
- JSXFactory: opts.JSXFactory,
- JSXFragment: opts.JSXFragment,
-
- JSX: jsx,
- JSXImportSource: opts.JSXImportSource,
-
- Tsconfig: opts.tsConfig,
-
- // Note: We're not passing Sourcefile to ESBuild.
- // This makes ESBuild pass `stdin` as the Importer to the import
- // resolver, which is what we need/expect.
- Stdin: &api.StdinOptions{
- Contents: opts.contents,
- ResolveDir: opts.resolveDir,
- Loader: loader,
- },
- }
- return
-}
diff --git a/resources/resource_transformers/js/options_test.go b/resources/resource_transformers/js/options_test.go
deleted file mode 100644
index a49b174ae..000000000
--- a/resources/resource_transformers/js/options_test.go
+++ /dev/null
@@ -1,209 +0,0 @@
-// Copyright 2020 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package js
-
-import (
- "path"
- "path/filepath"
- "testing"
-
- "github.com/gohugoio/hugo/config"
- "github.com/gohugoio/hugo/config/testconfig"
- "github.com/gohugoio/hugo/hugofs"
- "github.com/gohugoio/hugo/hugolib/filesystems"
- "github.com/gohugoio/hugo/hugolib/paths"
- "github.com/gohugoio/hugo/media"
-
- "github.com/spf13/afero"
-
- "github.com/evanw/esbuild/pkg/api"
-
- qt "github.com/frankban/quicktest"
-)
-
-// This test is added to test/warn against breaking the "stability" of the
-// cache key. It's sometimes needed to break this, but should be avoided if possible.
-func TestOptionKey(t *testing.T) {
- c := qt.New(t)
-
- opts := map[string]any{
- "TargetPath": "foo",
- "Target": "es2018",
- }
-
- key := (&buildTransformation{optsm: opts}).Key()
-
- c.Assert(key.Value(), qt.Equals, "jsbuild_7891849149754191852")
-}
-
-func TestToBuildOptions(t *testing.T) {
- c := qt.New(t)
-
- opts, err := toBuildOptions(Options{mediaType: media.Builtin.JavascriptType})
-
- c.Assert(err, qt.IsNil)
- c.Assert(opts, qt.DeepEquals, api.BuildOptions{
- Bundle: true,
- Target: api.ESNext,
- Format: api.FormatIIFE,
- Stdin: &api.StdinOptions{
- Loader: api.LoaderJS,
- },
- })
-
- opts, err = toBuildOptions(Options{
- Target: "es2018",
- Format: "cjs",
- Minify: true,
- mediaType: media.Builtin.JavascriptType,
- AvoidTDZ: true,
- })
- c.Assert(err, qt.IsNil)
- c.Assert(opts, qt.DeepEquals, api.BuildOptions{
- Bundle: true,
- Target: api.ES2018,
- Format: api.FormatCommonJS,
- MinifyIdentifiers: true,
- MinifySyntax: true,
- MinifyWhitespace: true,
- Stdin: &api.StdinOptions{
- Loader: api.LoaderJS,
- },
- })
-
- opts, err = toBuildOptions(Options{
- Target: "es2018", Format: "cjs", Minify: true, mediaType: media.Builtin.JavascriptType,
- SourceMap: "inline",
- })
- c.Assert(err, qt.IsNil)
- c.Assert(opts, qt.DeepEquals, api.BuildOptions{
- Bundle: true,
- Target: api.ES2018,
- Format: api.FormatCommonJS,
- MinifyIdentifiers: true,
- MinifySyntax: true,
- MinifyWhitespace: true,
- Sourcemap: api.SourceMapInline,
- Stdin: &api.StdinOptions{
- Loader: api.LoaderJS,
- },
- })
-
- opts, err = toBuildOptions(Options{
- Target: "es2018", Format: "cjs", Minify: true, mediaType: media.Builtin.JavascriptType,
- SourceMap: "inline",
- })
- c.Assert(err, qt.IsNil)
- c.Assert(opts, qt.DeepEquals, api.BuildOptions{
- Bundle: true,
- Target: api.ES2018,
- Format: api.FormatCommonJS,
- MinifyIdentifiers: true,
- MinifySyntax: true,
- MinifyWhitespace: true,
- Sourcemap: api.SourceMapInline,
- Stdin: &api.StdinOptions{
- Loader: api.LoaderJS,
- },
- })
-
- opts, err = toBuildOptions(Options{
- Target: "es2018", Format: "cjs", Minify: true, mediaType: media.Builtin.JavascriptType,
- SourceMap: "external",
- })
- c.Assert(err, qt.IsNil)
- c.Assert(opts, qt.DeepEquals, api.BuildOptions{
- Bundle: true,
- Target: api.ES2018,
- Format: api.FormatCommonJS,
- MinifyIdentifiers: true,
- MinifySyntax: true,
- MinifyWhitespace: true,
- Sourcemap: api.SourceMapExternal,
- Stdin: &api.StdinOptions{
- Loader: api.LoaderJS,
- },
- })
-
- opts, err = toBuildOptions(Options{mediaType: media.Builtin.JavascriptType,
- JSX: "automatic", JSXImportSource: "preact"})
- c.Assert(err, qt.IsNil)
- c.Assert(opts, qt.DeepEquals, api.BuildOptions{
- Bundle: true,
- Target: api.ESNext,
- Format: api.FormatIIFE,
- Stdin: &api.StdinOptions{
- Loader: api.LoaderJS,
- },
- JSX: api.JSXAutomatic,
- JSXImportSource: "preact",
- })
-}
-
-func TestResolveComponentInAssets(t *testing.T) {
- c := qt.New(t)
-
- for _, test := range []struct {
- name string
- files []string
- impPath string
- expect string
- }{
- {"Basic, extension", []string{"foo.js", "bar.js"}, "foo.js", "foo.js"},
- {"Basic, no extension", []string{"foo.js", "bar.js"}, "foo", "foo.js"},
- {"Basic, no extension, typescript", []string{"foo.ts", "bar.js"}, "foo", "foo.ts"},
- {"Not found", []string{"foo.js", "bar.js"}, "moo.js", ""},
- {"Not found, double js extension", []string{"foo.js.js", "bar.js"}, "foo.js", ""},
- {"Index file, folder only", []string{"foo/index.js", "bar.js"}, "foo", "foo/index.js"},
- {"Index file, folder and index", []string{"foo/index.js", "bar.js"}, "foo/index", "foo/index.js"},
- {"Index file, folder and index and suffix", []string{"foo/index.js", "bar.js"}, "foo/index.js", "foo/index.js"},
- {"Index ESM file, folder only", []string{"foo/index.esm.js", "bar.js"}, "foo", "foo/index.esm.js"},
- {"Index ESM file, folder and index", []string{"foo/index.esm.js", "bar.js"}, "foo/index", "foo/index.esm.js"},
- {"Index ESM file, folder and index and suffix", []string{"foo/index.esm.js", "bar.js"}, "foo/index.esm.js", "foo/index.esm.js"},
- // We added these index.esm.js cases in v0.101.0. The case below is unlikely to happen in the wild, but add a test
- // to document Hugo's behavior. We pick the file with the name index.js; anything else would be breaking.
- {"Index and Index ESM file, folder only", []string{"foo/index.esm.js", "foo/index.js", "bar.js"}, "foo", "foo/index.js"},
-
- // Issue #8949
- {"Check file before directory", []string{"foo.js", "foo/index.js"}, "foo", "foo.js"},
- } {
- c.Run(test.name, func(c *qt.C) {
- baseDir := "assets"
- mfs := afero.NewMemMapFs()
-
- for _, filename := range test.files {
- c.Assert(afero.WriteFile(mfs, filepath.Join(baseDir, filename), []byte("let foo='bar';"), 0o777), qt.IsNil)
- }
-
- conf := testconfig.GetTestConfig(mfs, config.New())
- fs := hugofs.NewFrom(mfs, conf.BaseConfig())
-
- p, err := paths.New(fs, conf)
- c.Assert(err, qt.IsNil)
- bfs, err := filesystems.NewBase(p, nil)
- c.Assert(err, qt.IsNil)
-
- got := resolveComponentInAssets(bfs.Assets.Fs, test.impPath)
-
- gotPath := ""
- expect := test.expect
- if got != nil {
- gotPath = filepath.ToSlash(got.Filename)
- expect = path.Join(baseDir, test.expect)
- }
-
- c.Assert(gotPath, qt.Equals, expect)
- })
- }
-}
diff --git a/resources/resource_transformers/js/transform.go b/resources/resource_transformers/js/transform.go
new file mode 100644
index 000000000..13909e54c
--- /dev/null
+++ b/resources/resource_transformers/js/transform.go
@@ -0,0 +1,68 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package js
+
+import (
+ "io"
+ "path"
+ "path/filepath"
+
+ "github.com/gohugoio/hugo/internal/js/esbuild"
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/resources/internal"
+)
+
+type buildTransformation struct {
+ optsm map[string]any
+ c *Client
+}
+
+func (t *buildTransformation) Key() internal.ResourceTransformationKey {
+ return internal.NewResourceTransformationKey("jsbuild", t.optsm)
+}
+
+func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
+ ctx.OutMediaType = media.Builtin.JavascriptType
+
+ var opts esbuild.Options
+
+ if t.optsm != nil {
+ optsExt, err := esbuild.DecodeExternalOptions(t.optsm)
+ if err != nil {
+ return err
+ }
+ opts.ExternalOptions = optsExt
+ }
+
+ if opts.TargetPath != "" {
+ ctx.OutPath = opts.TargetPath
+ } else {
+ ctx.ReplaceOutPathExtension(".js")
+ }
+
+ src, err := io.ReadAll(ctx.From)
+ if err != nil {
+ return err
+ }
+
+ opts.SourceDir = filepath.FromSlash(path.Dir(ctx.SourcePath))
+ opts.Contents = string(src)
+ opts.MediaType = ctx.InMediaType
+ opts.Stdin = true
+
+ _, err = t.c.transform(opts, ctx)
+
+ return err
+}
diff --git a/resources/resource_transformers/templates/execute_as_template.go b/resources/resource_transformers/templates/execute_as_template.go
index 79d249bd6..cd421e08f 100644
--- a/resources/resource_transformers/templates/execute_as_template.go
+++ b/resources/resource_transformers/templates/execute_as_template.go
@@ -23,17 +23,17 @@ import (
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/internal"
"github.com/gohugoio/hugo/resources/resource"
- "github.com/gohugoio/hugo/tpl"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
)
// Client contains methods to perform template processing of Resource objects.
type Client struct {
rs *resources.Spec
- t tpl.TemplatesProvider
+ t tplimpl.TemplateStoreProvider
}
// New creates a new Client with the given specification.
-func New(rs *resources.Spec, t tpl.TemplatesProvider) *Client {
+func New(rs *resources.Spec, t tplimpl.TemplateStoreProvider) *Client {
if rs == nil {
panic("must provide a resource Spec")
}
@@ -45,7 +45,7 @@ func New(rs *resources.Spec, t tpl.TemplatesProvider) *Client {
type executeAsTemplateTransform struct {
rs *resources.Spec
- t tpl.TemplatesProvider
+ t tplimpl.TemplateStoreProvider
targetPath string
data any
}
@@ -56,14 +56,13 @@ func (t *executeAsTemplateTransform) Key() internal.ResourceTransformationKey {
func (t *executeAsTemplateTransform) Transform(ctx *resources.ResourceTransformationCtx) error {
tplStr := helpers.ReaderToString(ctx.From)
- templ, err := t.t.TextTmpl().Parse(ctx.InPath, tplStr)
+ th := t.t.GetTemplateStore()
+ ti, err := th.TextParse(ctx.InPath, tplStr)
if err != nil {
return fmt.Errorf("failed to parse Resource %q as Template:: %w", ctx.InPath, err)
}
-
ctx.OutPath = t.targetPath
-
- return t.t.Tmpl().ExecuteWithContext(ctx.Ctx, templ, ctx.To, t.data)
+ return th.ExecuteWithContext(ctx.Ctx, ti, ctx.To, t.data)
}
func (c *Client) ExecuteAsTemplate(ctx context.Context, res resources.ResourceTransformer, targetPath string, data any) (resource.Resource, error) {
diff --git a/resources/resource_transformers/tocss/dartsass/client.go b/resources/resource_transformers/tocss/dartsass/client.go
index e6f5567e4..965232ad4 100644
--- a/resources/resource_transformers/tocss/dartsass/client.go
+++ b/resources/resource_transformers/tocss/dartsass/client.go
@@ -20,7 +20,6 @@ import (
"io"
"strings"
- godartsassv1 "github.com/bep/godartsass"
"github.com/bep/godartsass/v2"
"github.com/bep/logg"
"github.com/gohugoio/hugo/common/herrors"
@@ -45,89 +44,71 @@ const dartSassStdinPrefix = "hugostdin:"
func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) (*Client, error) {
if !Supports() {
- return &Client{dartSassNotAvailable: true}, nil
+ return &Client{}, nil
}
if hugo.DartSassBinaryName == "" {
return nil, fmt.Errorf("no Dart Sass binary found in $PATH")
}
+ if !hugo.IsDartSassGeV2() {
+ return nil, fmt.Errorf("unsupported Dart Sass version detected, please upgrade to Dart Sass 1.63.0 or later, see https://gohugo.io/functions/css/sass/#dart-sass")
+ }
+
if err := rs.ExecHelper.Sec().CheckAllowedExec(hugo.DartSassBinaryName); err != nil {
return nil, err
}
var (
- transpiler *godartsass.Transpiler
- transpilerv1 *godartsassv1.Transpiler
- err error
- infol = rs.Logger.InfoCommand("Dart Sass")
- warnl = rs.Logger.WarnCommand("Dart Sass")
+ transpiler *godartsass.Transpiler
+ err error
+ infol = rs.Logger.InfoCommand("Dart Sass")
+ warnl = rs.Logger.WarnCommand("Dart Sass")
)
- if hugo.IsDartSassV2() {
- transpiler, err = godartsass.Start(godartsass.Options{
- DartSassEmbeddedFilename: hugo.DartSassBinaryName,
- LogEventHandler: func(event godartsass.LogEvent) {
- message := strings.ReplaceAll(event.Message, dartSassStdinPrefix, "")
- switch event.Type {
- case godartsass.LogEventTypeDebug:
- // Log as Info for now, we may adjust this if it gets too chatty.
- infol.Log(logg.String(message))
- default:
- // The rest are either deprecations or @warn statements.
- warnl.Log(logg.String(message))
- }
- },
- })
- } else {
- transpilerv1, err = godartsassv1.Start(godartsassv1.Options{
- DartSassEmbeddedFilename: hugo.DartSassBinaryName,
- LogEventHandler: func(event godartsassv1.LogEvent) {
- message := strings.ReplaceAll(event.Message, dartSassStdinPrefix, "")
- switch event.Type {
- case godartsassv1.LogEventTypeDebug:
- // Log as Info for now, we may adjust this if it gets too chatty.
- infol.Log(logg.String(message))
- default:
- // The rest are either deprecations or @warn statements.
- warnl.Log(logg.String(message))
- }
- },
- })
- }
-
+ transpiler, err = godartsass.Start(godartsass.Options{
+ DartSassEmbeddedFilename: hugo.DartSassBinaryName,
+ LogEventHandler: func(event godartsass.LogEvent) {
+ message := strings.ReplaceAll(event.Message, dartSassStdinPrefix, "")
+ switch event.Type {
+ case godartsass.LogEventTypeDebug:
+ // Log as Info for now, we may adjust this if it gets too chatty.
+ infol.Log(logg.String(message))
+ case godartsass.LogEventTypeDeprecated:
+ warnl.Logf("DEPRECATED [%s]: %s", event.DeprecationType, message)
+ default:
+ // The rest are @warn statements.
+ warnl.Log(logg.String(message))
+ }
+ },
+ })
if err != nil {
return nil, err
}
- return &Client{sfs: fs, workFs: rs.BaseFs.Work, rs: rs, transpiler: transpiler, transpilerV1: transpilerv1}, nil
+ return &Client{sfs: fs, workFs: rs.BaseFs.Work, rs: rs, transpiler: transpiler}, nil
}
type Client struct {
- dartSassNotAvailable bool
- rs *resources.Spec
- sfs *filesystems.SourceFilesystem
- workFs afero.Fs
+ rs *resources.Spec
+ sfs *filesystems.SourceFilesystem
+ workFs afero.Fs
- // One of these are non-nil.
- transpiler *godartsass.Transpiler
- transpilerV1 *godartsassv1.Transpiler
+ // This may be nil if Dart Sass is not available.
+ transpiler *godartsass.Transpiler
}
func (c *Client) ToCSS(res resources.ResourceTransformer, args map[string]any) (resource.Resource, error) {
- if c.dartSassNotAvailable {
+ if c.transpiler == nil {
return res.Transform(resources.NewFeatureNotAvailableTransformer(transformationName, args))
}
return res.Transform(&transform{c: c, optsm: args})
}
func (c *Client) Close() error {
- if c.transpilerV1 != nil {
- return c.transpilerV1.Close()
+ if c.transpiler == nil {
+ return nil
}
- if c.transpiler != nil {
- return c.transpiler.Close()
- }
- return nil
+ return c.transpiler.Close()
}
func (c *Client) toCSS(args godartsass.Args, src io.Reader) (godartsass.Result, error) {
@@ -135,26 +116,7 @@ func (c *Client) toCSS(args godartsass.Args, src io.Reader) (godartsass.Result,
args.Source = in
- var (
- err error
- res godartsass.Result
- )
-
- if c.transpilerV1 != nil {
- var resv1 godartsassv1.Result
- var argsv1 godartsassv1.Args
- mapstructure.Decode(args, &argsv1)
- if args.ImportResolver != nil {
- argsv1.ImportResolver = importResolverV1{args.ImportResolver}
- }
- resv1, err = c.transpilerV1.Execute(argsv1)
- if err == nil {
- mapstructure.Decode(resv1, &res)
- }
- } else {
- res, err = c.transpiler.Execute(args)
- }
-
+ res, err := c.transpiler.Execute(args)
if err != nil {
if err.Error() == "unexpected EOF" {
//lint:ignore ST1005 end user message.
@@ -194,6 +156,16 @@ type Options struct {
// @use "hugo:vars";
// $color: vars.$color;
Vars map[string]any
+
+ // Deprecations IDs in this slice will be silenced.
+ // The IDs can be found in the Dart Sass log output, e.g. "import" in
+ // WARN Dart Sass: DEPRECATED [import].
+ SilenceDeprecations []string
+
+ // Whether to silence deprecation warnings from dependencies, where a
+ // dependency is considered any file transitively imported through a load
+ // path. This does not apply to @warn or @debug rules.
+ SilenceDependencyDeprecations bool
}
func decodeOptions(m map[string]any) (opts Options, err error) {
diff --git a/resources/resource_transformers/tocss/dartsass/dartsass_integration_test.go b/resources/resource_transformers/tocss/dartsass/dartsass_integration_test.go
index 4d48b3b6a..89d503d36 100644
--- a/resources/resource_transformers/tocss/dartsass/dartsass_integration_test.go
+++ b/resources/resource_transformers/tocss/dartsass/dartsass_integration_test.go
@@ -562,3 +562,145 @@ Styles: {{ $r.RelPermalink }}
b.AssertFileContent("public/index.html", "Styles: /scss/main.css")
}
+
+// Issue 12849
+func TestDirectoryIndexes(t *testing.T) {
+ t.Parallel()
+ if !dartsass.Supports() {
+ t.Skip()
+ }
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','section','rss','sitemap','taxonomy','term']
+
+[[module.mounts]]
+source = 'assets'
+target = 'assets'
+[[module.mounts]]
+source = "miscellaneous/sass"
+target = "assets/sass"
+-- layouts/index.html --
+{{ $opts := dict "transpiler" "dartsass" "outputStyle" "compressed" }}
+{{ (resources.Get "sass/main.scss" | toCSS $opts).Content }}
+-- assets/sass/main.scss --
+@use "foo1"; // directory with _index file from OS file system
+@use "bar1"; // directory with _index file from module mount
+@use "foo2"; // directory with index file from OS file system
+@use "bar2"; // directory with index file from module mount
+-- assets/sass/foo1/_index.scss --
+.foo1 {color: red;}
+-- miscellaneous/sass/bar1/_index.scss --
+.bar1 {color: blue;}
+-- assets/sass/foo2/index.scss --
+.foo2 {color: red;}
+-- miscellaneous/sass/bar2/index.scss --
+.bar2 {color: blue;}
+`
+
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ NeedsOsFS: true,
+ TxtarString: files,
+ }).Build()
+
+ b.AssertFileContent("public/index.html", ".foo1{color:red}.bar1{color:blue}.foo2{color:red}.bar2{color:blue}")
+}
+
+func TestIgnoreDeprecationWarnings(t *testing.T) {
+ t.Parallel()
+ if !dartsass.Supports() {
+ t.Skip()
+ }
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','section','rss','sitemap','taxonomy','term']
+-- assets/scss/main.scss --
+@import "moo";
+-- node_modules/foo/_moo.scss --
+$moolor: #fff;
+
+moo {
+ color: $moolor;
+}
+-- config.toml --
+-- layouts/index.html --
+{{ $cssOpts := (dict "includePaths" (slice "node_modules/foo") "transpiler" "dartsass" ) }}
+{{ $r := resources.Get "scss/main.scss" | toCSS $cssOpts | minify }}
+T1: {{ $r.Content }}
+ `
+
+ b := hugolib.Test(t, files, hugolib.TestOptOsFs(), hugolib.TestOptWarn())
+ b.AssertLogContains("Dart Sass: DEPRECATED [import]")
+ b.AssertFileContent("public/index.html", `moo{color:#fff}`)
+
+ files = strings.ReplaceAll(files, `"transpiler" "dartsass"`, `"transpiler" "dartsass" "silenceDeprecations" (slice "import")`)
+
+ b = hugolib.Test(t, files, hugolib.TestOptOsFs(), hugolib.TestOptWarn())
+ b.AssertLogContains("! Dart Sass: DEPRECATED [import]")
+ b.AssertFileContent("public/index.html", `moo{color:#fff}`)
+}
+
+func TestSilenceDependencyDeprecations(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- layouts/index.html --
+{{ $opts := dict
+ "transpiler" "dartsass"
+ "outputStyle" "compressed"
+ "includePaths" (slice "node_modules")
+ KVPAIR
+}}
+{{ (resources.Get "sass/main.scss" | css.Sass $opts).Content }}
+-- assets/sass/main.scss --
+@use "sass:color";
+@use "foo/deprecated.scss";
+h3 { color: rgb(color.channel(#ccc, "red", $space: rgb), 0, 0); }
+// COMMENT
+-- node_modules/foo/deprecated.scss --
+@use "sass:color";
+h1 { color: rgb(color.channel(#eee, "red", $space: rgb), 0, 0); }
+h2 { color: rgb(color.red(#ddd), 0, 0); } // deprecated
+`
+
+ expectedCSS := "h1{color:#e00}h2{color:#d00}h3{color:#c00}"
+
+ // Do not silence dependency deprecation warnings (default).
+ f := strings.ReplaceAll(files, "KVPAIR", "")
+ b := hugolib.Test(t, f, hugolib.TestOptWarn(), hugolib.TestOptOsFs())
+ b.AssertFileContent("public/index.html", expectedCSS)
+ b.AssertLogContains(
+ "WARN Dart Sass: DEPRECATED [color-functions]",
+ "color.red() is deprecated",
+ )
+
+ // Do not silence dependency deprecation warnings (explicit).
+ f = strings.ReplaceAll(files, "KVPAIR", `"silenceDependencyDeprecations" false`)
+ b = hugolib.Test(t, f, hugolib.TestOptWarn(), hugolib.TestOptOsFs())
+ b.AssertFileContent("public/index.html", expectedCSS)
+ b.AssertLogContains(
+ "WARN Dart Sass: DEPRECATED [color-functions]",
+ "color.red() is deprecated",
+ )
+
+ // Silence dependency deprecation warnings.
+ f = strings.ReplaceAll(files, "KVPAIR", `"silenceDependencyDeprecations" true`)
+ b = hugolib.Test(t, f, hugolib.TestOptWarn(), hugolib.TestOptOsFs())
+ b.AssertFileContent("public/index.html", expectedCSS)
+ b.AssertLogContains("! WARN")
+
+ // Make sure that we are not silencing non-dependency deprecation warnings.
+ f = strings.ReplaceAll(files, "KVPAIR", `"silenceDependencyDeprecations" true`)
+ f = strings.ReplaceAll(f, "// COMMENT", "h4 { color: rgb(0, color.green(#bbb), 0); }")
+ b = hugolib.Test(t, f, hugolib.TestOptWarn(), hugolib.TestOptOsFs())
+ b.AssertFileContent("public/index.html", expectedCSS+"h4{color:#0b0}")
+ b.AssertLogContains(
+ "WARN Dart Sass: DEPRECATED [color-functions]",
+ "color.green() is deprecated",
+ )
+}
diff --git a/resources/resource_transformers/tocss/dartsass/transform.go b/resources/resource_transformers/tocss/dartsass/transform.go
index 52d24da7d..e1e9b0be0 100644
--- a/resources/resource_transformers/tocss/dartsass/transform.go
+++ b/resources/resource_transformers/tocss/dartsass/transform.go
@@ -29,17 +29,16 @@ import (
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/internal"
- "github.com/gohugoio/hugo/resources/resource_transformers/tocss/internal/sass"
+ "github.com/gohugoio/hugo/resources/resource_transformers/tocss/sass"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/hugofs"
- godartsassv1 "github.com/bep/godartsass"
"github.com/bep/godartsass/v2"
)
-// Supports returns whether dart-sass-embedded is found in $PATH.
+// Supports returns whether sass, dart-sass, or dart-sass-embedded is found in $PATH.
func Supports() bool {
if htesting.SupportsAll() {
return true
@@ -85,11 +84,13 @@ func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error {
c: t.c,
dependencyManager: ctx.DependencyManager,
- varsStylesheet: godartsass.Import{Content: sass.CreateVarsStyleSheet(opts.Vars)},
+ varsStylesheet: godartsass.Import{Content: sass.CreateVarsStyleSheet(sass.TranspilerDart, opts.Vars)},
},
- OutputStyle: godartsass.ParseOutputStyle(opts.OutputStyle),
- EnableSourceMap: opts.EnableSourceMap,
- SourceMapIncludeSources: opts.SourceMapIncludeSources,
+ OutputStyle: godartsass.ParseOutputStyle(opts.OutputStyle),
+ EnableSourceMap: opts.EnableSourceMap,
+ SourceMapIncludeSources: opts.SourceMapIncludeSources,
+ SilenceDeprecations: opts.SilenceDeprecations,
+ SilenceDependencyDeprecations: opts.SilenceDependencyDeprecations,
}
// Append any workDir relative include paths
@@ -139,7 +140,7 @@ func (t importResolver) CanonicalizeURL(url string) (string, error) {
return url, nil
}
- filePath, isURL := paths.UrlToFilename(url)
+ filePath, isURL := paths.UrlStringToFilename(url)
var prevDir string
var pathDir string
if isURL {
@@ -165,7 +166,13 @@ func (t importResolver) CanonicalizeURL(url string) (string, error) {
} else if strings.HasPrefix(name, "_") {
namePatterns = []string{"_%s.scss", "_%s.sass", "_%s.css"}
} else {
- namePatterns = []string{"_%s.scss", "%s.scss", "_%s.sass", "%s.sass", "_%s.css", "%s.css"}
+ namePatterns = []string{
+ "_%s.scss", "%s.scss",
+ "_%s.sass", "%s.sass",
+ "_%s.css", "%s.css",
+ "%s/_index.scss", "%s/_index.sass",
+ "%s/index.scss", "%s/index.sass",
+ }
}
name = strings.TrimPrefix(name, "_")
@@ -189,7 +196,7 @@ func (t importResolver) Load(url string) (godartsass.Import, error) {
if url == sass.HugoVarsNamespace {
return t.varsStylesheet, nil
}
- filename, _ := paths.UrlToFilename(url)
+ filename, _ := paths.UrlStringToFilename(url)
b, err := afero.ReadFile(hugofs.Os, filename)
sourceSyntax := godartsass.SourceSyntaxSCSS
@@ -201,12 +208,3 @@ func (t importResolver) Load(url string) (godartsass.Import, error) {
return godartsass.Import{Content: string(b), SourceSyntax: sourceSyntax}, err
}
-
-type importResolverV1 struct {
- godartsass.ImportResolver
-}
-
-func (t importResolverV1) Load(url string) (godartsassv1.Import, error) {
- res, err := t.ImportResolver.Load(url)
- return godartsassv1.Import{Content: res.Content, SourceSyntax: godartsassv1.SourceSyntax(res.SourceSyntax)}, err
-}
diff --git a/resources/resource_transformers/tocss/internal/sass/helpers.go b/resources/resource_transformers/tocss/sass/helpers.go
similarity index 75%
rename from resources/resource_transformers/tocss/internal/sass/helpers.go
rename to resources/resource_transformers/tocss/sass/helpers.go
index c1cef141e..d4091a39c 100644
--- a/resources/resource_transformers/tocss/internal/sass/helpers.go
+++ b/resources/resource_transformers/tocss/sass/helpers.go
@@ -24,9 +24,14 @@ import (
const (
HugoVarsNamespace = "hugo:vars"
+ // Transpiler implementation can be controlled from the client by
+ // setting the 'transpiler' option.
+ // Default is currently 'libsass', but that may change.
+ TranspilerDart = "dartsass"
+ TranspilerLibSass = "libsass"
)
-func CreateVarsStyleSheet(vars map[string]any) string {
+func CreateVarsStyleSheet(transpiler string, vars map[string]any) string {
if vars == nil {
return ""
}
@@ -49,12 +54,22 @@ func CreateVarsStyleSheet(vars map[string]any) string {
varsSlice = append(varsSlice, fmt.Sprintf("%s%s: %v;", prefix, k, v))
} else {
// unquote will preserve quotes around URLs etc. if needed.
- varsSlice = append(varsSlice, fmt.Sprintf("%s%s: unquote(%q);", prefix, k, v))
+ if transpiler == TranspilerDart {
+ varsSlice = append(varsSlice, fmt.Sprintf("%s%s: string.unquote(%q);", prefix, k, v))
+ } else {
+ varsSlice = append(varsSlice, fmt.Sprintf("%s%s: unquote(%q);", prefix, k, v))
+ }
}
}
}
sort.Strings(varsSlice)
- varsStylesheet = strings.Join(varsSlice, "\n")
+
+ if transpiler == TranspilerDart {
+ varsStylesheet = `@use "sass:string";` + "\n" + strings.Join(varsSlice, "\n")
+ } else {
+ varsStylesheet = strings.Join(varsSlice, "\n")
+ }
+
return varsStylesheet
}
diff --git a/resources/resource_transformers/tocss/internal/sass/helpers_test.go b/resources/resource_transformers/tocss/sass/helpers_test.go
similarity index 100%
rename from resources/resource_transformers/tocss/internal/sass/helpers_test.go
rename to resources/resource_transformers/tocss/sass/helpers_test.go
diff --git a/resources/resource_transformers/tocss/scss/client_extended.go b/resources/resource_transformers/tocss/scss/client_extended.go
index c9b347c4c..bd2330f6d 100644
--- a/resources/resource_transformers/tocss/scss/client_extended.go
+++ b/resources/resource_transformers/tocss/scss/client_extended.go
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build extended
-// +build extended
package scss
diff --git a/resources/resource_transformers/tocss/scss/client_notavailable.go b/resources/resource_transformers/tocss/scss/client_notavailable.go
index efd79109b..783a7d7db 100644
--- a/resources/resource_transformers/tocss/scss/client_notavailable.go
+++ b/resources/resource_transformers/tocss/scss/client_notavailable.go
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build !extended
-// +build !extended
package scss
diff --git a/resources/resource_transformers/tocss/scss/scss_integration_test.go b/resources/resource_transformers/tocss/scss/scss_integration_test.go
index 469463872..0154a4634 100644
--- a/resources/resource_transformers/tocss/scss/scss_integration_test.go
+++ b/resources/resource_transformers/tocss/scss/scss_integration_test.go
@@ -111,7 +111,7 @@ moo {
@import "another.css";
/* foo */
-
+
`)
}
@@ -262,7 +262,7 @@ body {
body {
background: url($image) no-repeat center/cover;
font-family: $font;
- }
+ }
}
p {
@@ -396,7 +396,7 @@ h3 {
{{ range $stylesheets }}
- {{ with . | resources.ToCSS | fingerprint }}
+ {{ with . | css.Sass | fingerprint }}
{{ end }}
{{ end }}
@@ -417,3 +417,48 @@ h3 {
b.AssertFileContent("public/index.html", `b.46b2d77c7ffe37ee191678f72df991ecb1319f849957151654362f09b0ef467f.css`)
}
+
+// Issue 12851
+func TestDirectoryIndexes(t *testing.T) {
+ t.Parallel()
+ if !scss.Supports() {
+ t.Skip()
+ }
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','section','rss','sitemap','taxonomy','term']
+
+[[module.mounts]]
+source = 'assets'
+target = 'assets'
+[[module.mounts]]
+source = "miscellaneous/sass"
+target = "assets/sass"
+-- layouts/index.html --
+{{ $opts := dict "transpiler" "libsass" "outputStyle" "compressed" }}
+{{ (resources.Get "sass/main.scss" | toCSS $opts).Content }}
+-- assets/sass/main.scss --
+@import "foo1"; // directory with _index file from OS file system
+@import "bar1"; // directory with _index file from module mount
+@import "foo2"; // directory with index file from OS file system
+@import "bar2"; // directory with index file from module mount
+-- assets/sass/foo1/_index.scss --
+.foo1 {color: red;}
+-- miscellaneous/sass/bar1/_index.scss --
+.bar1 {color: blue;}
+-- assets/sass/foo2/index.scss --
+.foo2 {color: red;}
+-- miscellaneous/sass/bar2/index.scss --
+.bar2 {color: blue;}
+`
+
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ NeedsOsFS: true,
+ TxtarString: files,
+ }).Build()
+
+ b.AssertFileContent("public/index.html", ".foo1{color:red}.bar1{color:blue}.foo2{color:red}.bar2{color:blue}")
+}
diff --git a/resources/resource_transformers/tocss/scss/tocss.go b/resources/resource_transformers/tocss/scss/tocss.go
index 3a46e6016..2976578af 100644
--- a/resources/resource_transformers/tocss/scss/tocss.go
+++ b/resources/resource_transformers/tocss/scss/tocss.go
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build extended
-// +build extended
package scss
@@ -31,7 +30,7 @@ import (
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources"
- "github.com/gohugoio/hugo/resources/resource_transformers/tocss/internal/sass"
+ "github.com/gohugoio/hugo/resources/resource_transformers/tocss/sass"
)
// Used in tests. This feature requires Hugo to be built with the extended tag.
@@ -64,7 +63,7 @@ func (t *toCSSTransformation) Transform(ctx *resources.ResourceTransformationCtx
}
}
- varsStylesheet := sass.CreateVarsStyleSheet(options.from.Vars)
+ varsStylesheet := sass.CreateVarsStyleSheet(sass.TranspilerLibSass, options.from.Vars)
// To allow for overrides of SCSS files anywhere in the project/theme hierarchy, we need
// to help libsass revolve the filename by looking in the composite filesystem first.
@@ -105,7 +104,12 @@ func (t *toCSSTransformation) Transform(ctx *resources.ResourceTransformationCtx
} else if strings.HasPrefix(name, "_") {
namePatterns = []string{"_%s.scss", "_%s.sass"}
} else {
- namePatterns = []string{"_%s.scss", "%s.scss", "_%s.sass", "%s.sass"}
+ namePatterns = []string{
+ "_%s.scss", "%s.scss",
+ "_%s.sass", "%s.sass",
+ "%s/_index.scss", "%s/_index.sass",
+ "%s/index.scss", "%s/index.sass",
+ }
}
name = strings.TrimPrefix(name, "_")
diff --git a/resources/resources_integration_test.go b/resources/resources_integration_test.go
index ba580b4dc..0d02b45d5 100644
--- a/resources/resources_integration_test.go
+++ b/resources/resources_integration_test.go
@@ -27,6 +27,7 @@ func TestImageCache(t *testing.T) {
files := `
-- config.toml --
+disableLiveReload = true
baseURL = "https://example.org"
-- content/mybundle/index.md --
---
@@ -61,9 +62,9 @@ anigif: {{ $anigif.RelPermalink }}|{{ $anigif.Width }}|{{ $anigif.Height }}|{{ $
assertImages := func() {
b.AssertFileContent("public/index.html", `
- gif: /mybundle/pixel_hu8aa3346827e49d756ff4e630147c42b5_70_1x2_resize_box_3.gif|}|1|2|image/gif|
- bmp: /mybundle/pixel_hu8aa3346827e49d756ff4e630147c42b5_70_2x3_resize_box_3.bmp|}|2|3|image/bmp|
- anigif: /mybundle/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_4x5_resize_box_1.gif|4|5|image/gif|
+ gif: /mybundle/pixel_hu_93429543fc146fce.gif|}|1|2|image/gif|
+bmp: /mybundle/pixel_hu_f9bf2acd6578e2c6.bmp|}|2|3|image/bmp|
+anigif: /mybundle/giphy_hu_652d28653068b48f.gif|4|5|image/gif|
`)
}
@@ -121,7 +122,7 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA
`
- for i := 0; i < 3; i++ {
+ for range 3 {
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
@@ -159,9 +160,9 @@ resize 2|RelPermalink: {{ $image.RelPermalink }}|MediaType: {{ $image.MediaType
b := hugolib.Test(t, files)
b.AssertFileContent("public/index.html",
- "jpg|RelPermalink: /images/pixel_hu8aa3346827e49d756ff4e630147c42b5_70_filter_17010532266664966692.jpg|MediaType: image/jpeg|Width: 1|Height: 1|",
- "resize 1|RelPermalink: /images/pixel_hu8aa3346827e49d756ff4e630147c42b5_70_filter_6707036659822075562.jpg|MediaType: image/jpeg|Width: 20|Height: 30|",
- "resize 2|RelPermalink: /images/pixel_hu8aa3346827e49d756ff4e630147c42b5_70_filter_6707036659822075562.jpg|MediaType: image/jpeg|Width: 20|Height: 30|",
+ "jpg|RelPermalink: /images/pixel_hu_38c3f257174fc757.jpg|MediaType: image/jpeg|Width: 1|Height: 1|",
+ "resize 1|RelPermalink: /images/pixel_hu_b5c2a3d88991f65a.jpg|MediaType: image/jpeg|Width: 20|Height: 30|",
+ "resize 2|RelPermalink: /images/pixel_hu_b5c2a3d88991f65a.jpg|MediaType: image/jpeg|Width: 20|Height: 30|",
)
}
diff --git a/resources/testdata/exif/orientation6.jpg b/resources/testdata/exif/orientation6.jpg
new file mode 100644
index 000000000..4e2c86415
Binary files /dev/null and b/resources/testdata/exif/orientation6.jpg differ
diff --git a/resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_100x0_resize_q75_bgffffff_box_1.jpg b/resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_100x0_resize_q75_bgffffff_box_1.jpg
deleted file mode 100644
index c55e5d3ad..000000000
Binary files a/resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_100x0_resize_q75_bgffffff_box_1.jpg and /dev/null differ
diff --git a/resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_200x0_resize_box_1.gif b/resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_200x0_resize_box_1.gif
deleted file mode 100644
index ca826432c..000000000
Binary files a/resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_200x0_resize_box_1.gif and /dev/null differ
diff --git a/resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_512x0_resize_box_1.gif b/resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_512x0_resize_box_1.gif
deleted file mode 100644
index 590d2a780..000000000
Binary files a/resources/testdata/golden/giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_512x0_resize_box_1.gif and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio-card_hu4d09f75255d3942fd4680641110a1a73_10820_100x0_resize_box_1.gif b/resources/testdata/golden/gohugoio-card_hu4d09f75255d3942fd4680641110a1a73_10820_100x0_resize_box_1.gif
deleted file mode 100644
index 7d810c1f9..000000000
Binary files a/resources/testdata/golden/gohugoio-card_hu4d09f75255d3942fd4680641110a1a73_10820_100x0_resize_box_1.gif and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio-card_hu4d09f75255d3942fd4680641110a1a73_10820_220x0_resize_box_1.gif b/resources/testdata/golden/gohugoio-card_hu4d09f75255d3942fd4680641110a1a73_10820_220x0_resize_box_1.gif
deleted file mode 100644
index c4b39b041..000000000
Binary files a/resources/testdata/golden/gohugoio-card_hu4d09f75255d3942fd4680641110a1a73_10820_220x0_resize_box_1.gif and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_100x100_fill_box_center_3.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_100x100_fill_box_center_3.png
deleted file mode 100644
index d2f0afd27..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_100x100_fill_box_center_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_1a923841aa34545db29f46a8fc4c5b0d.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_1a923841aa34545db29f46a8fc4c5b0d.png
deleted file mode 100644
index a48a0f25a..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_1a923841aa34545db29f46a8fc4c5b0d.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_200x0_resize_q50_r90_box_3.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_200x0_resize_q50_r90_box_3.png
deleted file mode 100644
index 5abf378b4..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_200x0_resize_q50_r90_box_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_200x100_resize_box_3.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_200x100_resize_box_3.png
deleted file mode 100644
index cd56200ea..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_200x100_resize_box_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_300x100_fill_nearestneighbor_topleft_3.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_300x100_fill_nearestneighbor_topleft_3.png
deleted file mode 100644
index dd11ce7ed..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_300x100_fill_nearestneighbor_topleft_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_300x200_fill_gaussian_smart1_3.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_300x200_fill_gaussian_smart1_3.png
deleted file mode 100644
index 4ef633564..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_300x200_fill_gaussian_smart1_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_300x200_fit_linear_3.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_300x200_fit_linear_3.png
deleted file mode 100644
index 5ad74bf79..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_300x200_fit_linear_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_324b4d42c8746a684068d123fad8b744.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_324b4d42c8746a684068d123fad8b744.png
deleted file mode 100644
index eba4b1e66..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_324b4d42c8746a684068d123fad8b744.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_400x200_fill_box_bottomleft_3.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_400x200_fill_box_bottomleft_3.png
deleted file mode 100644
index 76deeabc7..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_400x200_fill_box_bottomleft_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_400x200_fill_box_center_3.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_400x200_fill_box_center_3.png
deleted file mode 100644
index 76deeabc7..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_400x200_fill_box_center_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_43055c40cb4a15bd8491bfc502799f43.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_43055c40cb4a15bd8491bfc502799f43.png
deleted file mode 100644
index 0ce82e49c..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_43055c40cb4a15bd8491bfc502799f43.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_4ea8f246299cc5fba9744bdf162bd57d.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_4ea8f246299cc5fba9744bdf162bd57d.png
deleted file mode 100644
index 841d369ef..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_4ea8f246299cc5fba9744bdf162bd57d.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_600x0_resize_box_3.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_600x0_resize_box_3.png
deleted file mode 100644
index 28028b72d..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_600x0_resize_box_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_60c098f0ca6626668d9e3ad6bfb38b5b.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_60c098f0ca6626668d9e3ad6bfb38b5b.png
deleted file mode 100644
index 46fa3fd1b..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_60c098f0ca6626668d9e3ad6bfb38b5b.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_6575f3a3c39a30cba9d76a6045c36de6.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_6575f3a3c39a30cba9d76a6045c36de6.png
deleted file mode 100644
index 056648a74..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_6575f3a3c39a30cba9d76a6045c36de6.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_8166ccaf22bdabb94c9bb90bffe64133.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_8166ccaf22bdabb94c9bb90bffe64133.png
deleted file mode 100644
index 2fece7804..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_8166ccaf22bdabb94c9bb90bffe64133.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_9165e5559db8ba31a401327b5617c098.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_9165e5559db8ba31a401327b5617c098.png
deleted file mode 100644
index 50fae767a..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_9165e5559db8ba31a401327b5617c098.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_9a8d95423df65a9c230a4cc88056c13a.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_9a8d95423df65a9c230a4cc88056c13a.png
deleted file mode 100644
index 32c5b49d8..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_9a8d95423df65a9c230a4cc88056c13a.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_a38a1924befb1721a09be7d432f5f70f.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_a38a1924befb1721a09be7d432f5f70f.png
deleted file mode 100644
index 603b95ae0..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_a38a1924befb1721a09be7d432f5f70f.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_a487ef4bea3dba1e1a84be5358cfef39.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_a487ef4bea3dba1e1a84be5358cfef39.png
deleted file mode 100644
index dde14757c..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_a487ef4bea3dba1e1a84be5358cfef39.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_a751b6cd969d7feab12540a8bb0ca927.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_a751b6cd969d7feab12540a8bb0ca927.png
deleted file mode 100644
index 93f8dfda2..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_a751b6cd969d7feab12540a8bb0ca927.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_abcdd770eaed9301cfff4bc2f96459ba.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_abcdd770eaed9301cfff4bc2f96459ba.png
deleted file mode 100644
index 0991ca984..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_abcdd770eaed9301cfff4bc2f96459ba.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_aeaaf23afe6fb4702bd3992426d0cad3.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_aeaaf23afe6fb4702bd3992426d0cad3.png
deleted file mode 100644
index ce791767f..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_aeaaf23afe6fb4702bd3992426d0cad3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_bdde5e36f15689c1451933f92fd357b3.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_bdde5e36f15689c1451933f92fd357b3.png
deleted file mode 100644
index 25ac82485..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_bdde5e36f15689c1451933f92fd357b3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_d111079da5d8d143b6cae10d6fedbc24.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_d111079da5d8d143b6cae10d6fedbc24.png
deleted file mode 100644
index 362be673b..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_d111079da5d8d143b6cae10d6fedbc24.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_d87fd348ad697a9b16399709441d9d56.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_d87fd348ad697a9b16399709441d9d56.png
deleted file mode 100644
index 174649232..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_d87fd348ad697a9b16399709441d9d56.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_e8ef2efdde4357a79694ea9c2be82f63.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_e8ef2efdde4357a79694ea9c2be82f63.png
deleted file mode 100644
index 697ac914e..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_e8ef2efdde4357a79694ea9c2be82f63.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_ee57777f148caaa6993972d9709fdf2d.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_ee57777f148caaa6993972d9709fdf2d.png
deleted file mode 100644
index c1a64b59f..000000000
Binary files a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_ee57777f148caaa6993972d9709fdf2d.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_00cd4ff18b53ecbd78e42aefe5fbf522.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_00cd4ff18b53ecbd78e42aefe5fbf522.png
deleted file mode 100644
index 1fa2bc9de..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_00cd4ff18b53ecbd78e42aefe5fbf522.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_100x100_fill_box_center_3.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_100x100_fill_box_center_3.png
deleted file mode 100644
index 0eef0aaf3..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_100x100_fill_box_center_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_200x0_resize_q50_r90_box_3.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_200x0_resize_q50_r90_box_3.png
deleted file mode 100644
index c35f00722..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_200x0_resize_q50_r90_box_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_200x100_resize_box_3.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_200x100_resize_box_3.png
deleted file mode 100644
index 6ddb55158..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_200x100_resize_box_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_276650b97daa7ae98e79b929d7f87c19.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_276650b97daa7ae98e79b929d7f87c19.png
deleted file mode 100644
index 0b914391c..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_276650b97daa7ae98e79b929d7f87c19.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_2e05d39f4cb329be10e8c515494cef76.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_2e05d39f4cb329be10e8c515494cef76.png
deleted file mode 100644
index 795a608e8..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_2e05d39f4cb329be10e8c515494cef76.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_300x100_fill_nearestneighbor_topleft_3.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_300x100_fill_nearestneighbor_topleft_3.png
deleted file mode 100644
index 08eccf7cd..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_300x100_fill_nearestneighbor_topleft_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_300x200_fill_gaussian_smart1_3.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_300x200_fill_gaussian_smart1_3.png
deleted file mode 100644
index 162dc4ec9..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_300x200_fill_gaussian_smart1_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_300x200_fit_linear_3.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_300x200_fit_linear_3.png
deleted file mode 100644
index 0660c20d7..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_300x200_fit_linear_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_3980c5868e0b6f20ec95424dfdcb1d67.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_3980c5868e0b6f20ec95424dfdcb1d67.png
deleted file mode 100644
index 7134de473..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_3980c5868e0b6f20ec95424dfdcb1d67.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_398ca764abfff83bb15318068105dcb9.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_398ca764abfff83bb15318068105dcb9.png
deleted file mode 100644
index 37dc0f798..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_398ca764abfff83bb15318068105dcb9.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_3ed273f49d1dc83891f5736e21fc5f44.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_3ed273f49d1dc83891f5736e21fc5f44.png
deleted file mode 100644
index 1a229a429..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_3ed273f49d1dc83891f5736e21fc5f44.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_400x200_fill_box_bottomleft_3.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_400x200_fill_box_bottomleft_3.png
deleted file mode 100644
index acde6a0f7..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_400x200_fill_box_bottomleft_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_400x200_fill_box_center_3.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_400x200_fill_box_center_3.png
deleted file mode 100644
index acde6a0f7..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_400x200_fill_box_center_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_428e769d14483c2fcdd6f5c5138e2066.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_428e769d14483c2fcdd6f5c5138e2066.png
deleted file mode 100644
index c96e04108..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_428e769d14483c2fcdd6f5c5138e2066.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_600x0_resize_box_3.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_600x0_resize_box_3.png
deleted file mode 100644
index 40fffa23a..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_600x0_resize_box_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_73c19c5f80881858a85aa23cd0ca400d.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_73c19c5f80881858a85aa23cd0ca400d.png
deleted file mode 100644
index 51f6cfa7e..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_73c19c5f80881858a85aa23cd0ca400d.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_871826faffc414ca3746f65fc9910eed.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_871826faffc414ca3746f65fc9910eed.png
deleted file mode 100644
index 53dd0b224..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_871826faffc414ca3746f65fc9910eed.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_a0505112c99af88626ac9b9a16a27acb.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_a0505112c99af88626ac9b9a16a27acb.png
deleted file mode 100644
index 156b42f43..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_a0505112c99af88626ac9b9a16a27acb.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_a0ffc0f22f22e6920f3cad414d6db6ba.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_a0ffc0f22f22e6920f3cad414d6db6ba.png
deleted file mode 100644
index a5852e14c..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_a0ffc0f22f22e6920f3cad414d6db6ba.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_ae631e5252bb5d7b92bc766ad1a89069.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_ae631e5252bb5d7b92bc766ad1a89069.png
deleted file mode 100644
index c8f782598..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_ae631e5252bb5d7b92bc766ad1a89069.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_b34412412a1cf1658e516a335b0a8dd4.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_b34412412a1cf1658e516a335b0a8dd4.png
deleted file mode 100644
index c29c6e613..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_b34412412a1cf1658e516a335b0a8dd4.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_c5140f11378ddb13843432a5b489594a.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_c5140f11378ddb13843432a5b489594a.png
deleted file mode 100644
index 09d991972..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_c5140f11378ddb13843432a5b489594a.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_d17d0184674fcf0a4d770c90bed503db.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_d17d0184674fcf0a4d770c90bed503db.png
deleted file mode 100644
index 325c31acd..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_d17d0184674fcf0a4d770c90bed503db.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_d1bbfa2629bffb90118cacce3fcfb924.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_d1bbfa2629bffb90118cacce3fcfb924.png
deleted file mode 100644
index 2def214c8..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_d1bbfa2629bffb90118cacce3fcfb924.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_e4d38674b70d9ef559c5df72c9262790.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_e4d38674b70d9ef559c5df72c9262790.png
deleted file mode 100644
index 414acff3b..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_e4d38674b70d9ef559c5df72c9262790.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_eff9583d9b94ac79c60cb099846ce8f3.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_eff9583d9b94ac79c60cb099846ce8f3.png
deleted file mode 100644
index 69aa35885..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_eff9583d9b94ac79c60cb099846ce8f3.png and /dev/null differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_f17bba59421e7a500387232295512fc0.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_f17bba59421e7a500387232295512fc0.png
deleted file mode 100644
index 64b0b3f7a..000000000
Binary files a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_f17bba59421e7a500387232295512fc0.png and /dev/null differ
diff --git a/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_200x0_resize_bge3e615_box_3.png b/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_200x0_resize_bge3e615_box_3.png
deleted file mode 100644
index 50c55c9eb..000000000
Binary files a/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_200x0_resize_bge3e615_box_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_200x0_resize_q75_bge3e615_box_3.jpg b/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_200x0_resize_q75_bge3e615_box_3.jpg
deleted file mode 100644
index 17fca6e6a..000000000
Binary files a/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_200x0_resize_q75_bge3e615_box_3.jpg and /dev/null differ
diff --git a/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_30x0_resize_box_3.png b/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_30x0_resize_box_3.png
deleted file mode 100644
index eb9f1170c..000000000
Binary files a/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_30x0_resize_box_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_filter_10590793696706257122.png b/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_filter_10590793696706257122.png
deleted file mode 100644
index 935c2391b..000000000
Binary files a/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_filter_10590793696706257122.png and /dev/null differ
diff --git a/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_200x0_resize_bge3e615_box_3.png b/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_200x0_resize_bge3e615_box_3.png
deleted file mode 100644
index b01efee50..000000000
Binary files a/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_200x0_resize_bge3e615_box_3.png and /dev/null differ
diff --git a/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_200x0_resize_q75_bge3e615_box_3.jpg b/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_200x0_resize_q75_bge3e615_box_3.jpg
deleted file mode 100644
index 56642d7e1..000000000
Binary files a/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_200x0_resize_q75_bge3e615_box_3.jpg and /dev/null differ
diff --git a/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_filter_10590793696706257122.png b/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_filter_10590793696706257122.png
deleted file mode 100644
index fe39286bd..000000000
Binary files a/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_filter_10590793696706257122.png and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_0d1b300da7a815ed567b6dadb6f2ce5e.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_0d1b300da7a815ed567b6dadb6f2ce5e.jpg
deleted file mode 100644
index 1e2cb535b..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_0d1b300da7a815ed567b6dadb6f2ce5e.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_100x100_fill_q75_box_center.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_100x100_fill_q75_box_center.jpg
deleted file mode 100644
index 8e6164e32..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_100x100_fill_q75_box_center.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_17fd3c558d78ce249b5f0bcbe1ddbffb.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_17fd3c558d78ce249b5f0bcbe1ddbffb.jpg
deleted file mode 100644
index 2aa3dad2b..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_17fd3c558d78ce249b5f0bcbe1ddbffb.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x0_resize_q50_r90_box.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x0_resize_q50_r90_box.jpg
deleted file mode 100644
index 05d98c67a..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x0_resize_q50_r90_box.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x100_resize_q75_box.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x100_resize_q75_box.jpg
deleted file mode 100644
index f12dd18fc..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x100_resize_q75_box.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x100_fill_q75_nearestneighbor_topleft.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x100_fill_q75_nearestneighbor_topleft.jpg
deleted file mode 100644
index 8ac3b2524..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x100_fill_q75_nearestneighbor_topleft.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x200_fill_q75_gaussian_smart1.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x200_fill_q75_gaussian_smart1.jpg
deleted file mode 100644
index 03de912fb..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x200_fill_q75_gaussian_smart1.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x200_fit_q75_linear.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x200_fit_q75_linear.jpg
deleted file mode 100644
index 3801c17d9..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x200_fit_q75_linear.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_30fc2aab35ca0861bf396d09aebc85a4.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_30fc2aab35ca0861bf396d09aebc85a4.jpg
deleted file mode 100644
index 60207a829..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_30fc2aab35ca0861bf396d09aebc85a4.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_352eb0101b7c88107520ba719432bbb2.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_352eb0101b7c88107520ba719432bbb2.jpg
deleted file mode 100644
index f7e84e33d..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_352eb0101b7c88107520ba719432bbb2.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_3efc2d0f29a8e12c5a690fc6c9288854.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_3efc2d0f29a8e12c5a690fc6c9288854.jpg
deleted file mode 100644
index 17a5927e2..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_3efc2d0f29a8e12c5a690fc6c9288854.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_3f1b1455c4a7d13c5aeb7510f9a6a581.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_3f1b1455c4a7d13c5aeb7510f9a6a581.jpg
deleted file mode 100644
index 93b914161..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_3f1b1455c4a7d13c5aeb7510f9a6a581.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_400x200_fill_q75_box_bottomleft.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_400x200_fill_q75_box_bottomleft.jpg
deleted file mode 100644
index 9a6255687..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_400x200_fill_q75_box_bottomleft.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_400x200_fill_q75_box_center.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_400x200_fill_q75_box_center.jpg
deleted file mode 100644
index b2db97485..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_400x200_fill_q75_box_center.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_510813cc53c37e2d489d2f9fdb13f749.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_510813cc53c37e2d489d2f9fdb13f749.jpg
deleted file mode 100644
index 6c3da1385..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_510813cc53c37e2d489d2f9fdb13f749.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_600x0_resize_q75_box.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_600x0_resize_q75_box.jpg
deleted file mode 100644
index a5ad199d8..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_600x0_resize_q75_box.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_6673ece428cb7d523234ca0d7c299542.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_6673ece428cb7d523234ca0d7c299542.jpg
deleted file mode 100644
index 7e2bdeef0..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_6673ece428cb7d523234ca0d7c299542.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_6c5c12ac79d3455ccb1993d51eec3cdf.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_6c5c12ac79d3455ccb1993d51eec3cdf.jpg
deleted file mode 100644
index e77e78d7b..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_6c5c12ac79d3455ccb1993d51eec3cdf.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_7d9bc4700565266807dc476421066137.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_7d9bc4700565266807dc476421066137.jpg
deleted file mode 100644
index ee246814d..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_7d9bc4700565266807dc476421066137.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_9f00027c376fe8556cc9996c47f23f78.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_9f00027c376fe8556cc9996c47f23f78.jpg
deleted file mode 100644
index e7db706c2..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_9f00027c376fe8556cc9996c47f23f78.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_abf356affd7d70d6bec3b3498b572191.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_abf356affd7d70d6bec3b3498b572191.jpg
deleted file mode 100644
index 9688c99c3..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_abf356affd7d70d6bec3b3498b572191.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c2d24766b49f3147f5a4137a8db592ac.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c2d24766b49f3147f5a4137a8db592ac.jpg
deleted file mode 100644
index b425b0d92..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c2d24766b49f3147f5a4137a8db592ac.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c36da6818db1ab630c3f87f65170003b.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c36da6818db1ab630c3f87f65170003b.jpg
deleted file mode 100644
index 41b42a883..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c36da6818db1ab630c3f87f65170003b.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c50a17db1e6d1bd0fe31a9a3444f1587.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c50a17db1e6d1bd0fe31a9a3444f1587.jpg
deleted file mode 100644
index 1857f8758..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c50a17db1e6d1bd0fe31a9a3444f1587.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_cb45fcba865177290c89dc9f41d6ff7a.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_cb45fcba865177290c89dc9f41d6ff7a.jpg
deleted file mode 100644
index f09ff9e33..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_cb45fcba865177290c89dc9f41d6ff7a.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_d30c10468b33df9010d185a8fe8f0491.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_d30c10468b33df9010d185a8fe8f0491.jpg
deleted file mode 100644
index 0b7d4e5d0..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_d30c10468b33df9010d185a8fe8f0491.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_de1fe6c0f40e7165355507d0f1748083.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_de1fe6c0f40e7165355507d0f1748083.jpg
deleted file mode 100644
index 7e35750db..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_de1fe6c0f40e7165355507d0f1748083.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_f6d8fe32ce3e83abf130e91e33456914.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_f6d8fe32ce3e83abf130e91e33456914.jpg
deleted file mode 100644
index b67650061..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_f6d8fe32ce3e83abf130e91e33456914.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_filter_16531506165985954191.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_filter_16531506165985954191.jpg
deleted file mode 100644
index bc43f4ef9..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_filter_16531506165985954191.jpg and /dev/null differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_filter_18171945436439920693.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_filter_18171945436439920693.jpg
deleted file mode 100644
index 80f06bf66..000000000
Binary files a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_filter_18171945436439920693.jpg and /dev/null differ
diff --git a/resources/testdata/golden_webp/fuzzy-cirlcle_hu525d1a6cf670e85f5e8f19890241399b_26792_200x0_resize_q75_h2_box_3.webp b/resources/testdata/golden_webp/fuzzy-cirlcle_hu525d1a6cf670e85f5e8f19890241399b_26792_200x0_resize_q75_h2_box_3.webp
deleted file mode 100644
index c9d229782..000000000
Binary files a/resources/testdata/golden_webp/fuzzy-cirlcle_hu525d1a6cf670e85f5e8f19890241399b_26792_200x0_resize_q75_h2_box_3.webp and /dev/null differ
diff --git a/resources/testdata/mask.png b/resources/testdata/mask.png
new file mode 100644
index 000000000..26ac85791
Binary files /dev/null and b/resources/testdata/mask.png differ
diff --git a/resources/testdata/mask2.png b/resources/testdata/mask2.png
new file mode 100644
index 000000000..b58a5e4b0
Binary files /dev/null and b/resources/testdata/mask2.png differ
diff --git a/resources/testdata/sunrise.webp b/resources/testdata/sunrise.webp
new file mode 100644
index 000000000..25ea7b046
Binary files /dev/null and b/resources/testdata/sunrise.webp differ
diff --git a/resources/transform.go b/resources/transform.go
index 9adec38cc..572143d49 100644
--- a/resources/transform.go
+++ b/resources/transform.go
@@ -24,6 +24,7 @@ import (
"sync"
"github.com/gohugoio/hugo/common/constants"
+ "github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/identity"
@@ -36,7 +37,6 @@ import (
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/common/maps"
- "github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/resources/internal"
"github.com/gohugoio/hugo/resources/resource"
@@ -52,11 +52,16 @@ var (
_ identity.IdentityGroupProvider = (*resourceAdapterInner)(nil)
_ resource.Source = (*resourceAdapter)(nil)
_ resource.Identifier = (*resourceAdapter)(nil)
+ _ resource.TransientIdentifier = (*resourceAdapter)(nil)
+ _ targetPathProvider = (*resourceAdapter)(nil)
+ _ sourcePathProvider = (*resourceAdapter)(nil)
+ _ resource.Identifier = (*resourceAdapter)(nil)
_ resource.ResourceNameTitleProvider = (*resourceAdapter)(nil)
_ resource.WithResourceMetaProvider = (*resourceAdapter)(nil)
_ identity.DependencyManagerProvider = (*resourceAdapter)(nil)
_ identity.IdentityGroupProvider = (*resourceAdapter)(nil)
_ resource.NameNormalizedProvider = (*resourceAdapter)(nil)
+ _ isPublishedProvider = (*resourceAdapter)(nil)
)
// These are transformations that need special support in Hugo that may not
@@ -188,10 +193,6 @@ func (r *resourceAdapter) Content(ctx context.Context) (any, error) {
return r.target.Content(ctx)
}
-func (r *resourceAdapter) Err() resource.ResourceError {
- return nil
-}
-
func (r *resourceAdapter) GetIdentity() identity.Identity {
return identity.FirstIdentity(r.target)
}
@@ -256,6 +257,10 @@ func (r *resourceAdapter) Filter(filters ...any) (images.ImageResource, error) {
return r.getImageOps().Filter(filters...)
}
+func (r *resourceAdapter) Resize(spec string) (images.ImageResource, error) {
+ return r.getImageOps().Resize(spec)
+}
+
func (r *resourceAdapter) Height() int {
return r.getImageOps().Height()
}
@@ -273,6 +278,23 @@ func (r *resourceAdapter) Key() string {
return r.target.(resource.Identifier).Key()
}
+func (r *resourceAdapter) TransientKey() string {
+ return r.Key()
+}
+
+func (r *resourceAdapter) targetPath() string {
+ r.init(false, false)
+ return r.target.(targetPathProvider).targetPath()
+}
+
+func (r *resourceAdapter) sourcePath() string {
+ r.init(false, false)
+ if sp, ok := r.target.(sourcePathProvider); ok {
+ return sp.sourcePath()
+ }
+ return ""
+}
+
func (r *resourceAdapter) MediaType() media.Type {
r.init(false, false)
return r.target.MediaType()
@@ -304,6 +326,11 @@ func (r *resourceAdapter) Publish() error {
return r.target.Publish()
}
+func (r *resourceAdapter) isPublished() bool {
+ r.init(false, false)
+ return r.target.isPublished()
+}
+
func (r *resourceAdapter) ReadSeekCloser() (hugio.ReadSeekCloser, error) {
r.init(false, false)
return r.target.ReadSeekCloser()
@@ -314,10 +341,6 @@ func (r *resourceAdapter) RelPermalink() string {
return r.target.RelPermalink()
}
-func (r *resourceAdapter) Resize(spec string) (images.ImageResource, error) {
- return r.getImageOps().Resize(spec)
-}
-
func (r *resourceAdapter) ResourceType() string {
r.init(false, false)
return r.target.ResourceType()
@@ -371,7 +394,6 @@ func (r *resourceAdapter) getImageOps() images.ImageResourceOps {
if r.MediaType().SubType == "svg" {
panic("this method is only available for raster images. To determine if an image is SVG, you can do {{ if eq .MediaType.SubType \"svg\" }}{{ end }}")
}
- fmt.Println(r.MediaType().SubType)
panic("this method is only available for image resources")
}
r.init(false, false)
@@ -397,7 +419,7 @@ func (r *resourceAdapter) TransformationKey() string {
for _, tr := range r.transformations {
key = key + "_" + tr.Key().Value()
}
- return r.spec.ResourceCache.cleanKey(r.target.Key()) + "_" + helpers.MD5String(key)
+ return r.spec.ResourceCache.cleanKey(r.target.Key()) + "_" + hashing.MD5FromStringHexEncoded(key)
}
func (r *resourceAdapter) getOrTransform(publish, setContent bool) error {
@@ -486,16 +508,20 @@ func (r *resourceAdapter) transform(key string, publish, setContent bool) (*reso
if herrors.IsFeatureNotAvailableError(err) {
var errMsg string
- if tr.Key().Name == "postcss" {
+ switch strings.ToLower(tr.Key().Name) {
+ case "postcss":
// This transformation is not available in this
// Most likely because PostCSS is not installed.
- errMsg = ". Check your PostCSS installation; install with \"npm install postcss-cli\". See https://gohugo.io/hugo-pipes/postcss/"
- } else if tr.Key().Name == "tocss" {
+ errMsg = ". You need to install PostCSS. See https://gohugo.io/functions/css/postcss/"
+ case "tailwindcss":
+ errMsg = ". You need to install TailwindCSS CLI. See https://gohugo.io/functions/css/tailwindcss/"
+ case "tocss":
errMsg = ". Check your Hugo installation; you need the extended version to build SCSS/SASS with transpiler set to 'libsass'."
- } else if tr.Key().Name == "tocss-dart" {
- errMsg = ". You need dart-sass-embedded in your system $PATH."
- } else if tr.Key().Name == "babel" {
- errMsg = ". You need to install Babel, see https://gohugo.io/hugo-pipes/babel/"
+ case "tocss-dart":
+ errMsg = ". You need to install Dart Sass, see https://gohugo.io//functions/css/sass/#dart-sass"
+ case "babel":
+ errMsg = ". You need to install Babel, see https://gohugo.io/functions/js/babel/"
+
}
return fmt.Errorf(msg+errMsg+": %w", err)
diff --git a/resources/transform_test.go b/resources/transform_test.go
index 7f91360f1..eac85ada9 100644
--- a/resources/transform_test.go
+++ b/resources/transform_test.go
@@ -207,7 +207,7 @@ func TestTransform(t *testing.T) {
fs := afero.NewMemMapFs()
- for i := 0; i < 2; i++ {
+ for i := range 2 {
spec := newTestResourceSpec(specDescriptor{c: c, fs: fs})
r := createTransformer(c, spec, "f1.txt", "color is blue")
@@ -337,12 +337,12 @@ func TestTransform(t *testing.T) {
const count = 26 // A-Z
transformations := make([]resources.ResourceTransformation, count)
- for i := 0; i < count; i++ {
+ for i := range count {
transformations[i] = createContentReplacer(fmt.Sprintf("t%d", i), fmt.Sprint(i), string(rune(i+65)))
}
var countstr strings.Builder
- for i := 0; i < count; i++ {
+ for i := range count {
countstr.WriteString(fmt.Sprint(i))
}
@@ -386,22 +386,15 @@ func TestTransform(t *testing.T) {
resizedPublished1, err := img.Resize("40x40")
c.Assert(err, qt.IsNil)
c.Assert(resizedPublished1.Height(), qt.Equals, 40)
- c.Assert(resizedPublished1.RelPermalink(), qt.Equals, "/gopher.changed_hu2e827f5a78333ebc04166dd643235dea_1462_40x40_resize_linear_3.png")
- assertShouldExist(c, spec, "public/gopher.changed_hu2e827f5a78333ebc04166dd643235dea_1462_40x40_resize_linear_3.png", true)
+ c.Assert(resizedPublished1.RelPermalink(), qt.Equals, "/gopher.changed_hu_85920388a7ff96fa.png")
+ assertShouldExist(c, spec, "public/gopher.changed_hu_85920388a7ff96fa.png", true)
// Permalink called.
resizedPublished2, err := img.Resize("30x30")
c.Assert(err, qt.IsNil)
c.Assert(resizedPublished2.Height(), qt.Equals, 30)
- c.Assert(resizedPublished2.Permalink(), qt.Equals, "https://example.com/gopher.changed_hu2e827f5a78333ebc04166dd643235dea_1462_30x30_resize_linear_3.png")
- assertShouldExist(c, spec, "public/gopher.changed_hu2e827f5a78333ebc04166dd643235dea_1462_30x30_resize_linear_3.png", true)
-
- // Not published because none of RelPermalink or Permalink was called.
- resizedNotPublished, err := img.Resize("50x50")
- c.Assert(err, qt.IsNil)
- c.Assert(resizedNotPublished.Height(), qt.Equals, 50)
- // c.Assert(resized.RelPermalink(), qt.Equals, "/gopher.changed_hu2e827f5a78333ebc04166dd643235dea_1462_50x50_resize_linear_2.png")
- assertShouldExist(c, spec, "public/gopher.changed_hu2e827f5a78333ebc04166dd643235dea_1462_50x50_resize_linear_3.png", false)
+ c.Assert(resizedPublished2.Permalink(), qt.Equals, "https://example.com/gopher.changed_hu_c8d8163c08643a7f.png")
+ assertShouldExist(c, spec, "public/gopher.changed_hu_c8d8163c08643a7f.png", true)
assertNoDuplicateWrites(c, spec)
})
@@ -412,18 +405,18 @@ func TestTransform(t *testing.T) {
transformers := make([]resources.Transformer, 10)
transformations := make([]resources.ResourceTransformation, 10)
- for i := 0; i < 10; i++ {
+ for i := range 10 {
transformers[i] = createTransformer(c, spec, fmt.Sprintf("f%d.txt", i), fmt.Sprintf("color is %d", i))
transformations[i] = createContentReplacer("test", strconv.Itoa(i), "blue")
}
var wg sync.WaitGroup
- for i := 0; i < 13; i++ {
+ for i := range 13 {
wg.Add(1)
go func(i int) {
defer wg.Done()
- for j := 0; j < 23; j++ {
+ for j := range 23 {
id := (i + j) % 10
tr, err := transformers[id].Transform(transformations[id])
c.Assert(err, qt.IsNil)
diff --git a/scripts/docker/entrypoint.sh b/scripts/docker/entrypoint.sh
new file mode 100755
index 000000000..20ffbe5f7
--- /dev/null
+++ b/scripts/docker/entrypoint.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+# Check if a custom hugo-docker-entrypoint.sh file exists.
+if [ -f hugo-docker-entrypoint.sh ]; then
+ # Execute the custom entrypoint file.
+ sh hugo-docker-entrypoint.sh "$@"
+ exit $?
+fi
+
+# Check if a package.json file exists.
+if [ -f package.json ]; then
+ # Check if node_modules exists.
+ if [ ! -d node_modules ]; then
+ # Install npm packages.
+ # Note that we deliberately do not use `npm ci` here, as it would fail if the package-lock.json file is not up-to-date,
+ # which would be the case if you run the container with a different OS or architecture than the one used to create the package-lock.json file.
+ npm i
+ fi
+fi
+
+exec "hugo" "$@"
\ No newline at end of file
diff --git a/scripts/fork_go_templates/main.go b/scripts/fork_go_templates/main.go
index 38e81ac9d..e4895c87a 100644
--- a/scripts/fork_go_templates/main.go
+++ b/scripts/fork_go_templates/main.go
@@ -4,19 +4,18 @@ import (
"fmt"
"log"
"os"
+ "os/exec"
"path/filepath"
"regexp"
"strings"
- "github.com/gohugoio/hugo/common/hexec"
-
"github.com/gohugoio/hugo/common/hugio"
"github.com/spf13/afero"
)
func main() {
- // The current is built with db6097f8cb [release-branch.go1.22] go1.22.1
+ // The current is built with 3901409b5d [release-branch.go1.24] go1.24.0
// TODO(bep) preserve the staticcheck.conf file.
fmt.Println("Forking ...")
defer fmt.Println("Done ...")
@@ -208,7 +207,7 @@ func removeAll(expression, content string) string {
}
func rewrite(filename, rule string) {
- cmf, _ := hexec.SafeCommand("gofmt", "-w", "-r", rule, filename)
+ cmf := exec.Command("gofmt", "-w", "-r", rule, filename)
out, err := cmf.CombinedOutput()
if err != nil {
log.Fatal("gofmt failed:", string(out))
@@ -216,7 +215,8 @@ func rewrite(filename, rule string) {
}
func goimports(dir string) {
- cmf, _ := hexec.SafeCommand("goimports", "-w", dir)
+ // Needs go install golang.org/x/tools/cmd/goimports@latest
+ cmf := exec.Command("goimports", "-w", dir)
out, err := cmf.CombinedOutput()
if err != nil {
log.Fatal("goimports failed:", string(out))
@@ -224,7 +224,7 @@ func goimports(dir string) {
}
func gofmt(dir string) {
- cmf, _ := hexec.SafeCommand("gofmt", "-w", dir)
+ cmf := exec.Command("gofmt", "-w", dir)
out, err := cmf.CombinedOutput()
if err != nil {
log.Fatal("gofmt failed:", string(out))
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index e35e849cc..6a64bd2d4 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -29,13 +29,6 @@ license: "Apache-2.0"
source-code: https://github.com/gohugoio/hugo.git
website: https://gohugo.io/
-package-repositories:
- - type: apt
- components: [main]
- suites: [focal]
- key-id: 9FD3B784BC1C6FC31A8A0A1C1655A0AB68576280
- url: https://deb.nodesource.com/node_16.x
-
plugs:
etc-gitconfig:
interface: system-files
@@ -88,6 +81,7 @@ environment:
# rst2html: PYTHONHOME and SNAP
# asciidoctor: RUBYLIB
HUGO_SECURITY_EXEC_OSENV: (?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG|SYSTEMDRIVE|GIT_EXEC_PATH|LD_LIBRARY_PATH|npm_config_(cache|init_module|userconfig)|pandoc_datadir|PYTHONHOME|SNAP|RUBYLIB)$
+
apps:
hugo:
command: bin/hugo
@@ -116,7 +110,7 @@ parts:
go:
plugin: nil
stage-snaps:
- - go/1.22/stable
+ - go/latest/stable
prime:
- bin/go
- pkg/tool
@@ -145,7 +139,7 @@ parts:
HUGO_BUILD_TAGS="extended"
echo " * Building hugo (HUGO_BUILD_TAGS=\"$HUGO_BUILD_TAGS\")..."
- go build -v -ldflags "-X github.com/gohugoio/hugo/common/hugo.vendorInfo=snap:$(git describe --tags --always --match 'v[0-9]*' | sed 's/^v//; s/-/+git/; s/-g/./')" -tags "$HUGO_BUILD_TAGS"
+ go build -v -ldflags "-s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=snap:$(git describe --tags --always --match 'v[0-9]*' | sed 's/^v//; s/-/+git/; s/-g/./')" -tags "$HUGO_BUILD_TAGS"
./hugo version
ldd hugo || :
@@ -199,8 +193,11 @@ parts:
node:
plugin: nil
- stage-packages:
- - nodejs
+ stage-snaps:
+ - node/22/stable
+ organize:
+ "LICENSE": "LICENSE_NODE" # rename to prevent conflict with Go snap
+ "README.md": "README_NODE.md" # rename to prevent conflict with Go snap
pandoc:
plugin: nil
diff --git a/source/fileInfo.go b/source/fileInfo.go
index 44d08e620..dfa5cda26 100644
--- a/source/fileInfo.go
+++ b/source/fileInfo.go
@@ -19,14 +19,14 @@ import (
"time"
"github.com/bep/gitmap"
+ "github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/paths"
+ "github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/hugofs"
-
- "github.com/gohugoio/hugo/helpers"
)
// File describes a source file.
@@ -37,6 +37,12 @@ type File struct {
lazyInit sync.Once
}
+// IsContentAdapter returns whether the file represents a content adapter.
+// This means that there may be more than one Page associated with this file.
+func (fi *File) IsContentAdapter() bool {
+ return fi.fim.Meta().PathInfo.IsContentData()
+}
+
// Filename returns a file's absolute path and filename on disk.
func (fi *File) Filename() string { return fi.fim.Meta().Filename }
@@ -50,13 +56,6 @@ func (fi *File) Dir() string {
return fi.pathToDir(fi.p().Dir())
}
-// Extension is an alias to Ext().
-// Deprecated: Use Ext() instead.
-func (fi *File) Extension() string {
- hugo.Deprecate(".File.Extension", "Use .File.Ext instead.", "v0.96.0")
- return fi.Ext()
-}
-
// Ext returns a file's extension without the leading period (e.g. "md").
func (fi *File) Ext() string { return fi.p().Ext() }
@@ -118,7 +117,7 @@ func (fi *File) IsZero() bool {
// in some cases that is slightly expensive to construct.
func (fi *File) init() {
fi.lazyInit.Do(func() {
- fi.uniqueID = helpers.MD5String(filepath.ToSlash(fi.Path()))
+ fi.uniqueID = hashing.MD5FromStringHexEncoded(filepath.ToSlash(fi.Path()))
})
}
@@ -133,10 +132,17 @@ func (fi *File) p() *paths.Path {
return fi.fim.Meta().PathInfo.Unnormalized()
}
-func NewFileInfoFrom(path, filename string) *File {
+var contentPathParser = &paths.PathParser{
+ IsContentExt: func(ext string) bool {
+ return true
+ },
+}
+
+// Used in tests.
+func NewContentFileInfoFrom(path, filename string) *File {
meta := &hugofs.FileMeta{
Filename: filename,
- PathInfo: paths.Parse("", filepath.ToSlash(path)),
+ PathInfo: contentPathParser.Parse(files.ComponentFolderContent, filepath.ToSlash(path)),
}
return NewFileInfo(hugofs.NewFileMetaInfo(nil, meta))
@@ -168,6 +174,8 @@ type GitInfo struct {
AuthorDate time.Time `json:"authorDate"`
// The commit date.
CommitDate time.Time `json:"commitDate"`
+ // The commit message's body.
+ Body string `json:"body"`
}
// IsZero returns true if the GitInfo is empty,
diff --git a/testscripts/commands/config.txt b/testscripts/commands/config.txt
index b1dba8d11..46386eb92 100644
--- a/testscripts/commands/config.txt
+++ b/testscripts/commands/config.txt
@@ -1,7 +1,7 @@
# Test the config command.
hugo config -h
-stdout 'Print the site configuration'
+stdout 'Display site configuration'
hugo config
diff --git a/testscripts/commands/convert.txt b/testscripts/commands/convert.txt
index 1cf756215..811aeecc8 100644
--- a/testscripts/commands/convert.txt
+++ b/testscripts/commands/convert.txt
@@ -1,7 +1,7 @@
# Test the convert commands.
hugo convert -h
-stdout 'Convert your content'
+stdout 'Convert front matter to another format'
hugo convert toJSON -h
stdout 'to use JSON for the front matter'
hugo convert toTOML -h
diff --git a/testscripts/commands/deprecate.txt b/testscripts/commands/deprecate.txt
index 3be4976d5..8791c3a78 100644
--- a/testscripts/commands/deprecate.txt
+++ b/testscripts/commands/deprecate.txt
@@ -1,13 +1,13 @@
# Test deprecation logging.
hugo -e info --logLevel info
-stdout 'INFO deprecated: item was deprecated in Hugo'
+stderr 'INFO deprecated: item was deprecated in Hugo'
hugo -e warn --logLevel warn
-stdout 'WARN deprecated: item was deprecated in Hugo'
+stderr 'WARN deprecated: item was deprecated in Hugo'
! hugo -e error --logLevel warn
-stdout 'ERROR deprecated: item was deprecated in Hugo'
+stderr 'ERROR deprecated: item was deprecated in Hugo'
-- hugo.toml --
baseURL = "https://example.com/"
diff --git a/testscripts/commands/gen.txt b/testscripts/commands/gen.txt
index 2ab90e5be..e83e9982f 100644
--- a/testscripts/commands/gen.txt
+++ b/testscripts/commands/gen.txt
@@ -1,19 +1,22 @@
# Test the gen commands.
-# Note that adding new commands will require updating the NUM_COMMANDS value.
-env NUM_COMMANDS=42
hugo gen -h
-stdout 'A collection of several useful generators\.'
-
+stdout 'Generate documentation for your project using Hugo''s documentation engine, including syntax highlighting for various programming languages\.'
hugo gen doc --dir clidocs
-checkfilecount $NUM_COMMANDS clidocs
hugo gen man -h
stdout 'up-to-date man pages'
hugo gen man --dir manpages
-checkfilecount $NUM_COMMANDS manpages
hugo gen chromastyles -h
stdout 'Generate CSS stylesheet for the Chroma code highlighter'
hugo gen chromastyles --style monokai
-stdout '/\* LineHighlight \*/ \.chroma \.hl \{ background-color:#3c3d38 \}'
+stdout 'Generated using: hugo gen chromastyles --style monokai'
+! hugo gen chromastyles --style __invalid_style__
+stderr 'invalid style: __invalid_style__'
+
+# Issue 13475
+hugo gen chromastyles --style monokai
+stdout '{ }'
+hugo gen chromastyles --omitEmpty --style monokai
+! stdout '\{ \}'
diff --git a/testscripts/commands/hugo.txt b/testscripts/commands/hugo.txt
index 60f7ffe71..bf0f5cf0d 100644
--- a/testscripts/commands/hugo.txt
+++ b/testscripts/commands/hugo.txt
@@ -11,7 +11,7 @@ grep 'IsServer: false;IsProduction: true' public/index.html
baseURL = "http://example.org/"
disableKinds = ["RSS", "sitemap", "robotsTXT", "404", "taxonomy", "term"]
-- layouts/index.html --
-Home|IsServer: {{ .Site.IsServer }};IsProduction: {{ hugo.IsProduction }}|
+Home|IsServer: {{ hugo.IsServer }};IsProduction: {{ hugo.IsProduction }}|
-- layouts/_default/single.html --
Title: {{ .Title }}
-- content/p1.md --
diff --git a/testscripts/commands/hugo__configdir.txt b/testscripts/commands/hugo__configdir.txt
index 4da62ade5..148523e9f 100644
--- a/testscripts/commands/hugo__configdir.txt
+++ b/testscripts/commands/hugo__configdir.txt
@@ -3,4 +3,5 @@ hugo
! stderr .
-- config/_default/hugo.toml --
-baseURL = "https://example.com/"
\ No newline at end of file
+baseURL = "https://example.com/"
+disableKinds = ["RSS", "page", "sitemap", "robotsTXT", "404", "taxonomy", "term", "home"]
\ No newline at end of file
diff --git a/testscripts/commands/hugo__errors.txt b/testscripts/commands/hugo__errors.txt
index 2400ce69b..975d11616 100644
--- a/testscripts/commands/hugo__errors.txt
+++ b/testscripts/commands/hugo__errors.txt
@@ -2,7 +2,7 @@
# The hugo mod get command handles flags a little special, but the -h flag should print the help.
hugo mod get -h
-stdout 'Resolves dependencies in your current Hugo Project'
+stdout 'Resolves dependencies in your current Hugo project'
# Invalid flag. Should print an error message to stderr and the help to stdout.
! hugo --asdf
@@ -15,4 +15,4 @@ stdout 'hugo is the main command'
stderr 'failed to load config'
-- hugo.toml --
-invalid: toml
\ No newline at end of file
+invalid: toml
diff --git a/testscripts/commands/hugo__path-warnings.txt b/testscripts/commands/hugo__path-warnings.txt
index f7e3acd95..8eccb6567 100644
--- a/testscripts/commands/hugo__path-warnings.txt
+++ b/testscripts/commands/hugo__path-warnings.txt
@@ -1,6 +1,6 @@
hugo --printPathWarnings
-stdout 'Duplicate'
+stderr 'Duplicate'
-- hugo.toml --
-- assets/css/styles.css --
diff --git a/testscripts/commands/hugo__path-warnings_issue13164.txt b/testscripts/commands/hugo__path-warnings_issue13164.txt
new file mode 100644
index 000000000..1342c287a
--- /dev/null
+++ b/testscripts/commands/hugo__path-warnings_issue13164.txt
@@ -0,0 +1,15 @@
+hugo --printPathWarnings
+
+! stderr 'Duplicate target paths'
+
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- assets/foo.txt --
+foo
+-- layouts/index.html --
+A: {{ (resources.Get "foo.txt").RelPermalink }}
+B: {{ (resources.GetMatch "foo.txt").RelPermalink }}
+C: {{ (index (resources.Match "foo.txt") 0).RelPermalink }}
+D: {{ (index (resources.ByType "text") 0).RelPermalink }}
+-- layouts/unused/single.html --
+{{ .Title }}
diff --git a/testscripts/commands/hugo_build.txt b/testscripts/commands/hugo_build.txt
new file mode 100644
index 000000000..0bcbcba7a
--- /dev/null
+++ b/testscripts/commands/hugo_build.txt
@@ -0,0 +1,20 @@
+# Test the hugo build command (alias for hugo)
+
+hugo build
+stdout 'Pages.*|1'
+stdout 'Total in'
+checkfile public/index.html
+checkfile public/p1/index.html
+grep 'IsServer: false;IsProduction: true' public/index.html
+
+-- hugo.toml --
+baseURL = "http://example.org/"
+disableKinds = ["RSS", "sitemap", "robotsTXT", "404", "taxonomy", "term"]
+-- layouts/index.html --
+Home|IsServer: {{ hugo.IsServer }};IsProduction: {{ hugo.IsProduction }}|
+-- layouts/_default/single.html --
+Title: {{ .Title }}
+-- content/p1.md --
+---
+title: "P1"
+---
diff --git a/testscripts/commands/hugo_printpathwarnings.txt b/testscripts/commands/hugo_printpathwarnings.txt
index f4c76ebab..51eb46d91 100644
--- a/testscripts/commands/hugo_printpathwarnings.txt
+++ b/testscripts/commands/hugo_printpathwarnings.txt
@@ -1,6 +1,6 @@
hugo --printPathWarnings
-stdout 'Duplicate target paths: .index.html \(2\)'
+stderr 'Duplicate target paths: .index.html \(2\)'
-- hugo.toml --
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "404", "section"]
diff --git a/testscripts/commands/hugo_printunusedtemplates.txt b/testscripts/commands/hugo_printunusedtemplates.txt
index 312e4920d..a7a5d87c3 100644
--- a/testscripts/commands/hugo_printunusedtemplates.txt
+++ b/testscripts/commands/hugo_printunusedtemplates.txt
@@ -1,6 +1,6 @@
hugo --printUnusedTemplates
-stdout 'Template _default/list.html is unused'
+stderr 'Template /list.html is unused'
-- hugo.toml --
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "404", "section", "page"]
diff --git a/testscripts/commands/import_jekyll.txt b/testscripts/commands/import_jekyll.txt
index 8d229ba2e..953349acf 100644
--- a/testscripts/commands/import_jekyll.txt
+++ b/testscripts/commands/import_jekyll.txt
@@ -1,7 +1,7 @@
-# Test the import jekyll command.
+# Test the import + import jekyll command.
hugo import -h
-stdout 'Import your site from other web site generators like Jekyll\.'
+stdout 'Import a site from another system'
hugo import jekyll -h
stdout 'hugo import from Jekyll\.'
diff --git a/testscripts/commands/mod.txt b/testscripts/commands/mod.txt
index 56cea2c00..2fa17dbbe 100644
--- a/testscripts/commands/mod.txt
+++ b/testscripts/commands/mod.txt
@@ -18,7 +18,8 @@ hugo mod clean
! stderr .
stdout 'hugo: removed 1 dirs in module cache for \"github.com/bep/empty-hugo-module\"'
hugo mod clean --all
-stdout 'Deleted 2\d{2} files from module cache\.'
+# Currently this is 299 on MacOS and 301 on Linux.
+stdout 'Deleted (2|3)\d{2} files from module cache\.'
cd submod
hugo mod init testsubmod
cmpenv go.mod $WORK/golden/go.mod.testsubmod
diff --git a/testscripts/commands/mod_init.txt b/testscripts/commands/mod_init.txt
new file mode 100644
index 000000000..c09e71a23
--- /dev/null
+++ b/testscripts/commands/mod_init.txt
@@ -0,0 +1,15 @@
+# Test the hugo init command.
+dostounix golden/go.mod.testsubmod
+
+hugo mod init testsubmod
+cmpenv go.mod $WORK/golden/go.mod.testsubmod
+
+-- hugo.toml --
+title = "Hugo Modules Test"
+[module]
+[[module.imports]]
+path="github.com/bep/empty-hugo-module"
+-- golden/go.mod.testsubmod --
+module testsubmod
+
+go ${GOVERSION}
\ No newline at end of file
diff --git a/testscripts/commands/mod_npm.txt b/testscripts/commands/mod_npm.txt
index 32cc37f06..3d8903e6a 100644
--- a/testscripts/commands/mod_npm.txt
+++ b/testscripts/commands/mod_npm.txt
@@ -2,6 +2,7 @@
dostounix golden/package.json
+
hugo mod npm pack
cmp package.json golden/package.json
@@ -41,3 +42,4 @@ path="github.com/gohugoio/hugoTestModule2"
}
-- go.mod --
module github.com/gohugoio/hugoTestModule
+go 1.20
diff --git a/testscripts/commands/mod_npm_withexisting.txt b/testscripts/commands/mod_npm_withexisting.txt
index e92eba3fd..9f72eefdc 100644
--- a/testscripts/commands/mod_npm_withexisting.txt
+++ b/testscripts/commands/mod_npm_withexisting.txt
@@ -1,5 +1,8 @@
# Test mod npm.
+# See https://github.com/gohugoio/hugo/issues/13465
+[windows] skip
+
dostounix golden/package.json
hugo mod npm pack
@@ -55,3 +58,4 @@ path="github.com/gohugoio/hugoTestModule2"
}
-- go.mod --
module github.com/gohugoio/hugoTestModule
+go 1.20
diff --git a/testscripts/commands/new.txt b/testscripts/commands/new.txt
index 4ac264eb1..f8d7c1ec1 100644
--- a/testscripts/commands/new.txt
+++ b/testscripts/commands/new.txt
@@ -1,7 +1,7 @@
# Test the new command.
hugo new site -h
-stdout 'Create a new site in the provided directory'
+stdout 'Create a new site at the specified path.'
hugo new site my-yaml-site --format yml
checkfile my-yaml-site/hugo.yml
hugo new site mysite -f
@@ -19,8 +19,8 @@ exists themes
! exists resources
hugo new theme -h
-stdout 'Create a new theme \(skeleton\) called \[name\] in ./themes'
-hugo new theme mytheme
+stdout 'Create a new theme with the specified name in the ./themes directory.'
+hugo new theme mytheme --format yml
stdout 'Creating new theme'
! exists resources
cd themes
@@ -34,22 +34,21 @@ checkfile content/posts/post-1.md
checkfile content/posts/post-2.md
checkfile content/posts/post-3/bryce-canyon.jpg
checkfile content/posts/post-3/index.md
-checkfile layouts/_default/baseof.html
-checkfile layouts/_default/home.html
-checkfile layouts/_default/list.html
-checkfile layouts/_default/single.html
-checkfile layouts/partials/footer.html
-checkfile layouts/partials/head.html
-checkfile layouts/partials/head/css.html
-checkfile layouts/partials/head/js.html
-checkfile layouts/partials/header.html
-checkfile layouts/partials/menu.html
-checkfile layouts/partials/terms.html
+checkfile layouts/baseof.html
+checkfile layouts/home.html
+checkfile layouts/section.html
+checkfile layouts/page.html
+checkfile layouts/taxonomy.html
+checkfile layouts/term.html
+checkfile layouts/_partials/footer.html
+checkfile layouts/_partials/head.html
+checkfile layouts/_partials/head/css.html
+checkfile layouts/_partials/head/js.html
+checkfile layouts/_partials/header.html
+checkfile layouts/_partials/menu.html
+checkfile layouts/_partials/terms.html
checkfile static/favicon.ico
-checkfile LICENSE
-checkfile README.md
-checkfile hugo.toml
-checkfile theme.toml
+checkfile hugo.yml
exists data
exists i18n
@@ -65,6 +64,20 @@ cd myexistingsite
hugo new post/foo.md -t mytheme
grep 'Dummy content' content/post/foo.md
+cd $WORK
+
+# In the three archetype format tests below, skip Windows testing to avoid
+# newline differences when comparing to golden.
+
+hugo new site json-site --format json
+[!windows] cmp json-site/archetypes/default.md archetype-golden-json.md
+
+hugo new site toml-site --format toml
+[!windows] cmp toml-site/archetypes/default.md archetype-golden-toml.md
+
+hugo new site yaml-site --format yaml
+[!windows] cmp yaml-site/archetypes/default.md archetype-golden-yaml.md
+
-- myexistingsite/hugo.toml --
theme = "mytheme"
-- myexistingsite/content/p1.md --
@@ -80,3 +93,22 @@ draft: true
---
Dummy content.
+
+-- archetype-golden-json.md --
+{
+ "date": "{{ .Date }}",
+ "draft": true,
+ "title": "{{ replace .File.ContentBaseName \"-\" \" \" | title }}"
+}
+-- archetype-golden-toml.md --
++++
+date = '{{ .Date }}'
+draft = true
+title = '{{ replace .File.ContentBaseName "-" " " | title }}'
++++
+-- archetype-golden-yaml.md --
+---
+date: '{{ .Date }}'
+draft: true
+title: '{{ replace .File.ContentBaseName "-" " " | title }}'
+---
diff --git a/testscripts/commands/server.txt b/testscripts/commands/server.txt
index a28e4d698..7f6afd8fd 100644
--- a/testscripts/commands/server.txt
+++ b/testscripts/commands/server.txt
@@ -25,7 +25,7 @@ myenv = "theproduction"
myenv = "thedevelopment"
-- layouts/index.html --
-Title: {{ .Title }}|BaseURL: {{ site.BaseURL }}|ServerPort: {{ site.ServerPort }}|myenv: {{ .Site.Params.myenv }}|Env: {{ hugo.Environment }}|IsServer: {{ site.IsServer }}|
+Title: {{ .Title }}|BaseURL: {{ site.BaseURL }}|ServerPort: {{ site.ServerPort }}|myenv: {{ .Site.Params.myenv }}|Env: {{ hugo.Environment }}|IsServer: {{ hugo.IsServer }}|
-- layouts/404.html --
custom 404
diff --git a/testscripts/commands/server__error_recovery_edit_config.txt b/testscripts/commands/server__error_recovery_edit_config.txt
new file mode 100644
index 000000000..664d99272
--- /dev/null
+++ b/testscripts/commands/server__error_recovery_edit_config.txt
@@ -0,0 +1,42 @@
+# Test the hugo server command when adding an error to a config file
+# and then fixing it.
+
+hugo server &
+
+waitServer
+
+httpget ${HUGOTEST_BASEURL_0}p1/ 'Title: P1'
+
+replace $WORK/hugo.toml 'title =' 'titlefoo'
+httpget ${HUGOTEST_BASEURL_0}p1/ 'failed'
+
+replace $WORK/hugo.toml 'titlefoo' 'title ='
+httpget ${HUGOTEST_BASEURL_0}p1/ 'Title: P1'
+
+stopServer
+
+-- hugo.toml --
+title = "Hugo Server Test"
+baseURL = "https://example.org/"
+disableKinds = ["taxonomy", "term", "sitemap"]
+-- layouts/index.html --
+Title: {{ .Title }}|BaseURL: {{ site.BaseURL }}|
+-- layouts/_default/single.html --
+Title: {{ .Title }}|BaseURL: {{ site.BaseURL }}|
+-- content/_index.md --
+---
+title: Hugo Home
+---
+-- content/p1/index.md --
+---
+title: P1
+---
+-- content/p2/index.md --
+---
+title: P2
+---
+-- static/staticfiles/static.txt --
+static
+
+
+
diff --git a/testscripts/commands/server__error_recovery_edit_content.txt b/testscripts/commands/server__error_recovery_edit_content.txt
new file mode 100644
index 000000000..f5ea7e94b
--- /dev/null
+++ b/testscripts/commands/server__error_recovery_edit_content.txt
@@ -0,0 +1,42 @@
+# Test the hugo server command when adding a front matter error to a content file
+# and then fixing it.
+
+hugo server &
+
+waitServer
+
+httpget ${HUGOTEST_BASEURL_0}p1/ 'Title: P1'
+
+replace $WORK/content/p1/index.md 'title:' 'titlecolon'
+httpget ${HUGOTEST_BASEURL_0}p1/ 'failed'
+
+replace $WORK/content/p1/index.md 'titlecolon' 'title:'
+httpget ${HUGOTEST_BASEURL_0}p1/ 'Title: P1'
+
+stopServer
+
+-- hugo.toml --
+title = "Hugo Server Test"
+baseURL = "https://example.org/"
+disableKinds = ["taxonomy", "term", "sitemap"]
+-- layouts/index.html --
+Title: {{ .Title }}|BaseURL: {{ site.BaseURL }}|
+-- layouts/_default/single.html --
+Title: {{ .Title }}|BaseURL: {{ site.BaseURL }}|
+-- content/_index.md --
+---
+title: Hugo Home
+---
+-- content/p1/index.md --
+---
+title: P1
+---
+-- content/p2/index.md --
+---
+title: P2
+---
+-- static/staticfiles/static.txt --
+static
+
+
+
diff --git a/testscripts/commands/warnf_stderr.txt b/testscripts/commands/warnf_stderr.txt
new file mode 100644
index 000000000..f899253c5
--- /dev/null
+++ b/testscripts/commands/warnf_stderr.txt
@@ -0,0 +1,13 @@
+# Issue #13074
+
+hugo
+stderr 'warning'
+! stdout 'warning'
+
+-- hugo.toml --
+baseURL = "http://example.org/"
+disableKinds = ["RSS", "page", "sitemap", "robotsTXT", "404", "taxonomy", "term"]
+-- layouts/index.html --
+Home
+{{ warnf "This is a warning" }}
+
diff --git a/testscripts/withdeploy-off/deploy_off.txt b/testscripts/withdeploy-off/deploy_off.txt
new file mode 100644
index 000000000..5e6c65d27
--- /dev/null
+++ b/testscripts/withdeploy-off/deploy_off.txt
@@ -0,0 +1,3 @@
+! hugo deploy --force
+# Issue 13012
+stderr 'deploy not supported in this version of Hugo'
\ No newline at end of file
diff --git a/testscripts/commands/deploy.txt b/testscripts/withdeploy/deploy.txt
similarity index 93%
rename from testscripts/commands/deploy.txt
rename to testscripts/withdeploy/deploy.txt
index 0afe1fc44..2586f8b8f 100644
--- a/testscripts/commands/deploy.txt
+++ b/testscripts/withdeploy/deploy.txt
@@ -1,7 +1,7 @@
# Test the deploy command.
hugo deploy -h
-stdout 'Deploy your site to a Cloud provider\.'
+stdout 'Deploy your site to a cloud provider'
mkdir mybucket
hugo deploy --target mydeployment --invalidateCDN=false
grep 'hello' mybucket/index.html
diff --git a/tpl/cast/cast_test.go b/tpl/cast/cast_test.go
index 5b4a36c3a..a8fdc662b 100644
--- a/tpl/cast/cast_test.go
+++ b/tpl/cast/cast_test.go
@@ -17,7 +17,9 @@ import (
"html/template"
"testing"
+ "github.com/bep/imagemeta"
qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/htesting/hqt"
)
func TestToInt(t *testing.T) {
@@ -85,6 +87,7 @@ func TestToFloat(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New()
+ oneThird, _ := imagemeta.NewRat[uint32](1, 3)
for i, test := range []struct {
v any
@@ -101,6 +104,7 @@ func TestToFloat(t *testing.T) {
{"0", 0.0},
{float64(2.12), 2.12},
{int64(123), 123.0},
+ {oneThird, 0.3333333333333333},
{2, 2.0},
{t, false},
} {
@@ -114,6 +118,6 @@ func TestToFloat(t *testing.T) {
}
c.Assert(err, qt.IsNil, errMsg)
- c.Assert(result, qt.Equals, test.expect, errMsg)
+ c.Assert(result, hqt.IsSameFloat64, test.expect, errMsg)
}
}
diff --git a/tpl/collections/apply.go b/tpl/collections/apply.go
index 3d50395b9..caa51f149 100644
--- a/tpl/collections/apply.go
+++ b/tpl/collections/apply.go
@@ -21,7 +21,6 @@ import (
"strings"
"github.com/gohugoio/hugo/common/hreflect"
- "github.com/gohugoio/hugo/tpl"
)
// Apply takes an array or slice c and returns a new slice with the function fname applied over it.
@@ -48,7 +47,7 @@ func (ns *Namespace) Apply(ctx context.Context, c any, fname string, args ...any
switch seqv.Kind() {
case reflect.Array, reflect.Slice:
r := make([]any, seqv.Len())
- for i := 0; i < seqv.Len(); i++ {
+ for i := range seqv.Len() {
vv := seqv.Index(i)
vvv, err := applyFnToThis(ctx, fnv, vv, args...)
@@ -91,7 +90,7 @@ func applyFnToThis(ctx context.Context, fn, this reflect.Value, args ...any) (re
return reflect.ValueOf(nil), errors.New("Too many arguments")
}*/
- for i := 0; i < num; i++ {
+ for i := range num {
// AssignableTo reports whether xt is assignable to type targ.
if xt, targ := n[i].Type(), fn.Type().In(i); !xt.AssignableTo(targ) {
return reflect.ValueOf(nil), errors.New("called apply using " + xt.String() + " as type " + targ.String())
@@ -109,8 +108,7 @@ func applyFnToThis(ctx context.Context, fn, this reflect.Value, args ...any) (re
func (ns *Namespace) lookupFunc(ctx context.Context, fname string) (reflect.Value, bool) {
namespace, methodName, ok := strings.Cut(fname, ".")
if !ok {
- templ := ns.deps.Tmpl().(tpl.TemplateFuncGetter)
- return templ.GetFunc(fname)
+ return ns.deps.GetTemplateStore().GetFunc(fname)
}
// Namespace
diff --git a/tpl/collections/apply_test.go b/tpl/collections/apply_test.go
deleted file mode 100644
index 0a5764264..000000000
--- a/tpl/collections/apply_test.go
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package collections
-
-import (
- "context"
- "fmt"
- "io"
- "reflect"
- "testing"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/config/testconfig"
- "github.com/gohugoio/hugo/identity"
- "github.com/gohugoio/hugo/output"
- "github.com/gohugoio/hugo/output/layouts"
- "github.com/gohugoio/hugo/tpl"
-)
-
-type templateFinder int
-
-func (templateFinder) GetIdentity(string) (identity.Identity, bool) {
- return identity.StringIdentity("test"), true
-}
-
-func (templateFinder) Lookup(name string) (tpl.Template, bool) {
- return nil, false
-}
-
-func (templateFinder) HasTemplate(name string) bool {
- return false
-}
-
-func (templateFinder) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
- return nil, false, false
-}
-
-func (templateFinder) LookupVariants(name string) []tpl.Template {
- return nil
-}
-
-func (templateFinder) LookupLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) {
- return nil, false, nil
-}
-
-func (templateFinder) Execute(t tpl.Template, wr io.Writer, data any) error {
- return nil
-}
-
-func (templateFinder) ExecuteWithContext(ctx context.Context, t tpl.Template, wr io.Writer, data any) error {
- return nil
-}
-
-func (templateFinder) GetFunc(name string) (reflect.Value, bool) {
- if name == "dobedobedo" {
- return reflect.Value{}, false
- }
-
- return reflect.ValueOf(fmt.Sprint), true
-}
-
-func TestApply(t *testing.T) {
- t.Parallel()
- c := qt.New(t)
- d := testconfig.GetTestDeps(nil, nil)
- d.SetTempl(&tpl.TemplateHandlers{
- Tmpl: new(templateFinder),
- })
- ns := New(d)
-
- strings := []any{"a\n", "b\n"}
-
- ctx := context.Background()
-
- result, err := ns.Apply(ctx, strings, "print", "a", "b", "c")
- c.Assert(err, qt.IsNil)
- c.Assert(result, qt.DeepEquals, []any{"abc", "abc"})
-
- _, err = ns.Apply(ctx, strings, "apply", ".")
- c.Assert(err, qt.Not(qt.IsNil))
-
- var nilErr *error
- _, err = ns.Apply(ctx, nilErr, "chomp", ".")
- c.Assert(err, qt.Not(qt.IsNil))
-
- _, err = ns.Apply(ctx, strings, "dobedobedo", ".")
- c.Assert(err, qt.Not(qt.IsNil))
-
- _, err = ns.Apply(ctx, strings, "foo.Chomp", "c\n")
- if err == nil {
- t.Errorf("apply with unknown func should fail")
- }
-}
diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go
index edec536ef..0653a453a 100644
--- a/tpl/collections/collections.go
+++ b/tpl/collections/collections.go
@@ -20,13 +20,11 @@ import (
"errors"
"fmt"
"math/rand"
- "net/url"
"reflect"
"strings"
"time"
"github.com/gohugoio/hugo/common/collections"
- "github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/deps"
@@ -127,7 +125,7 @@ func (ns *Namespace) Delimit(ctx context.Context, l, sep any, last ...any) (stri
lv = reflect.ValueOf(sortSeq)
fallthrough
case reflect.Array, reflect.Slice, reflect.String:
- for i := 0; i < lv.Len(); i++ {
+ for i := range lv.Len() {
val := lv.Index(i).Interface()
valStr, err := cast.ToStringE(val)
if err != nil {
@@ -167,7 +165,7 @@ func (ns *Namespace) Dictionary(values ...any) (map[string]any, error) {
case string:
key = v
case []string:
- for i := 0; i < len(v)-1; i++ {
+ for i := range len(v) - 1 {
key = v[i]
var m map[string]any
v, found := dict[key]
@@ -189,54 +187,6 @@ func (ns *Namespace) Dictionary(values ...any) (map[string]any, error) {
return root, nil
}
-// EchoParam returns the value in the collection c with key k if is set; otherwise, it returns an
-// empty string.
-// Deprecated: Use the index function instead.
-func (ns *Namespace) EchoParam(c, k any) any {
- hugo.Deprecate("collections.EchoParam", "Use the index function instead.", "v0.120.0")
- av, isNil := indirect(reflect.ValueOf(c))
- if isNil {
- return ""
- }
-
- var avv reflect.Value
- switch av.Kind() {
- case reflect.Array, reflect.Slice:
- index, ok := k.(int)
- if ok && av.Len() > index {
- avv = av.Index(index)
- }
- case reflect.Map:
- kv := reflect.ValueOf(k)
- if kv.Type().AssignableTo(av.Type().Key()) {
- avv = av.MapIndex(kv)
- }
- }
-
- avv, isNil = indirect(avv)
-
- if isNil {
- return ""
- }
-
- if avv.IsValid() {
- switch avv.Kind() {
- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
- return avv.Int()
- case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
- return avv.Uint()
- case reflect.Float32, reflect.Float64:
- return avv.Float()
- case reflect.String:
- return avv.String()
- case reflect.Bool:
- return avv.Bool()
- }
- }
-
- return ""
-}
-
// First returns the first limit items in list l.
func (ns *Namespace) First(limit any, l any) (any, error) {
if limit == nil || l == nil {
@@ -285,7 +235,7 @@ func (ns *Namespace) In(l any, v any) (bool, error) {
switch lv.Kind() {
case reflect.Array, reflect.Slice:
- for i := 0; i < lv.Len(); i++ {
+ for i := range lv.Len() {
lvv, isNil := indirectInterface(lv.Index(i))
if isNil {
continue
@@ -327,13 +277,13 @@ func (ns *Namespace) Intersect(l1, l2 any) (any, error) {
ins = &intersector{r: reflect.MakeSlice(l1v.Type(), 0, 0), seen: make(map[any]bool)}
switch l2v.Kind() {
case reflect.Array, reflect.Slice:
- for i := 0; i < l1v.Len(); i++ {
+ for i := range l1v.Len() {
l1vv := l1v.Index(i)
if !l1vv.Type().Comparable() {
return make([]any, 0), errors.New("intersect does not support slices or arrays of uncomparable types")
}
- for j := 0; j < l2v.Len(); j++ {
+ for j := range l2v.Len() {
l2vv := l2v.Index(j)
if !l2vv.Type().Comparable() {
return make([]any, 0), errors.New("intersect does not support slices or arrays of uncomparable types")
@@ -432,47 +382,6 @@ func (ns *Namespace) Last(limit any, l any) (any, error) {
return seqv.Slice(seqv.Len()-limitv, seqv.Len()).Interface(), nil
}
-// Querify encodes the given params in URL-encoded form ("bar=baz&foo=quux") sorted by key.
-func (ns *Namespace) Querify(params ...any) (string, error) {
- qs := url.Values{}
-
- if len(params) == 1 {
- switch v := params[0].(type) {
- case []string:
- if len(v)%2 != 0 {
- return "", errors.New("invalid query")
- }
-
- for i := 0; i < len(v); i += 2 {
- qs.Add(v[i], v[i+1])
- }
-
- return qs.Encode(), nil
-
- case []any:
- params = v
-
- default:
- return "", errors.New("query keys must be strings")
- }
- }
-
- if len(params)%2 != 0 {
- return "", errors.New("invalid query")
- }
-
- for i := 0; i < len(params); i += 2 {
- switch v := params[i].(type) {
- case string:
- qs.Add(v, fmt.Sprintf("%v", params[i+1]))
- default:
- return "", errors.New("query keys must be strings")
- }
- }
-
- return qs.Encode(), nil
-}
-
// Reverse creates a copy of the list l and reverses it.
func (ns *Namespace) Reverse(l any) (any, error) {
if l == nil {
@@ -681,7 +590,7 @@ func (ns *Namespace) Union(l1, l2 any) (any, error) {
isNil bool
)
- for i := 0; i < l1v.Len(); i++ {
+ for i := range l1v.Len() {
l1vv, isNil = indirectInterface(l1v.Index(i))
if !l1vv.Type().Comparable() {
@@ -701,7 +610,7 @@ func (ns *Namespace) Union(l1, l2 any) (any, error) {
}
}
- for j := 0; j < l2v.Len(); j++ {
+ for j := range l2v.Len() {
l2vv := l2v.Index(j)
switch kind := l1vv.Kind(); {
@@ -752,7 +661,7 @@ func (ns *Namespace) Uniq(l any) (any, error) {
seen := make(map[any]bool)
- for i := 0; i < v.Len(); i++ {
+ for i := range v.Len() {
ev, _ := indirectInterface(v.Index(i))
key := normalize(ev)
diff --git a/tpl/collections/collections_integration_test.go b/tpl/collections/collections_integration_test.go
index 3bcd4effb..b60aaea87 100644
--- a/tpl/collections/collections_integration_test.go
+++ b/tpl/collections/collections_integration_test.go
@@ -52,7 +52,7 @@ Desc: {{ sort (sort $values "b" "desc") "a" "desc" }}
`
- for i := 0; i < 4; i++ {
+ for range 4 {
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
@@ -122,7 +122,7 @@ func TestAppendNilsToSliceWithNils(t *testing.T) {
`
- for i := 0; i < 4; i++ {
+ for range 4 {
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
@@ -202,35 +202,6 @@ foo: bc
b.AssertFileContent("public/index.html", "")
}
-// Issue #11498
-func TestEchoParams(t *testing.T) {
- t.Parallel()
- files := `
--- hugo.toml --
-[params.footer]
-string = 'foo'
-int = 42
-float = 3.1415
-boolt = true
-boolf = false
--- layouts/index.html --
-{{ echoParam .Site.Params.footer "string" }}
-{{ echoParam .Site.Params.footer "int" }}
-{{ echoParam .Site.Params.footer "float" }}
-{{ echoParam .Site.Params.footer "boolt" }}
-{{ echoParam .Site.Params.footer "boolf" }}
- `
-
- b := hugolib.Test(t, files)
- b.AssertFileContent("public/index.html",
- "foo",
- "42",
- "3.1415",
- "true",
- "false",
- )
-}
-
func TestTermEntriesCollectionsIssue12254(t *testing.T) {
t.Parallel()
@@ -278,3 +249,52 @@ tags: ['tag-b']
"2: Intersect: 1|\n2: Union: 3|\n2: SymDiff: 2|\n2: Uniq: 3|",
)
}
+
+// Issue #13181
+func TestUnionResourcesMatch(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- config.toml --
+disableKinds = ['rss','sitemap', 'taxonomy', 'term', 'page']
+-- layouts/index.html --
+{{ $a := resources.Match "*a*" }}
+{{ $b := resources.Match "*b*" }}
+{{ $union := $a | union $b }}
+{{ range $i, $e := $union }}
+{{ $i }}: {{ .Name }}
+{{ end }}$
+-- assets/a1.html --
+file1
+-- assets/a2.html --
+file2
+-- assets/a3_b1.html --
+file3
+-- assets/b2.html --
+file4
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContentExact("public/index.html", "0: /a3_b1.html\n\n1: /b2.html\n\n2: /a1.html\n\n3: /a2.html\n$")
+}
+
+// Issue 13621.
+func TestWhereNotInEmptySlice(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/home.html --
+{{- $pages := where site.RegularPages "Kind" "not in" (slice) -}}
+Len: {{ $pages | len }}|
+-- layouts/all.html --
+All|{{ .Title }}|
+-- content/p1.md --
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Len: 1|")
+}
diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go
index 7dd518759..fe7f2144d 100644
--- a/tpl/collections/collections_test.go
+++ b/tpl/collections/collections_test.go
@@ -232,38 +232,6 @@ func TestReverse(t *testing.T) {
c.Assert(err, qt.Not(qt.IsNil))
}
-func TestEchoParam(t *testing.T) {
- t.Parallel()
- c := qt.New(t)
-
- ns := newNs()
-
- for i, test := range []struct {
- a any
- key any
- expect any
- }{
- {[]int{1, 2, 3}, 1, int64(2)},
- {[]uint{1, 2, 3}, 1, uint64(2)},
- {[]float64{1.1, 2.2, 3.3}, 1, float64(2.2)},
- {[]string{"foo", "bar", "baz"}, 1, "bar"},
- {[]TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}}, 1, ""},
- {map[string]int{"foo": 1, "bar": 2, "baz": 3}, "bar", int64(2)},
- {map[string]uint{"foo": 1, "bar": 2, "baz": 3}, "bar", uint64(2)},
- {map[string]float64{"foo": 1.1, "bar": 2.2, "baz": 3.3}, "bar", float64(2.2)},
- {map[string]string{"foo": "FOO", "bar": "BAR", "baz": "BAZ"}, "bar", "BAR"},
- {map[string]TstX{"foo": {A: "a", B: "b"}, "bar": {A: "c", B: "d"}, "baz": {A: "e", B: "f"}}, "bar", ""},
- {map[string]any{"foo": nil}, "foo", ""},
- {(*[]string)(nil), "bar", ""},
- } {
- errMsg := qt.Commentf("[%d] %v", i, test)
-
- result := ns.EchoParam(test.a, test.key)
-
- c.Assert(result, qt.Equals, test.expect, errMsg)
- }
-}
-
func TestFirst(t *testing.T) {
t.Parallel()
c := qt.New(t)
@@ -544,68 +512,6 @@ func TestLast(t *testing.T) {
}
}
-func TestQuerify(t *testing.T) {
- t.Parallel()
- c := qt.New(t)
- ns := newNs()
-
- for i, test := range []struct {
- params []any
- expect any
- }{
- {[]any{"a", "b"}, "a=b"},
- {[]any{"a", "b", "c", "d", "f", " &"}, `a=b&c=d&f=+%26`},
- {[]any{[]string{"a", "b"}}, "a=b"},
- {[]any{[]string{"a", "b", "c", "d", "f", " &"}}, `a=b&c=d&f=+%26`},
- {[]any{[]any{"x", "y"}}, `x=y`},
- {[]any{[]any{"x", 5}}, `x=5`},
- // errors
- {[]any{5, "b"}, false},
- {[]any{"a", "b", "c"}, false},
- {[]any{[]string{"a", "b", "c"}}, false},
- {[]any{[]string{"a", "b"}, "c"}, false},
- {[]any{[]any{"c", "d", "e"}}, false},
- } {
- errMsg := qt.Commentf("[%d] %v", i, test.params)
-
- result, err := ns.Querify(test.params...)
-
- if b, ok := test.expect.(bool); ok && !b {
- c.Assert(err, qt.Not(qt.IsNil), errMsg)
- continue
- }
-
- c.Assert(err, qt.IsNil, errMsg)
- c.Assert(result, qt.Equals, test.expect, errMsg)
- }
-}
-
-func BenchmarkQuerify(b *testing.B) {
- ns := newNs()
- params := []any{"a", "b", "c", "d", "f", " &"}
-
- b.ResetTimer()
- for i := 0; i < b.N; i++ {
- _, err := ns.Querify(params...)
- if err != nil {
- b.Fatal(err)
- }
- }
-}
-
-func BenchmarkQuerifySlice(b *testing.B) {
- ns := newNs()
- params := []string{"a", "b", "c", "d", "f", " &"}
-
- b.ResetTimer()
- for i := 0; i < b.N; i++ {
- _, err := ns.Querify(params)
- if err != nil {
- b.Fatal(err)
- }
- }
-}
-
func TestSeq(t *testing.T) {
t.Parallel()
c := qt.New(t)
@@ -950,7 +856,7 @@ func ToTstXIs(slice any) []TstXI {
}
tis := make([]TstXI, s.Len())
- for i := 0; i < s.Len(); i++ {
+ for i := range s.Len() {
tsti, ok := s.Index(i).Interface().(TstXI)
if !ok {
return nil
diff --git a/tpl/collections/complement.go b/tpl/collections/complement.go
index 0cc2b5857..606d77dde 100644
--- a/tpl/collections/complement.go
+++ b/tpl/collections/complement.go
@@ -44,7 +44,7 @@ func (ns *Namespace) Complement(ls ...any) (any, error) {
switch v.Kind() {
case reflect.Array, reflect.Slice:
sl := reflect.MakeSlice(v.Type(), 0, 0)
- for i := 0; i < v.Len(); i++ {
+ for i := range v.Len() {
ev, _ := indirectInterface(v.Index(i))
if _, found := aset[normalize(ev)]; !found {
sl = reflect.Append(sl, ev)
diff --git a/tpl/collections/index.go b/tpl/collections/index.go
index df932f7c6..a319ea298 100644
--- a/tpl/collections/index.go
+++ b/tpl/collections/index.go
@@ -52,7 +52,7 @@ func (ns *Namespace) doIndex(item any, args ...any) (any, error) {
if len(args) == 1 {
v := reflect.ValueOf(args[0])
if v.Kind() == reflect.Slice {
- for i := 0; i < v.Len(); i++ {
+ for i := range v.Len() {
indices = append(indices, v.Index(i).Interface())
}
} else {
diff --git a/tpl/collections/init.go b/tpl/collections/init.go
index 8801422ac..f89651326 100644
--- a/tpl/collections/init.go
+++ b/tpl/collections/init.go
@@ -67,13 +67,6 @@ func init() {
[][2]string{},
)
- ns.AddMethodMapping(ctx.EchoParam,
- []string{"echoParam"},
- [][2]string{
- {`{{ echoParam .Params "langCode" }}`, `en`},
- },
- )
-
ns.AddMethodMapping(ctx.First,
[]string{"first"},
[][2]string{},
diff --git a/tpl/collections/merge.go b/tpl/collections/merge.go
index 8c019e412..b9696b502 100644
--- a/tpl/collections/merge.go
+++ b/tpl/collections/merge.go
@@ -75,9 +75,13 @@ func caseInsensitiveLookup(m, k reflect.Value) (reflect.Value, bool) {
return v, hreflect.IsTruthfulValue(v)
}
- for _, key := range m.MapKeys() {
- if strings.EqualFold(k.String(), key.String()) {
- return m.MapIndex(key), true
+ k2 := reflect.New(m.Type().Key()).Elem()
+
+ iter := m.MapRange()
+ for iter.Next() {
+ k2.SetIterKey(iter)
+ if strings.EqualFold(k.String(), k2.String()) {
+ return iter.Value(), true
}
}
@@ -90,17 +94,28 @@ func mergeMap(dst, src reflect.Value) reflect.Value {
// If the destination is Params, we must lower case all keys.
_, lowerCase := dst.Interface().(maps.Params)
+ k := reflect.New(dst.Type().Key()).Elem()
+ v := reflect.New(dst.Type().Elem()).Elem()
+
// Copy the destination map.
- for _, key := range dst.MapKeys() {
- v := dst.MapIndex(key)
- out.SetMapIndex(key, v)
+ iter := dst.MapRange()
+ for iter.Next() {
+ k.SetIterKey(iter)
+ v.SetIterValue(iter)
+ out.SetMapIndex(k, v)
}
// Add all keys in src not already in destination.
// Maps of the same type will be merged.
- for _, key := range src.MapKeys() {
- sv := src.MapIndex(key)
- dv, found := caseInsensitiveLookup(dst, key)
+ k = reflect.New(src.Type().Key()).Elem()
+ sv := reflect.New(src.Type().Elem()).Elem()
+
+ iter = src.MapRange()
+ for iter.Next() {
+ sv.SetIterValue(iter)
+ k.SetIterKey(iter)
+
+ dv, found := caseInsensitiveLookup(dst, k)
if found {
// If both are the same map key type, merge.
@@ -112,14 +127,15 @@ func mergeMap(dst, src reflect.Value) reflect.Value {
}
if dve.Type().Key() == sve.Type().Key() {
- out.SetMapIndex(key, mergeMap(dve, sve))
+ out.SetMapIndex(k, mergeMap(dve, sve))
}
}
} else {
- if lowerCase && key.Kind() == reflect.String {
- key = reflect.ValueOf(strings.ToLower(key.String()))
+ kk := k
+ if lowerCase && k.Kind() == reflect.String {
+ kk = reflect.ValueOf(strings.ToLower(k.String()))
}
- out.SetMapIndex(key, sv)
+ out.SetMapIndex(kk, sv)
}
}
diff --git a/tpl/collections/merge_test.go b/tpl/collections/merge_test.go
index 7809152d4..a8ef0afea 100644
--- a/tpl/collections/merge_test.go
+++ b/tpl/collections/merge_test.go
@@ -159,6 +159,18 @@ func TestMerge(t *testing.T) {
}
}
+func BenchmarkMerge(b *testing.B) {
+ ns := newNs()
+
+ for i := 0; i < b.N; i++ {
+ ns.Merge(
+ map[string]any{"a": 42, "c": 3, "e": 11},
+ map[string]any{"a": 1, "b": 2},
+ map[string]any{"a": 9, "c": 4, "d": 7},
+ )
+ }
+}
+
func TestMergeDataFormats(t *testing.T) {
c := qt.New(t)
ns := newNs()
diff --git a/tpl/collections/querify.go b/tpl/collections/querify.go
new file mode 100644
index 000000000..19e6d8afe
--- /dev/null
+++ b/tpl/collections/querify.go
@@ -0,0 +1,125 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package collections
+
+import (
+ "errors"
+ "net/url"
+
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/spf13/cast"
+)
+
+var (
+ errWrongArgStructure = errors.New("expected a map, a slice with an even number of elements, or an even number of scalar values, and each key must be a string")
+ errKeyIsEmptyString = errors.New("one of the keys is an empty string")
+)
+
+// Querify returns a URL query string composed of the given key-value pairs,
+// encoded and sorted by key.
+func (ns *Namespace) Querify(params ...any) (string, error) {
+ if len(params) == 0 {
+ return "", nil
+ }
+
+ if len(params) == 1 {
+ switch v := params[0].(type) {
+ case map[string]any: // created with collections.Dictionary
+ return mapToQueryString(v)
+ case maps.Params: // site configuration or page parameters
+ return mapToQueryString(v)
+ case []string:
+ return stringSliceToQueryString(v)
+ case []any:
+ s, err := interfaceSliceToStringSlice(v)
+ if err != nil {
+ return "", err
+ }
+ return stringSliceToQueryString(s)
+ default:
+ return "", errWrongArgStructure
+ }
+ }
+
+ if len(params)%2 != 0 {
+ return "", errWrongArgStructure
+ }
+
+ s, err := interfaceSliceToStringSlice(params)
+ if err != nil {
+ return "", err
+ }
+ return stringSliceToQueryString(s)
+}
+
+// mapToQueryString returns a URL query string derived from the given string
+// map, encoded and sorted by key. The function returns an error if it cannot
+// convert an element value to a string.
+func mapToQueryString[T map[string]any | maps.Params](m T) (string, error) {
+ if len(m) == 0 {
+ return "", nil
+ }
+
+ qs := url.Values{}
+ for k, v := range m {
+ if len(k) == 0 {
+ return "", errKeyIsEmptyString
+ }
+ vs, err := cast.ToStringE(v)
+ if err != nil {
+ return "", err
+ }
+ qs.Add(k, vs)
+ }
+ return qs.Encode(), nil
+}
+
+// sliceToQueryString returns a URL query string derived from the given slice
+// of strings, encoded and sorted by key. The function returns an error if
+// there are an odd number of elements.
+func stringSliceToQueryString(s []string) (string, error) {
+ if len(s) == 0 {
+ return "", nil
+ }
+ if len(s)%2 != 0 {
+ return "", errWrongArgStructure
+ }
+
+ qs := url.Values{}
+ for i := 0; i < len(s); i += 2 {
+ if len(s[i]) == 0 {
+ return "", errKeyIsEmptyString
+ }
+ qs.Add(s[i], s[i+1])
+ }
+ return qs.Encode(), nil
+}
+
+// interfaceSliceToStringSlice converts a slice of interfaces to a slice of
+// strings, returning an error if it cannot convert an element to a string.
+func interfaceSliceToStringSlice(s []any) ([]string, error) {
+ if len(s) == 0 {
+ return []string{}, nil
+ }
+
+ ss := make([]string, 0, len(s))
+ for _, v := range s {
+ vs, err := cast.ToStringE(v)
+ if err != nil {
+ return []string{}, err
+ }
+ ss = append(ss, vs)
+ }
+ return ss, nil
+}
diff --git a/tpl/collections/querify_test.go b/tpl/collections/querify_test.go
new file mode 100644
index 000000000..17556e4cb
--- /dev/null
+++ b/tpl/collections/querify_test.go
@@ -0,0 +1,121 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package collections
+
+import (
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/common/maps"
+)
+
+func TestQuerify(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+ ns := newNs()
+
+ for _, test := range []struct {
+ name string
+ params []any
+ expect any
+ }{
+ // map
+ {"01", []any{maps.Params{"a": "foo", "b": "bar"}}, `a=foo&b=bar`},
+ {"02", []any{maps.Params{"a": 6, "b": 7}}, `a=6&b=7`},
+ {"03", []any{maps.Params{"a": "foo", "b": 7}}, `a=foo&b=7`},
+ {"04", []any{map[string]any{"a": "foo", "b": "bar"}}, `a=foo&b=bar`},
+ {"05", []any{map[string]any{"a": 6, "b": 7}}, `a=6&b=7`},
+ {"06", []any{map[string]any{"a": "foo", "b": 7}}, `a=foo&b=7`},
+ // slice
+ {"07", []any{[]string{"a", "foo", "b", "bar"}}, `a=foo&b=bar`},
+ {"08", []any{[]any{"a", 6, "b", 7}}, `a=6&b=7`},
+ {"09", []any{[]any{"a", "foo", "b", 7}}, `a=foo&b=7`},
+ // sequence of scalar values
+ {"10", []any{"a", "foo", "b", "bar"}, `a=foo&b=bar`},
+ {"11", []any{"a", 6, "b", 7}, `a=6&b=7`},
+ {"12", []any{"a", "foo", "b", 7}, `a=foo&b=7`},
+ // empty map
+ {"13", []any{map[string]any{}}, ``},
+ // empty slice
+ {"14", []any{[]string{}}, ``},
+ {"15", []any{[]any{}}, ``},
+ // no arguments
+ {"16", []any{}, ``},
+ // errors: zero key length
+ {"17", []any{maps.Params{"": "foo"}}, false},
+ {"18", []any{map[string]any{"": "foo"}}, false},
+ {"19", []any{[]string{"", "foo"}}, false},
+ {"20", []any{[]any{"", 6}}, false},
+ {"21", []any{"", "foo"}, false},
+ // errors: odd number of values
+ {"22", []any{[]string{"a", "foo", "b"}}, false},
+ {"23", []any{[]any{"a", 6, "b"}}, false},
+ {"24", []any{"a", "foo", "b"}, false},
+ // errors: value cannot be cast to string
+ {"25", []any{map[string]any{"a": "foo", "b": tstNoStringer{}}}, false},
+ {"26", []any{[]any{"a", "foo", "b", tstNoStringer{}}}, false},
+ {"27", []any{"a", "foo", "b", tstNoStringer{}}, false},
+ } {
+ errMsg := qt.Commentf("[%s] %v", test.name, test.params)
+
+ result, err := ns.Querify(test.params...)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil), errMsg)
+ continue
+ }
+
+ c.Assert(err, qt.IsNil, errMsg)
+ c.Assert(result, qt.Equals, test.expect, errMsg)
+ }
+}
+
+func BenchmarkQuerify(b *testing.B) {
+ ns := newNs()
+ params := []any{"a", "b", "c", "d", "f", " &"}
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := ns.Querify(params...)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func BenchmarkQuerifySlice(b *testing.B) {
+ ns := newNs()
+ params := []string{"a", "b", "c", "d", "f", " &"}
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := ns.Querify(params)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func BenchmarkQuerifyMap(b *testing.B) {
+ ns := newNs()
+ params := map[string]any{"a": "b", "c": "d", "f": " &"}
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := ns.Querify(params)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
diff --git a/tpl/collections/reflect_helpers.go b/tpl/collections/reflect_helpers.go
index 4687acdde..05816a009 100644
--- a/tpl/collections/reflect_helpers.go
+++ b/tpl/collections/reflect_helpers.go
@@ -18,13 +18,14 @@ import (
"fmt"
"reflect"
+ "github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/types"
- "github.com/mitchellh/hashstructure"
+ "github.com/gohugoio/hugo/resources/resource"
)
var (
zero reflect.Value
- errorType = reflect.TypeOf((*error)(nil)).Elem()
+ errorType = reflect.TypeFor[error]()
)
func numberToFloat(v reflect.Value) (float64, error) {
@@ -47,21 +48,22 @@ func numberToFloat(v reflect.Value) (float64, error) {
// to make them comparable
func normalize(v reflect.Value) any {
k := v.Kind()
-
switch {
case !v.Type().Comparable():
- h, err := hashstructure.Hash(v.Interface(), nil)
- if err != nil {
- panic(err)
- }
- return h
+ return hashing.HashUint64(v.Interface())
case isNumber(k):
f, err := numberToFloat(v)
if err == nil {
return f
}
}
- return types.Unwrapv(v.Interface())
+
+ vv := types.Unwrapv(v.Interface())
+ if ip, ok := vv.(resource.TransientIdentifier); ok {
+ return ip.TransientKey()
+ }
+
+ return vv
}
// collects identities from the slices in seqs into a set. Numeric values are normalized,
@@ -72,7 +74,7 @@ func collectIdentities(seqs ...any) (map[any]bool, error) {
v := reflect.ValueOf(seq)
switch v.Kind() {
case reflect.Array, reflect.Slice:
- for i := 0; i < v.Len(); i++ {
+ for i := range v.Len() {
ev, _ := indirectInterface(v.Index(i))
if !ev.Type().Comparable() {
@@ -156,7 +158,6 @@ func convertNumber(v reflect.Value, to reflect.Kind) (reflect.Value, error) {
case reflect.Uint64:
n = reflect.ValueOf(uint64(i))
}
-
}
if !n.IsValid() {
diff --git a/tpl/collections/sort.go b/tpl/collections/sort.go
index 2040f8490..0c09f6af4 100644
--- a/tpl/collections/sort.go
+++ b/tpl/collections/sort.go
@@ -73,7 +73,7 @@ func (ns *Namespace) Sort(ctx context.Context, l any, args ...any) (any, error)
switch seqv.Kind() {
case reflect.Array, reflect.Slice:
- for i := 0; i < seqv.Len(); i++ {
+ for i := range seqv.Len() {
p.Pairs[i].Value = seqv.Index(i)
if sortByField == "" || sortByField == "value" {
p.Pairs[i].Key = p.Pairs[i].Value
@@ -99,18 +99,21 @@ func (ns *Namespace) Sort(ctx context.Context, l any, args ...any) (any, error)
}
case reflect.Map:
- keys := seqv.MapKeys()
- for i := 0; i < seqv.Len(); i++ {
- p.Pairs[i].Value = seqv.MapIndex(keys[i])
+ iter := seqv.MapRange()
+ i := 0
+ for iter.Next() {
+ key := iter.Key()
+ value := iter.Value()
+ p.Pairs[i].Value = value
if sortByField == "" {
- p.Pairs[i].Key = keys[i]
+ p.Pairs[i].Key = key
} else if sortByField == "value" {
p.Pairs[i].Key = p.Pairs[i].Value
} else {
v := p.Pairs[i].Value
var err error
- for i, elemName := range path {
+ for j, elemName := range path {
v, err = evaluateSubElem(ctxv, v, elemName)
if err != nil {
return nil, err
@@ -120,12 +123,13 @@ func (ns *Namespace) Sort(ctx context.Context, l any, args ...any) (any, error)
}
// Special handling of lower cased maps.
if params, ok := v.Interface().(maps.Params); ok {
- v = reflect.ValueOf(params.GetNested(path[i+1:]...))
+ v = reflect.ValueOf(params.GetNested(path[j+1:]...))
break
}
}
p.Pairs[i].Key = v
}
+ i++
}
}
diff --git a/tpl/collections/sort_test.go b/tpl/collections/sort_test.go
index 1ec95882f..cc4581921 100644
--- a/tpl/collections/sort_test.go
+++ b/tpl/collections/sort_test.go
@@ -261,3 +261,11 @@ func TestSort(t *testing.T) {
})
}
}
+
+func BenchmarkSortMap(b *testing.B) {
+ ns := newNs()
+ m := map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}
+ for i := 0; i < b.N; i++ {
+ ns.Sort(context.Background(), m)
+ }
+}
diff --git a/tpl/collections/symdiff.go b/tpl/collections/symdiff.go
index 8ecee3c4a..4b9dc6e42 100644
--- a/tpl/collections/symdiff.go
+++ b/tpl/collections/symdiff.go
@@ -44,7 +44,7 @@ func (ns *Namespace) SymDiff(s2, s1 any) (any, error) {
slice = reflect.MakeSlice(sliceType, 0, 0)
}
- for i := 0; i < v.Len(); i++ {
+ for i := range v.Len() {
ev, _ := indirectInterface(v.Index(i))
key := normalize(ev)
diff --git a/tpl/collections/where.go b/tpl/collections/where.go
index bf3f75044..ee49d0bbb 100644
--- a/tpl/collections/where.go
+++ b/tpl/collections/where.go
@@ -138,6 +138,9 @@ func (ns *Namespace) checkCondition(v, mv reflect.Value, op string) (bool, error
}
if mv.Len() == 0 {
+ if op == "not in" {
+ return true, nil
+ }
return false, nil
}
@@ -148,7 +151,7 @@ func (ns *Namespace) checkCondition(v, mv reflect.Value, op string) (bool, error
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
iv := v.Int()
ivp = &iv
- for i := 0; i < mv.Len(); i++ {
+ for i := range mv.Len() {
if anInt, err := toInt(mv.Index(i)); err == nil {
ima = append(ima, anInt)
}
@@ -156,7 +159,7 @@ func (ns *Namespace) checkCondition(v, mv reflect.Value, op string) (bool, error
case reflect.String:
sv := v.String()
svp = &sv
- for i := 0; i < mv.Len(); i++ {
+ for i := range mv.Len() {
if aString, err := toString(mv.Index(i)); err == nil {
sma = append(sma, aString)
}
@@ -164,7 +167,7 @@ func (ns *Namespace) checkCondition(v, mv reflect.Value, op string) (bool, error
case reflect.Float64:
fv := v.Float()
fvp = &fv
- for i := 0; i < mv.Len(); i++ {
+ for i := range mv.Len() {
if aFloat, err := toFloat(mv.Index(i)); err == nil {
fma = append(fma, aFloat)
}
@@ -173,7 +176,7 @@ func (ns *Namespace) checkCondition(v, mv reflect.Value, op string) (bool, error
if hreflect.IsTime(v.Type()) {
iv := ns.toTimeUnix(v)
ivp = &iv
- for i := 0; i < mv.Len(); i++ {
+ for i := range mv.Len() {
ima = append(ima, ns.toTimeUnix(mv.Index(i)))
}
}
@@ -397,7 +400,7 @@ func parseWhereArgs(args ...any) (mv reflect.Value, op string, err error) {
func (ns *Namespace) checkWhereArray(ctxv, seqv, kv, mv reflect.Value, path []string, op string) (any, error) {
rv := reflect.MakeSlice(seqv.Type(), 0, 0)
- for i := 0; i < seqv.Len(); i++ {
+ for i := range seqv.Len() {
var vvv reflect.Value
rvv := seqv.Index(i)
@@ -409,7 +412,6 @@ func (ns *Namespace) checkWhereArray(ctxv, seqv, kv, mv reflect.Value, path []st
for i, elemName := range path {
var err error
vvv, err = evaluateSubElem(ctxv, vvv, elemName)
-
if err != nil {
continue
}
@@ -442,9 +444,12 @@ func (ns *Namespace) checkWhereArray(ctxv, seqv, kv, mv reflect.Value, path []st
// checkWhereMap handles the where-matching logic when the seqv value is a Map.
func (ns *Namespace) checkWhereMap(ctxv, seqv, kv, mv reflect.Value, path []string, op string) (any, error) {
rv := reflect.MakeMap(seqv.Type())
- keys := seqv.MapKeys()
- for _, k := range keys {
- elemv := seqv.MapIndex(k)
+ k := reflect.New(seqv.Type().Key()).Elem()
+ elemv := reflect.New(seqv.Type().Elem()).Elem()
+ iter := seqv.MapRange()
+ for iter.Next() {
+ k.SetIterKey(iter)
+ elemv.SetIterValue(iter)
switch elemv.Kind() {
case reflect.Array, reflect.Slice:
r, err := ns.checkWhereArray(ctxv, elemv, kv, mv, path, op)
diff --git a/tpl/collections/where_test.go b/tpl/collections/where_test.go
index 7ec9572e0..ecf748f93 100644
--- a/tpl/collections/where_test.go
+++ b/tpl/collections/where_test.go
@@ -761,6 +761,7 @@ func TestCheckCondition(t *testing.T) {
expect{true, false},
},
{reflect.ValueOf(123), reflect.ValueOf([]int{45, 678}), "not in", expect{true, false}},
+ {reflect.ValueOf(123), reflect.ValueOf([]int{}), "not in", expect{true, false}},
{reflect.ValueOf("foo"), reflect.ValueOf([]string{"bar", "baz"}), "not in", expect{true, false}},
{
reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)),
@@ -865,10 +866,10 @@ func BenchmarkWhereOps(b *testing.B) {
ns := newNs()
var seq []map[string]string
ctx := context.Background()
- for i := 0; i < 500; i++ {
+ for range 500 {
seq = append(seq, map[string]string{"foo": "bar"})
}
- for i := 0; i < 500; i++ {
+ for range 500 {
seq = append(seq, map[string]string{"foo": "baz"})
}
// Shuffle the sequence.
@@ -902,3 +903,19 @@ func BenchmarkWhereOps(b *testing.B) {
}
})
}
+
+func BenchmarkWhereMap(b *testing.B) {
+ ns := newNs()
+ seq := map[string]string{}
+
+ for i := range 1000 {
+ seq[fmt.Sprintf("key%d", i)] = "value"
+ }
+
+ for i := 0; i < b.N; i++ {
+ _, err := ns.Where(context.Background(), seq, "key", "eq", "value")
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
diff --git a/tpl/compare/compare.go b/tpl/compare/compare.go
index 907be9b15..d32f3df95 100644
--- a/tpl/compare/compare.go
+++ b/tpl/compare/compare.go
@@ -117,7 +117,12 @@ func (n *Namespace) Eq(first any, others ...any) bool {
case reflect.Float32, reflect.Float64:
return vv.Float()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
- return vv.Uint()
+ i := vv.Uint()
+ // If it can fit in an int, convert it.
+ if i <= math.MaxInt64 {
+ return int64(i)
+ }
+ return i
case reflect.String:
return vv.String()
default:
@@ -226,8 +231,8 @@ func (n *Namespace) checkComparisonArgCount(min int, others ...any) bool {
// Conditional can be used as a ternary operator.
//
// It returns v1 if cond is true, else v2.
-func (n *Namespace) Conditional(cond bool, v1, v2 any) any {
- if cond {
+func (n *Namespace) Conditional(cond any, v1, v2 any) any {
+ if hreflect.IsTruthful(cond) {
return v1
}
return v2
@@ -237,6 +242,16 @@ func (ns *Namespace) compareGet(a any, b any) (float64, float64) {
return ns.compareGetWithCollator(nil, a, b)
}
+func (ns *Namespace) compareTwoUints(a uint64, b uint64) (float64, float64) {
+ if a < b {
+ return 1, 0
+ } else if a == b {
+ return 0, 0
+ } else {
+ return 0, 1
+ }
+}
+
func (ns *Namespace) compareGetWithCollator(collator *langs.Collator, a any, b any) (float64, float64) {
if ac, ok := a.(compare.Comparer); ok {
c := ac.Compare(b)
@@ -263,12 +278,22 @@ func (ns *Namespace) compareGetWithCollator(collator *langs.Collator, a any, b a
var left, right float64
var leftStr, rightStr *string
av := reflect.ValueOf(a)
+ bv := reflect.ValueOf(b)
switch av.Kind() {
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
left = float64(av.Len())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ if hreflect.IsUint(bv.Kind()) {
+ return ns.compareTwoUints(uint64(av.Int()), bv.Uint())
+ }
left = float64(av.Int())
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32:
+ left = float64(av.Uint())
+ case reflect.Uint64:
+ if hreflect.IsUint(bv.Kind()) {
+ return ns.compareTwoUints(av.Uint(), bv.Uint())
+ }
case reflect.Float32, reflect.Float64:
left = av.Float()
case reflect.String:
@@ -290,13 +315,20 @@ func (ns *Namespace) compareGetWithCollator(collator *langs.Collator, a any, b a
}
}
- bv := reflect.ValueOf(b)
-
switch bv.Kind() {
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
right = float64(bv.Len())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ if hreflect.IsUint(av.Kind()) {
+ return ns.compareTwoUints(av.Uint(), uint64(bv.Int()))
+ }
right = float64(bv.Int())
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32:
+ right = float64(bv.Uint())
+ case reflect.Uint64:
+ if hreflect.IsUint(av.Kind()) {
+ return ns.compareTwoUints(av.Uint(), bv.Uint())
+ }
case reflect.Float32, reflect.Float64:
right = bv.Float()
case reflect.String:
diff --git a/tpl/compare/compare_test.go b/tpl/compare/compare_test.go
index 4c50f5f0f..0ebebef4b 100644
--- a/tpl/compare/compare_test.go
+++ b/tpl/compare/compare_test.go
@@ -14,16 +14,16 @@
package compare
import (
+ "math"
"path"
"reflect"
"runtime"
"testing"
"time"
- "github.com/gohugoio/hugo/htesting/hqt"
-
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/htesting/hqt"
"github.com/spf13/cast"
)
@@ -199,6 +199,16 @@ func doTestCompare(t *testing.T, tp tstCompareType, funcUnderTest func(a, b any)
{5, 5, 0},
{int(5), int64(5), 0},
{int32(5), int(5), 0},
+ {int16(4), 4, 0},
+ {uint8(4), 4, 0},
+ {uint16(4), 4, 0},
+ {uint16(4), 4, 0},
+ {uint32(4), uint16(4), 0},
+ {uint32(4), uint16(3), 1},
+ {uint64(4), 4, 0},
+ {4, uint64(4), 0},
+ {uint64(math.MaxUint32), uint32(math.MaxUint32), 0},
+ {uint64(math.MaxUint16), int(math.MaxUint16), 0},
{int16(4), int(5), -1},
{uint(15), uint64(15), 0},
{-2, 1, -1},
@@ -436,12 +446,35 @@ func TestTimeUnix(t *testing.T) {
}
func TestConditional(t *testing.T) {
+ t.Parallel()
c := qt.New(t)
- n := New(time.UTC, false)
- a, b := "a", "b"
+ ns := New(time.UTC, false)
- c.Assert(n.Conditional(true, a, b), qt.Equals, a)
- c.Assert(n.Conditional(false, a, b), qt.Equals, b)
+ type args struct {
+ cond any
+ v1 any
+ v2 any
+ }
+ tests := []struct {
+ name string
+ args args
+ want any
+ }{
+ {"a", args{cond: true, v1: "true", v2: "false"}, "true"},
+ {"b", args{cond: false, v1: "true", v2: "false"}, "false"},
+ {"c", args{cond: 1, v1: "true", v2: "false"}, "true"},
+ {"d", args{cond: 0, v1: "true", v2: "false"}, "false"},
+ {"e", args{cond: "foo", v1: "true", v2: "false"}, "true"},
+ {"f", args{cond: "", v1: "true", v2: "false"}, "false"},
+ {"g", args{cond: []int{6, 7}, v1: "true", v2: "false"}, "true"},
+ {"h", args{cond: []int{}, v1: "true", v2: "false"}, "false"},
+ {"i", args{cond: nil, v1: "true", v2: "false"}, "false"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c.Assert(ns.Conditional(tt.args.cond, tt.args.v1, tt.args.v2), qt.Equals, tt.want)
+ })
+ }
}
// Issue 9462
diff --git a/tpl/crypto/crypto.go b/tpl/crypto/crypto.go
index 412a19212..677f59139 100644
--- a/tpl/crypto/crypto.go
+++ b/tpl/crypto/crypto.go
@@ -25,6 +25,7 @@ import (
"hash"
"hash/fnv"
+ "github.com/gohugoio/hugo/common/hugo"
"github.com/spf13/cast"
)
@@ -72,6 +73,7 @@ func (ns *Namespace) SHA256(v any) (string, error) {
// FNV32a hashes v using fnv32a algorithm.
// {"newIn": "0.98.0" }
func (ns *Namespace) FNV32a(v any) (int, error) {
+ hugo.Deprecate("crypto.FNV32a", "Use hash.FNV32a.", "v0.129.0")
conv, err := cast.ToStringE(v)
if err != nil {
return 0, err
diff --git a/tpl/crypto/init.go b/tpl/crypto/init.go
index 418fbd9fb..0527fba06 100644
--- a/tpl/crypto/init.go
+++ b/tpl/crypto/init.go
@@ -53,13 +53,6 @@ func init() {
},
)
- ns.AddMethodMapping(ctx.FNV32a,
- nil,
- [][2]string{
- {`{{ crypto.FNV32a "Hugo Rocks!!" }}`, `1515779328`},
- },
- )
-
ns.AddMethodMapping(ctx.HMAC,
[]string{"hmac"},
[][2]string{
diff --git a/tpl/css/css.go b/tpl/css/css.go
index 51fa1d518..199deda69 100644
--- a/tpl/css/css.go
+++ b/tpl/css/css.go
@@ -2,17 +2,42 @@ package css
import (
"context"
+ "errors"
+ "fmt"
+ "sync"
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/types/css"
"github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/gohugoio/hugo/resources/resource_transformers/babel"
+ "github.com/gohugoio/hugo/resources/resource_transformers/cssjs"
+ "github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass"
+ "github.com/gohugoio/hugo/resources/resource_transformers/tocss/sass"
+ "github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss"
"github.com/gohugoio/hugo/tpl/internal"
+ "github.com/gohugoio/hugo/tpl/internal/resourcehelpers"
"github.com/spf13/cast"
)
const name = "css"
// Namespace provides template functions for the "css" namespace.
-type Namespace struct{}
+type Namespace struct {
+ d *deps.Deps
+ scssClientLibSass *scss.Client
+ postcssClient *cssjs.PostCSSClient
+ tailwindcssClient *cssjs.TailwindCSSClient
+ babelClient *babel.Client
+
+ // The Dart Client requires a os/exec process, so only
+ // create it if we really need it.
+ // This is mostly to avoid creating one per site build test.
+ scssClientDartSassInit sync.Once
+ scssClientDartSass *dartsass.Client
+}
// Quoted returns a string that needs to be quoted in CSS.
func (ns *Namespace) Quoted(v any) css.QuotedString {
@@ -26,17 +51,142 @@ func (ns *Namespace) Unquoted(v any) css.UnquotedString {
return css.UnquotedString(s)
}
+// PostCSS processes the given Resource with PostCSS.
+func (ns *Namespace) PostCSS(args ...any) (resource.Resource, error) {
+ if len(args) > 2 {
+ return nil, errors.New("must not provide more arguments than resource object and options")
+ }
+
+ r, m, err := resourcehelpers.ResolveArgs(args)
+ if err != nil {
+ return nil, err
+ }
+
+ return ns.postcssClient.Process(r, m)
+}
+
+// TailwindCSS processes the given Resource with tailwindcss.
+func (ns *Namespace) TailwindCSS(args ...any) (resource.Resource, error) {
+ if len(args) > 2 {
+ return nil, errors.New("must not provide more arguments than resource object and options")
+ }
+
+ r, m, err := resourcehelpers.ResolveArgs(args)
+ if err != nil {
+ return nil, err
+ }
+
+ return ns.tailwindcssClient.Process(r, m)
+}
+
+// Sass processes the given Resource with SASS.
+func (ns *Namespace) Sass(args ...any) (resource.Resource, error) {
+ if len(args) > 2 {
+ return nil, errors.New("must not provide more arguments than resource object and options")
+ }
+
+ var (
+ r resources.ResourceTransformer
+ m map[string]any
+ targetPath string
+ err error
+ ok bool
+ transpiler = sass.TranspilerLibSass
+ )
+
+ r, targetPath, ok = resourcehelpers.ResolveIfFirstArgIsString(args)
+
+ if !ok {
+ r, m, err = resourcehelpers.ResolveArgs(args)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if m != nil {
+ if t, _, found := maps.LookupEqualFold(m, "transpiler"); found {
+ switch t {
+ case sass.TranspilerDart, sass.TranspilerLibSass:
+ transpiler = cast.ToString(t)
+ default:
+ return nil, fmt.Errorf("unsupported transpiler %q; valid values are %q or %q", t, sass.TranspilerLibSass, sass.TranspilerDart)
+ }
+ }
+ }
+
+ if transpiler == sass.TranspilerLibSass {
+ var options scss.Options
+ if targetPath != "" {
+ options.TargetPath = paths.ToSlashTrimLeading(targetPath)
+ } else if m != nil {
+ options, err = scss.DecodeOptions(m)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return ns.scssClientLibSass.ToCSS(r, options)
+ }
+
+ if m == nil {
+ m = make(map[string]any)
+ }
+ if targetPath != "" {
+ m["targetPath"] = targetPath
+ }
+
+ client, err := ns.getscssClientDartSass()
+ if err != nil {
+ return nil, err
+ }
+
+ return client.ToCSS(r, m)
+}
+
func init() {
f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
- ctx := &Namespace{}
+ scssClient, err := scss.New(d.BaseFs.Assets, d.ResourceSpec)
+ if err != nil {
+ panic(err)
+ }
+ ctx := &Namespace{
+ d: d,
+ scssClientLibSass: scssClient,
+ postcssClient: cssjs.NewPostCSSClient(d.ResourceSpec),
+ tailwindcssClient: cssjs.NewTailwindCSSClient(d.ResourceSpec),
+ babelClient: babel.New(d.ResourceSpec),
+ }
ns := &internal.TemplateFuncsNamespace{
Name: name,
Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
}
+ ns.AddMethodMapping(ctx.Sass,
+ []string{"toCSS"},
+ [][2]string{},
+ )
+
+ ns.AddMethodMapping(ctx.PostCSS,
+ []string{"postCSS"},
+ [][2]string{},
+ )
+
return ns
}
internal.AddTemplateFuncsNamespace(f)
}
+
+func (ns *Namespace) getscssClientDartSass() (*dartsass.Client, error) {
+ var err error
+ ns.scssClientDartSassInit.Do(func() {
+ ns.scssClientDartSass, err = dartsass.New(ns.d.BaseFs.Assets, ns.d.ResourceSpec)
+ if err != nil {
+ return
+ }
+ ns.d.BuildClosers.Add(ns.scssClientDartSass)
+ })
+
+ return ns.scssClientDartSass, err
+}
diff --git a/tpl/data/data.go b/tpl/data/data.go
index 097cfe4a8..ca1796826 100644
--- a/tpl/data/data.go
+++ b/tpl/data/data.go
@@ -36,6 +36,7 @@ import (
"github.com/spf13/cast"
"github.com/gohugoio/hugo/deps"
+ "slices"
)
// New returns a new instance of the data-namespaced template functions.
@@ -170,12 +171,7 @@ func hasHeaderValue(m http.Header, key, value string) bool {
return false
}
- for _, v := range s {
- if v == value {
- return true
- }
- }
- return false
+ return slices.Contains(s, value)
}
func hasHeaderKey(m http.Header, key string) bool {
diff --git a/tpl/data/resources.go b/tpl/data/resources.go
index 3a3701d60..9e06c0cce 100644
--- a/tpl/data/resources.go
+++ b/tpl/data/resources.go
@@ -23,7 +23,7 @@ import (
"time"
"github.com/gohugoio/hugo/cache/filecache"
- "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/common/hashing"
"github.com/spf13/afero"
)
@@ -44,7 +44,7 @@ func (ns *Namespace) getRemote(cache *filecache.Cache, unmarshal func([]byte) (b
var headers bytes.Buffer
req.Header.Write(&headers)
- id := helpers.MD5String(url + headers.String())
+ id := hashing.MD5FromStringHexEncoded(url + headers.String())
var handled bool
var retry bool
diff --git a/tpl/data/resources_test.go b/tpl/data/resources_test.go
index b8003bf43..d49e74d4c 100644
--- a/tpl/data/resources_test.go
+++ b/tpl/data/resources_test.go
@@ -155,11 +155,11 @@ func TestScpGetRemoteParallel(t *testing.T) {
var wg sync.WaitGroup
- for i := 0; i < 1; i++ {
+ for i := range 1 {
wg.Add(1)
go func(gor int) {
defer wg.Done()
- for j := 0; j < 10; j++ {
+ for range 10 {
var cb []byte
f := func(b []byte) (bool, error) {
cb = b
diff --git a/tpl/debug/debug.go b/tpl/debug/debug.go
index 027454b53..3909cbfa2 100644
--- a/tpl/debug/debug.go
+++ b/tpl/debug/debug.go
@@ -30,12 +30,9 @@ import (
// New returns a new instance of the debug-namespaced template functions.
func New(d *deps.Deps) *Namespace {
- var timers map[string][]*timer
+ ns := &Namespace{}
if d.Log.Level() <= logg.LevelInfo {
- timers = make(map[string][]*timer)
- }
- ns := &Namespace{
- timers: timers,
+ ns.timers = make(map[string][]*timer)
}
if ns.timers == nil {
@@ -44,7 +41,7 @@ func New(d *deps.Deps) *Namespace {
l := d.Log.InfoCommand("timer")
- d.BuildEndListeners.Add(func() {
+ d.BuildEndListeners.Add(func(...any) bool {
type data struct {
Name string
Count int
@@ -55,7 +52,7 @@ func New(d *deps.Deps) *Namespace {
var timersSorted []data
- for k, v := range timers {
+ for k, v := range ns.timers {
var total time.Duration
var median time.Duration
sort.Slice(v, func(i, j int) bool {
@@ -87,6 +84,8 @@ func New(d *deps.Deps) *Namespace {
}
ns.timers = make(map[string][]*timer)
+
+ return false
})
return ns
@@ -172,7 +171,7 @@ func (ns *Namespace) TestDeprecationInfo(item, alternative string) string {
// Internal template func, used in tests only.
func (ns *Namespace) TestDeprecationWarn(item, alternative string) string {
v := hugo.CurrentVersion
- v.Minor -= 6
+ v.Minor -= 3
hugo.Deprecate(item, alternative, v.String())
return ""
}
@@ -180,7 +179,7 @@ func (ns *Namespace) TestDeprecationWarn(item, alternative string) string {
// Internal template func, used in tests only.
func (ns *Namespace) TestDeprecationErr(item, alternative string) string {
v := hugo.CurrentVersion
- v.Minor -= 12
+ v.Minor -= 15
hugo.Deprecate(item, alternative, v.String())
return ""
}
diff --git a/tpl/fmt/fmt.go b/tpl/fmt/fmt.go
index e9c18360a..264cb1435 100644
--- a/tpl/fmt/fmt.go
+++ b/tpl/fmt/fmt.go
@@ -30,8 +30,9 @@ func New(d *deps.Deps) *Namespace {
logger: d.Log,
}
- d.BuildStartListeners.Add(func() {
+ d.BuildStartListeners.Add(func(...any) bool {
ns.logger.Reset()
+ return false
})
return ns
diff --git a/tpl/fmt/fmt_integration_test.go b/tpl/fmt/fmt_integration_test.go
index 74322770e..87a89943c 100644
--- a/tpl/fmt/fmt_integration_test.go
+++ b/tpl/fmt/fmt_integration_test.go
@@ -27,16 +27,22 @@ func TestErroridf(t *testing.T) {
files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
-ignoreErrors = ['error-b']
+ignoreErrors = ['error-b','error-C']
-- layouts/index.html --
{{ erroridf "error-a" "%s" "a"}}
{{ erroridf "error-b" "%s" "b"}}
+{{ erroridf "error-C" "%s" "C"}}
+{{ erroridf "error-c" "%s" "c"}}
+ {{ erroridf "error-d" "%s" "D"}}
`
b, err := hugolib.TestE(t, files)
b.Assert(err, qt.IsNotNil)
- b.AssertLogMatches(`^ERROR a\nYou can suppress this error by adding the following to your site configuration:\nignoreLogs = \['error-a'\]\n$`)
+ b.AssertLogMatches(`ERROR a\nYou can suppress this error by adding the following to your site configuration:\nignoreLogs = \['error-a'\]`)
+ b.AssertLogMatches(`ERROR D`)
+ b.AssertLogMatches(`! ERROR C`)
+ b.AssertLogMatches(`! ERROR c`)
}
func TestWarnidf(t *testing.T) {
@@ -45,13 +51,15 @@ func TestWarnidf(t *testing.T) {
files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
-ignoreLogs = ['warning-b']
+ignoreLogs = ['warning-b', 'WarniNg-C']
-- layouts/index.html --
{{ warnidf "warning-a" "%s" "a"}}
{{ warnidf "warning-b" "%s" "b"}}
+{{ warnidf "warNing-C" "%s" "c"}}
`
b := hugolib.Test(t, files, hugolib.TestOptWarn())
b.AssertLogContains("WARN a", "You can suppress this warning", "ignoreLogs", "['warning-a']")
- b.AssertLogNotContains("['warning-b']")
+ b.AssertLogContains("! ['warning-b']")
+ b.AssertLogContains("! ['warning-c']")
}
diff --git a/tpl/hash/hash.go b/tpl/hash/hash.go
new file mode 100644
index 000000000..00df4e3cd
--- /dev/null
+++ b/tpl/hash/hash.go
@@ -0,0 +1,85 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package hash provides non-cryptographic hash functions for template use.
+package hash
+
+import (
+ "context"
+ "hash/fnv"
+
+ "github.com/gohugoio/hugo/common/hashing"
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/tpl/internal"
+ "github.com/spf13/cast"
+)
+
+// New returns a new instance of the hash-namespaced template functions.
+func New() *Namespace {
+ return &Namespace{}
+}
+
+// Namespace provides template functions for the "hash" namespace.
+type Namespace struct{}
+
+// FNV32a hashes v using fnv32a algorithm.
+func (ns *Namespace) FNV32a(v any) (int, error) {
+ conv, err := cast.ToStringE(v)
+ if err != nil {
+ return 0, err
+ }
+ algorithm := fnv.New32a()
+ algorithm.Write([]byte(conv))
+ return int(algorithm.Sum32()), nil
+}
+
+// XxHash returns the xxHash of the input string.
+func (ns *Namespace) XxHash(v any) (string, error) {
+ conv, err := cast.ToStringE(v)
+ if err != nil {
+ return "", err
+ }
+
+ return hashing.XxHashFromStringHexEncoded(conv), nil
+}
+
+const name = "hash"
+
+func init() {
+ f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
+ ctx := New()
+
+ ns := &internal.TemplateFuncsNamespace{
+ Name: name,
+ Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
+ }
+
+ ns.AddMethodMapping(ctx.XxHash,
+ []string{"xxhash"},
+ [][2]string{
+ {`{{ hash.XxHash "The quick brown fox jumps over the lazy dog" }}`, `0b242d361fda71bc`},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.FNV32a,
+ nil,
+ [][2]string{
+ {`{{ hash.FNV32a "Hugo Rocks!!" }}`, `1515779328`},
+ },
+ )
+
+ return ns
+ }
+
+ internal.AddTemplateFuncsNamespace(f)
+}
diff --git a/tpl/hash/hash_test.go b/tpl/hash/hash_test.go
new file mode 100644
index 000000000..ff5d59a9a
--- /dev/null
+++ b/tpl/hash/hash_test.go
@@ -0,0 +1,84 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package hash provides non-cryptographic hash functions for template use.
+package hash
+
+import (
+ "crypto/md5"
+ "encoding/hex"
+ "fmt"
+ "strings"
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+ "github.com/spf13/cast"
+)
+
+func TestXxHash(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+
+ ns := New()
+
+ h, err := ns.XxHash("The quick brown fox jumps over the lazy dog")
+ c.Assert(err, qt.IsNil)
+ // Facit: https://asecuritysite.com/encryption/xxhash?val=The%20quick%20brown%20fox%20jumps%20over%20the%20lazy%20dog
+ c.Assert(h, qt.Equals, "0b242d361fda71bc")
+}
+
+func BenchmarkXxHash(b *testing.B) {
+ const inputSmall = "The quick brown fox jumps over the lazy dog"
+ inputLarge := strings.Repeat(inputSmall, 100)
+
+ runBench := func(name, input string, b *testing.B, fn func(v any)) {
+ b.Run(fmt.Sprintf("%s_%d", name, len(input)), func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ fn(input)
+ }
+ })
+ }
+
+ ns := New()
+ fnXxHash := func(v any) {
+ _, err := ns.XxHash(v)
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ fnFNv32a := func(v any) {
+ _, err := ns.FNV32a(v)
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ // Copied from the crypto tpl/crypto package,
+ // just to have something to compare the above with.
+ fnMD5 := func(v any) {
+ conv, err := cast.ToStringE(v)
+ if err != nil {
+ panic(err)
+ }
+
+ hash := md5.Sum([]byte(conv))
+ _ = hex.EncodeToString(hash[:])
+ }
+
+ for _, input := range []string{inputSmall, inputLarge} {
+ runBench("xxHash", input, b, fnXxHash)
+ runBench("mdb5", input, b, fnMD5)
+ runBench("fnv32a", input, b, fnFNv32a)
+ }
+}
diff --git a/tpl/images/images.go b/tpl/images/images.go
index 02ffb333f..6296a7214 100644
--- a/tpl/images/images.go
+++ b/tpl/images/images.go
@@ -16,11 +16,18 @@ package images
import (
"errors"
+ "fmt"
"image"
+ "path"
"sync"
"github.com/bep/overlayfs"
+ "github.com/gohugoio/hugo/common/hashing"
+ "github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/resources/images"
+ "github.com/gohugoio/hugo/resources/resource_factories/create"
+ "github.com/mitchellh/mapstructure"
+ "rsc.io/qr"
// Importing image codecs for image.DecodeConfig
_ "image/gif"
@@ -50,21 +57,22 @@ func New(d *deps.Deps) *Namespace {
}
return &Namespace{
- readFileFs: readFileFs,
- Filters: &images.Filters{},
- cache: map[string]image.Config{},
- deps: d,
+ readFileFs: readFileFs,
+ Filters: &images.Filters{},
+ cache: map[string]image.Config{},
+ deps: d,
+ createClient: create.New(d.ResourceSpec),
}
}
// Namespace provides template functions for the "images" namespace.
type Namespace struct {
*images.Filters
- readFileFs afero.Fs
- cacheMu sync.RWMutex
- cache map[string]image.Config
-
- deps *deps.Deps
+ readFileFs afero.Fs
+ cacheMu sync.RWMutex
+ cache map[string]image.Config
+ deps *deps.Deps
+ createClient *create.Client
}
// Config returns the image.Config for the specified path relative to the
@@ -117,3 +125,87 @@ func (ns *Namespace) Filter(args ...any) (images.ImageResource, error) {
return img.Filter(filtersv...)
}
+
+var qrErrorCorrectionLevels = map[string]qr.Level{
+ "low": qr.L,
+ "medium": qr.M,
+ "quartile": qr.Q,
+ "high": qr.H,
+}
+
+// QR encodes the given text into a QR code using the specified options,
+// returning an image resource.
+func (ns *Namespace) QR(args ...any) (images.ImageResource, error) {
+ const (
+ qrDefaultErrorCorrectionLevel = "medium"
+ qrDefaultScale = 4
+ )
+
+ opts := struct {
+ Level string // error correction level; one of low, medium, quartile, or high
+ Scale int // number of image pixels per QR code module
+ TargetDir string // target directory relative to publishDir
+ }{
+ Level: qrDefaultErrorCorrectionLevel,
+ Scale: qrDefaultScale,
+ }
+
+ if len(args) == 0 || len(args) > 2 {
+ return nil, errors.New("requires 1 or 2 arguments")
+ }
+
+ text, err := cast.ToStringE(args[0])
+ if err != nil {
+ return nil, err
+ }
+
+ if text == "" {
+ return nil, errors.New("cannot encode an empty string")
+ }
+
+ if len(args) == 2 {
+ err := mapstructure.WeakDecode(args[1], &opts)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ level, ok := qrErrorCorrectionLevels[opts.Level]
+ if !ok {
+ return nil, errors.New("error correction level must be one of low, medium, quartile, or high")
+ }
+
+ if opts.Scale < 2 {
+ return nil, errors.New("scale must be an integer greater than or equal to 2")
+ }
+
+ targetPath := path.Join(opts.TargetDir, fmt.Sprintf("qr_%s.png", hashing.HashStringHex(text, opts)))
+
+ r, err := ns.createClient.FromOpts(
+ create.Options{
+ TargetPath: targetPath,
+ TargetPathHasHash: true,
+ CreateContent: func() (func() (hugio.ReadSeekCloser, error), error) {
+ code, err := qr.Encode(text, level)
+ if err != nil {
+ return nil, err
+ }
+ code.Scale = opts.Scale
+ png := code.PNG()
+ return func() (hugio.ReadSeekCloser, error) {
+ return hugio.NewReadSeekerNoOpCloserFromBytes(png), nil
+ }, nil
+ },
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ ir, ok := r.(images.ImageResource)
+ if !ok {
+ panic("bug: resource is not an image resource")
+ }
+
+ return ir, nil
+}
diff --git a/tpl/images/images_integration_test.go b/tpl/images/images_integration_test.go
index 003422aed..226165070 100644
--- a/tpl/images/images_integration_test.go
+++ b/tpl/images/images_integration_test.go
@@ -14,9 +14,12 @@
package images_test
import (
+ "strings"
"testing"
+ qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/resources/images/imagetesting"
)
func TestImageConfigFromModule(t *testing.T) {
@@ -49,3 +52,93 @@ fileExists2 OK: true|
imageConfig2 OK: 1|
`)
}
+
+func TestQR(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- layouts/index.html --
+{{- $text := "https://gohugo.io" }}
+{{- $optionMaps := slice
+ (dict)
+ (dict "level" "medium")
+ (dict "level" "medium" "scale" 4)
+ (dict "level" "low" "scale" 2)
+ (dict "level" "medium" "scale" 3)
+ (dict "level" "quartile" "scale" 5)
+ (dict "level" "high" "scale" 6)
+ (dict "level" "high" "scale" 6 "targetDir" "foo/bar")
+}}
+{{- range $k, $opts := $optionMaps }}
+ {{- with images.QR $text $opts }}
+
+ {{- end }}
+{{- end }}
+`
+
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.html",
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ )
+
+ files = strings.ReplaceAll(files, "low", "foo")
+
+ b, err := hugolib.TestE(t, files)
+ b.Assert(err.Error(), qt.Contains, "error correction level must be one of low, medium, quartile, or high")
+
+ files = strings.ReplaceAll(files, "foo", "low")
+ files = strings.ReplaceAll(files, "https://gohugo.io", "")
+
+ b, err = hugolib.TestE(t, files)
+ b.Assert(err.Error(), qt.Contains, "cannot encode an empty string")
+}
+
+func TestImagesGoldenFuncs(t *testing.T) {
+ t.Parallel()
+
+ if imagetesting.SkipGoldenTests {
+ t.Skip("Skip golden test on this architecture")
+ }
+
+ // Will be used as the base folder for generated images.
+ name := "funcs"
+
+ files := `
+-- hugo.toml --
+-- assets/sunset.jpg --
+sourcefilename: ../../resources/testdata/sunset.jpg
+
+-- layouts/index.html --
+Home.
+
+{{ template "copy" (dict "name" "qr-default.png" "img" (images.QR "https://gohugo.io")) }}
+{{ template "copy" (dict "name" "qr-level-high_scale-6.png" "img" (images.QR "https://gohugo.io" (dict "level" "high" "scale" 6))) }}
+
+{{ define "copy"}}
+{{ if lt (len (path.Ext .name)) 4 }}
+ {{ errorf "No extension in %q" .name }}
+{{ end }}
+{{ $img := .img }}
+{{ $name := printf "images/%s" .name }}
+{{ with $img | resources.Copy $name }}
+{{ .Publish }}
+{{ end }}
+{{ end }}
+`
+
+ opts := imagetesting.DefaultGoldenOpts
+ opts.T = t
+ opts.Name = name
+ opts.Files = files
+
+ imagetesting.RunGolden(opts)
+}
diff --git a/tpl/images/testdata/images_golden/funcs/qr-default.png b/tpl/images/testdata/images_golden/funcs/qr-default.png
new file mode 100644
index 000000000..6c7ab919e
Binary files /dev/null and b/tpl/images/testdata/images_golden/funcs/qr-default.png differ
diff --git a/tpl/images/testdata/images_golden/funcs/qr-level-high_scale-6.png b/tpl/images/testdata/images_golden/funcs/qr-level-high_scale-6.png
new file mode 100644
index 000000000..17e00f1a1
Binary files /dev/null and b/tpl/images/testdata/images_golden/funcs/qr-level-high_scale-6.png differ
diff --git a/tpl/internal/go_templates/cfg/cfg.go b/tpl/internal/go_templates/cfg/cfg.go
index 2af0ec707..932976972 100644
--- a/tpl/internal/go_templates/cfg/cfg.go
+++ b/tpl/internal/go_templates/cfg/cfg.go
@@ -36,12 +36,15 @@ const KnownEnv = `
GOAMD64
GOARCH
GOARM
+ GOARM64
+ GOAUTH
GOBIN
GOCACHE
GOCACHEPROG
GOENV
GOEXE
GOEXPERIMENT
+ GOFIPS140
GOFLAGS
GOGCCFLAGS
GOHOSTARCH
@@ -57,6 +60,7 @@ const KnownEnv = `
GOPPC64
GOPRIVATE
GOPROXY
+ GORISCV64
GOROOT
GOSUMDB
GOTMPDIR
diff --git a/tpl/internal/go_templates/fmtsort/sort.go b/tpl/internal/go_templates/fmtsort/sort.go
index 278a89bd7..f51cdc708 100644
--- a/tpl/internal/go_templates/fmtsort/sort.go
+++ b/tpl/internal/go_templates/fmtsort/sort.go
@@ -9,25 +9,23 @@
package fmtsort
import (
+ "cmp"
"reflect"
- "sort"
+ "slices"
)
// Note: Throughout this package we avoid calling reflect.Value.Interface as
// it is not always legal to do so and it's easier to avoid the issue than to face it.
-// SortedMap represents a map's keys and values. The keys and values are
-// aligned in index order: Value[i] is the value in the map corresponding to Key[i].
-type SortedMap struct {
- Key []reflect.Value
- Value []reflect.Value
-}
+// SortedMap is a slice of KeyValue pairs that simplifies sorting
+// and iterating over map entries.
+//
+// Each KeyValue pair contains a map key and its corresponding value.
+type SortedMap []KeyValue
-func (o *SortedMap) Len() int { return len(o.Key) }
-func (o *SortedMap) Less(i, j int) bool { return compare(o.Key[i], o.Key[j]) < 0 }
-func (o *SortedMap) Swap(i, j int) {
- o.Key[i], o.Key[j] = o.Key[j], o.Key[i]
- o.Value[i], o.Value[j] = o.Value[j], o.Value[i]
+// KeyValue holds a single key and value pair found in a map.
+type KeyValue struct {
+ Key, Value reflect.Value
}
// Sort accepts a map and returns a SortedMap that has the same keys and
@@ -48,7 +46,7 @@ func (o *SortedMap) Swap(i, j int) {
// Otherwise identical arrays compare by length.
// - interface values compare first by reflect.Type describing the concrete type
// and then by concrete value as described in the previous rules.
-func Sort(mapValue reflect.Value) *SortedMap {
+func Sort(mapValue reflect.Value) SortedMap {
if mapValue.Type().Kind() != reflect.Map {
return nil
}
@@ -56,18 +54,14 @@ func Sort(mapValue reflect.Value) *SortedMap {
// of a concurrent map update. The runtime is responsible for
// yelling loudly if that happens. See issue 33275.
n := mapValue.Len()
- key := make([]reflect.Value, 0, n)
- value := make([]reflect.Value, 0, n)
+ sorted := make(SortedMap, 0, n)
iter := mapValue.MapRange()
for iter.Next() {
- key = append(key, iter.Key())
- value = append(value, iter.Value())
+ sorted = append(sorted, KeyValue{iter.Key(), iter.Value()})
}
- sorted := &SortedMap{
- Key: key,
- Value: value,
- }
- sort.Stable(sorted)
+ slices.SortStableFunc(sorted, func(a, b KeyValue) int {
+ return compare(a.Key, b.Key)
+ })
return sorted
}
@@ -82,43 +76,19 @@ func compare(aVal, bVal reflect.Value) int {
}
switch aVal.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
- a, b := aVal.Int(), bVal.Int()
- switch {
- case a < b:
- return -1
- case a > b:
- return 1
- default:
- return 0
- }
+ return cmp.Compare(aVal.Int(), bVal.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
- a, b := aVal.Uint(), bVal.Uint()
- switch {
- case a < b:
- return -1
- case a > b:
- return 1
- default:
- return 0
- }
+ return cmp.Compare(aVal.Uint(), bVal.Uint())
case reflect.String:
- a, b := aVal.String(), bVal.String()
- switch {
- case a < b:
- return -1
- case a > b:
- return 1
- default:
- return 0
- }
+ return cmp.Compare(aVal.String(), bVal.String())
case reflect.Float32, reflect.Float64:
- return floatCompare(aVal.Float(), bVal.Float())
+ return cmp.Compare(aVal.Float(), bVal.Float())
case reflect.Complex64, reflect.Complex128:
a, b := aVal.Complex(), bVal.Complex()
- if c := floatCompare(real(a), real(b)); c != 0 {
+ if c := cmp.Compare(real(a), real(b)); c != 0 {
return c
}
- return floatCompare(imag(a), imag(b))
+ return cmp.Compare(imag(a), imag(b))
case reflect.Bool:
a, b := aVal.Bool(), bVal.Bool()
switch {
@@ -130,28 +100,12 @@ func compare(aVal, bVal reflect.Value) int {
return -1
}
case reflect.Pointer, reflect.UnsafePointer:
- a, b := aVal.Pointer(), bVal.Pointer()
- switch {
- case a < b:
- return -1
- case a > b:
- return 1
- default:
- return 0
- }
+ return cmp.Compare(aVal.Pointer(), bVal.Pointer())
case reflect.Chan:
if c, ok := nilCompare(aVal, bVal); ok {
return c
}
- ap, bp := aVal.Pointer(), bVal.Pointer()
- switch {
- case ap < bp:
- return -1
- case ap > bp:
- return 1
- default:
- return 0
- }
+ return cmp.Compare(aVal.Pointer(), bVal.Pointer())
case reflect.Struct:
for i := 0; i < aVal.NumField(); i++ {
if c := compare(aVal.Field(i), bVal.Field(i)); c != 0 {
@@ -198,22 +152,3 @@ func nilCompare(aVal, bVal reflect.Value) (int, bool) {
}
return 0, false
}
-
-// floatCompare compares two floating-point values. NaNs compare low.
-func floatCompare(a, b float64) int {
- switch {
- case isNaN(a):
- return -1 // No good answer if b is a NaN so don't bother checking.
- case isNaN(b):
- return 1
- case a < b:
- return -1
- case a > b:
- return 1
- }
- return 0
-}
-
-func isNaN(a float64) bool {
- return a != a
-}
diff --git a/tpl/internal/go_templates/fmtsort/sort_test.go b/tpl/internal/go_templates/fmtsort/sort_test.go
index e86b4c673..0986dbb6d 100644
--- a/tpl/internal/go_templates/fmtsort/sort_test.go
+++ b/tpl/internal/go_templates/fmtsort/sort_test.go
@@ -5,12 +5,13 @@
package fmtsort_test
import (
+ "cmp"
"fmt"
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
"math"
"reflect"
"runtime"
- "sort"
+ "slices"
"strings"
"testing"
"unsafe"
@@ -67,10 +68,6 @@ func TestCompare(t *testing.T) {
switch {
case i == j:
expect = 0
- // NaNs are tricky.
- if typ := v0.Type(); (typ.Kind() == reflect.Float32 || typ.Kind() == reflect.Float64) && math.IsNaN(v0.Float()) {
- expect = -1
- }
case i < j:
expect = -1
case i > j:
@@ -142,13 +139,13 @@ func sprint(data any) string {
return "nil"
}
b := new(strings.Builder)
- for i, key := range om.Key {
+ for i, m := range om {
if i > 0 {
b.WriteRune(' ')
}
- b.WriteString(sprintKey(key))
+ b.WriteString(sprintKey(m.Key))
b.WriteRune(':')
- fmt.Fprint(b, om.Value[i])
+ fmt.Fprint(b, m.Value)
}
return b.String()
}
@@ -200,8 +197,8 @@ func makeChans() []chan int {
for i := range cs {
pin.Pin(reflect.ValueOf(cs[i]).UnsafePointer())
}
- sort.Slice(cs, func(i, j int) bool {
- return uintptr(reflect.ValueOf(cs[i]).UnsafePointer()) < uintptr(reflect.ValueOf(cs[j]).UnsafePointer())
+ slices.SortFunc(cs, func(a, b chan int) int {
+ return cmp.Compare(reflect.ValueOf(a).Pointer(), reflect.ValueOf(b).Pointer())
})
return cs
}
diff --git a/tpl/internal/go_templates/htmltemplate/content.go b/tpl/internal/go_templates/htmltemplate/content.go
index 898084876..d19b1ec12 100644
--- a/tpl/internal/go_templates/htmltemplate/content.go
+++ b/tpl/internal/go_templates/htmltemplate/content.go
@@ -29,7 +29,7 @@ const (
// indirect returns the value, after dereferencing as many times
// as necessary to reach the base type (or nil).
-func indirect(a any) any {
+func doIndirect(a any) any {
if a == nil {
return nil
}
@@ -45,8 +45,8 @@ func indirect(a any) any {
}
var (
- errorType = reflect.TypeOf((*error)(nil)).Elem()
- fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
+ errorType = reflect.TypeFor[error]()
+ fmtStringerType = reflect.TypeFor[fmt.Stringer]()
)
// indirectToStringerOrError returns the value, after dereferencing as many times
diff --git a/tpl/internal/go_templates/htmltemplate/doc.go b/tpl/internal/go_templates/htmltemplate/doc.go
index 6be5e0f84..7442194f7 100644
--- a/tpl/internal/go_templates/htmltemplate/doc.go
+++ b/tpl/internal/go_templates/htmltemplate/doc.go
@@ -232,11 +232,9 @@ Least Surprise Property:
knows that contextual autoescaping happens should be able to look at a {{.}}
and correctly infer what sanitization happens."
-As a consequence of the Least Surprise Property, template actions within an
-ECMAScript 6 template literal are disabled by default.
-Handling string interpolation within these literals is rather complex resulting
-in no clear safe way to support it.
-To re-enable template actions within ECMAScript 6 template literals, use the
-GODEBUG=jstmpllitinterp=1 environment variable.
+Previously, ECMAScript 6 template literal were disabled by default, and could be
+enabled with the GODEBUG=jstmpllitinterp=1 environment variable. Template
+literals are now supported by default, and setting jstmpllitinterp has no
+effect.
*/
package template
diff --git a/tpl/internal/go_templates/htmltemplate/escape.go b/tpl/internal/go_templates/htmltemplate/escape.go
index 334bbce0f..f5d5674fa 100644
--- a/tpl/internal/go_templates/htmltemplate/escape.go
+++ b/tpl/internal/go_templates/htmltemplate/escape.go
@@ -9,6 +9,7 @@ import (
"fmt"
"html"
"io"
+ "maps"
"regexp"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
@@ -145,7 +146,7 @@ func (e *escaper) escape(c context, n parse.Node) context {
return c
case *parse.ContinueNode:
c.n = n
- e.rangeContext.continues = append(e.rangeContext.breaks, c)
+ e.rangeContext.continues = append(e.rangeContext.continues, c)
return context{state: stateDead}
case *parse.IfNode:
return e.escapeBranch(c, &n.BranchNode, "if")
@@ -588,22 +589,14 @@ func (e *escaper) escapeListConditionally(c context, n *parse.ListNode, filter f
e1 := makeEscaper(e.ns)
e1.rangeContext = e.rangeContext
// Make type inferences available to f.
- for k, v := range e.output {
- e1.output[k] = v
- }
+ maps.Copy(e1.output, e.output)
c = e1.escapeList(c, n)
ok := filter != nil && filter(&e1, c)
if ok {
// Copy inferences and edits from e1 back into e.
- for k, v := range e1.output {
- e.output[k] = v
- }
- for k, v := range e1.derived {
- e.derived[k] = v
- }
- for k, v := range e1.called {
- e.called[k] = v
- }
+ maps.Copy(e.output, e1.output)
+ maps.Copy(e.derived, e1.derived)
+ maps.Copy(e.called, e1.called)
for k, v := range e1.actionNodeEdits {
e.editActionNode(k, v)
}
diff --git a/tpl/internal/go_templates/htmltemplate/escape_test.go b/tpl/internal/go_templates/htmltemplate/escape_test.go
index b202afde0..a5a810ffc 100644
--- a/tpl/internal/go_templates/htmltemplate/escape_test.go
+++ b/tpl/internal/go_templates/htmltemplate/escape_test.go
@@ -1065,6 +1065,10 @@ func TestErrors(t *testing.T) {
"{{range .Items}}{{end}}",
"z:1:29: at range loop continue: {{range}} branches end in different contexts",
},
+ {
+ "{{range .Items}}{{if .X}}{{break}}{{end}} {{if .Z}}{{continue}}{{end}}{{end}}",
+ "z:1:54: at range loop continue: {{range}} branches end in different contexts",
+ },
{
" - in text/template
@@ -381,21 +377,15 @@ var execTests = []execTest{
{".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: <nil>-", tVal, true},
{".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: <nil>-", tVal, true},
{"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true},
- {
- "method on chained var",
+ {"method on chained var",
"{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
- "true", tVal, true,
- },
- {
- "chained method",
+ "true", tVal, true},
+ {"chained method",
"{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
- "true", tVal, true,
- },
- {
- "chained method on variable",
+ "true", tVal, true},
+ {"chained method on variable",
"{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}",
- "true", tVal, true,
- },
+ "true", tVal, true},
{".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true},
{".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true},
{"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true},
@@ -481,14 +471,10 @@ var execTests = []execTest{
{"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true},
// HTML.
- {
- "html", `{{html ""}}`,
- "<script>alert("XSS");</script>", nil, true,
- },
- {
- "html pipeline", `{{printf "" | html}}`,
- "<script>alert("XSS");</script>", nil, true,
- },
+ {"html", `{{html ""}}`,
+ "<script>alert("XSS");</script>", nil, true},
+ {"html pipeline", `{{printf "" | html}}`,
+ "<script>alert("XSS");</script>", nil, true},
{"html", `{{html .PS}}`, "a string", tVal, true},
{"html typed nil", `{{html .NIL}}`, "<nil>", tVal, true},
{"html untyped nil", `{{html .Empty0}}`, "<nil>", tVal, true}, // NOTE: "<no value>" in text/template
@@ -580,6 +566,8 @@ var execTests = []execTest{
{"with $x struct.U.V", "{{with $x := $}}{{$x.U.V}}{{end}}", "v", tVal, true},
{"with variable and action", "{{with $x := $}}{{$y := $.U.V}}{{$y}}{{end}}", "v", tVal, true},
{"with on typed nil interface value", "{{with .NonEmptyInterfaceTypedNil}}TRUE{{ end }}", "", tVal, true},
+ {"with else with", "{{with 0}}{{.}}{{else with true}}{{.}}{{end}}", "true", tVal, true},
+ {"with else with chain", "{{with 0}}{{.}}{{else with false}}{{.}}{{else with `notempty`}}{{.}}{{end}}", "notempty", tVal, true},
// Range.
{"range []int", "{{range .SI}}-{{.}}-{{end}}", "-3--4--5-", tVal, true},
@@ -852,7 +840,7 @@ var delimPairs = []string{
func TestDelims(t *testing.T) {
const hello = "Hello, world"
- value := struct{ Str string }{hello}
+ var value = struct{ Str string }{hello}
for i := 0; i < len(delimPairs); i += 2 {
text := ".Str"
left := delimPairs[i+0]
@@ -875,7 +863,7 @@ func TestDelims(t *testing.T) {
if err != nil {
t.Fatalf("delim %q text %q parse err %s", left, text, err)
}
- b := new(strings.Builder)
+ var b = new(strings.Builder)
err = tmpl.Execute(b, value)
if err != nil {
t.Fatalf("delim %q exec err %s", left, err)
@@ -976,7 +964,7 @@ const treeTemplate = `
`
func TestTree(t *testing.T) {
- tree := &Tree{
+ var tree = &Tree{
1,
&Tree{
2, &Tree{
@@ -1227,7 +1215,7 @@ var cmpTests = []cmpTest{
func TestComparison(t *testing.T) {
b := new(strings.Builder)
- cmpStruct := struct {
+ var cmpStruct = struct {
Uthree, Ufour uint
NegOne, Three int
Ptr, NilPtr *int
diff --git a/tpl/internal/go_templates/htmltemplate/hugo_template.go b/tpl/internal/go_templates/htmltemplate/hugo_template.go
index 99edf8f68..d91baac70 100644
--- a/tpl/internal/go_templates/htmltemplate/hugo_template.go
+++ b/tpl/internal/go_templates/htmltemplate/hugo_template.go
@@ -14,6 +14,10 @@
package template
import (
+ "fmt"
+ "iter"
+
+ "github.com/gohugoio/hugo/common/types"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
)
@@ -35,7 +39,56 @@ func (t *Template) Prepare() (*template.Template, error) {
return t.text, nil
}
+func (t *Template) All() iter.Seq[*Template] {
+ return func(yield func(t *Template) bool) {
+ ns := t.nameSpace
+ ns.mu.Lock()
+ defer ns.mu.Unlock()
+ for _, v := range ns.set {
+ if !yield(v) {
+ return
+ }
+ }
+ }
+}
+
// See https://github.com/golang/go/issues/5884
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
+}
+
+// CloneShallow creates a shallow copy of the template. It does not clone or copy the nested templates.
+func (t *Template) CloneShallow() (*Template, error) {
+ t.nameSpace.mu.Lock()
+ defer t.nameSpace.mu.Unlock()
+ if t.escapeErr != nil {
+ return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
+ }
+ textClone, err := t.text.Clone()
+ if err != nil {
+ return nil, err
+ }
+ ns := &nameSpace{set: make(map[string]*Template)}
+ ns.esc = makeEscaper(ns)
+ ret := &Template{
+ nil,
+ textClone,
+ textClone.Tree,
+ ns,
+ }
+ ret.set[ret.Name()] = ret
+
+ // Return the template associated with the name of this template.
+ return ret.set[ret.Name()], nil
+}
diff --git a/tpl/internal/go_templates/htmltemplate/js.go b/tpl/internal/go_templates/htmltemplate/js.go
index cc80d2b64..b658aeabc 100644
--- a/tpl/internal/go_templates/htmltemplate/js.go
+++ b/tpl/internal/go_templates/htmltemplate/js.go
@@ -10,6 +10,7 @@ import (
"fmt"
htmltemplate "html/template"
"reflect"
+ "regexp"
"strings"
"unicode/utf8"
)
@@ -125,7 +126,7 @@ var regexpPrecederKeywords = map[string]bool{
"void": true,
}
-var jsonMarshalType = reflect.TypeOf((*json.Marshaler)(nil)).Elem()
+var jsonMarshalType = reflect.TypeFor[json.Marshaler]()
// indirectToJSONMarshaler returns the value, after dereferencing as many times
// as necessary to reach the base type (or nil) or an implementation of json.Marshal.
@@ -145,6 +146,8 @@ func indirectToJSONMarshaler(a any) any {
return v.Interface()
}
+var scriptTagRe = regexp.MustCompile("(?i)<(/?)script")
+
// jsValEscaper escapes its inputs to a JS Expression (section 11.14) that has
// neither side-effects nor free variables outside (NaN, Infinity).
func jsValEscaper(args ...any) string {
@@ -172,7 +175,7 @@ func jsValEscaper(args ...any) string {
// cyclic data. This may be an unacceptable DoS risk.
b, err := json.Marshal(a)
if err != nil {
- // While the standard JSON marshaller does not include user controlled
+ // While the standard JSON marshaler does not include user controlled
// information in the error message, if a type has a MarshalJSON method,
// the content of the error message is not guaranteed. Since we insert
// the error into the template, as part of a comment, we attempt to
@@ -182,9 +185,9 @@ func jsValEscaper(args ...any) string {
// In particular we:
// * replace "*/" comment end tokens with "* /", which does not
// terminate the comment
- // * replace "': `\u003e`,
}
-
var jsRegexpReplacementTable = []string{
0: `\u0000`,
'\t': `\t`,
diff --git a/tpl/internal/go_templates/htmltemplate/js_test.go b/tpl/internal/go_templates/htmltemplate/js_test.go
index c78f43b5f..b24fd69fd 100644
--- a/tpl/internal/go_templates/htmltemplate/js_test.go
+++ b/tpl/internal/go_templates/htmltemplate/js_test.go
@@ -110,7 +110,7 @@ func TestNextJsCtx(t *testing.T) {
type jsonErrType struct{}
func (e *jsonErrType) MarshalJSON() ([]byte, error) {
- return nil, errors.New("beep */ boop 0 && i+7 <= len(s) && bytes.Compare(bytes.ToLower(s[i-1:i+7]), []byte(" 0 && i+7 <= len(s) && bytes.Equal(bytes.ToLower(s[i-1:i+7]), []byte(" error
// MustHaveExecPath checks that the current system can start the named executable
@@ -93,6 +112,7 @@ func MustHaveExecPath(t testing.TB, path string) {
err, _ = execPaths.LoadOrStore(path, err)
}
if err != nil {
+ t.Helper()
t.Skipf("skipping test: %s: %s", path, err)
}
}
diff --git a/tpl/internal/go_templates/testenv/testenv.go b/tpl/internal/go_templates/testenv/testenv.go
index 97d897a1f..a46c8f5a8 100644
--- a/tpl/internal/go_templates/testenv/testenv.go
+++ b/tpl/internal/go_templates/testenv/testenv.go
@@ -12,7 +12,6 @@ package testenv
import (
"bytes"
- "errors"
"flag"
"fmt"
"os"
@@ -43,15 +42,22 @@ func Builder() string {
// HasGoBuild reports whether the current system can build programs with “go build”
// and then run them with os.StartProcess or exec.Command.
-// Modified by Hugo (not needed)
func HasGoBuild() bool {
- return false
+ if os.Getenv("GO_GCFLAGS") != "" {
+ // It's too much work to require every caller of the go command
+ // to pass along "-gcflags="+os.Getenv("GO_GCFLAGS").
+ // For now, if $GO_GCFLAGS is set, report that we simply can't
+ // run go build.
+ return false
+ }
+
+ return tryGoBuild() == nil
}
-var (
- goBuildOnce sync.Once
- goBuildErr error
-)
+var tryGoBuild = sync.OnceValue(func() error {
+ // Removed by Hugo, not used.
+ return nil
+})
// MustHaveGoBuild checks that the current system can build programs with “go build”
// and then run them with os.StartProcess or exec.Command.
@@ -63,7 +69,7 @@ func MustHaveGoBuild(t testing.TB) {
}
if !HasGoBuild() {
t.Helper()
- t.Skipf("skipping test: 'go build' unavailable: %v", goBuildErr)
+ t.Skipf("skipping test: 'go build' unavailable: %v", tryGoBuild())
}
}
@@ -77,6 +83,7 @@ func HasGoRun() bool {
// If not, MustHaveGoRun calls t.Skip with an explanation.
func MustHaveGoRun(t testing.TB) {
if !HasGoRun() {
+ t.Helper()
t.Skipf("skipping test: 'go run' not available on %s/%s", runtime.GOOS, runtime.GOARCH)
}
}
@@ -96,6 +103,7 @@ func HasParallelism() bool {
// threads in parallel. If not, MustHaveParallelism calls t.Skip with an explanation.
func MustHaveParallelism(t testing.TB) {
if !HasParallelism() {
+ t.Helper()
t.Skipf("skipping test: no parallelism available on %s/%s", runtime.GOOS, runtime.GOARCH)
}
}
@@ -119,84 +127,67 @@ func GoToolPath(t testing.TB) string {
return path
}
-var (
- gorootOnce sync.Once
- gorootPath string
- gorootErr error
-)
+var findGOROOT = sync.OnceValues(func() (path string, err error) {
+ if path := runtime.GOROOT(); path != "" {
+ // If runtime.GOROOT() is non-empty, assume that it is valid.
+ //
+ // (It might not be: for example, the user may have explicitly set GOROOT
+ // to the wrong directory. But this case is
+ // rare, and if that happens the user can fix what they broke.)
+ return path, nil
+ }
-func findGOROOT() (string, error) {
- gorootOnce.Do(func() {
- gorootPath = runtime.GOROOT()
- if gorootPath != "" {
- // If runtime.GOROOT() is non-empty, assume that it is valid.
- //
- // (It might not be: for example, the user may have explicitly set GOROOT
- // to the wrong directory, or explicitly set GOROOT_FINAL but not GOROOT
- // and hasn't moved the tree to GOROOT_FINAL yet. But those cases are
- // rare, and if that happens the user can fix what they broke.)
- return
+ // runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
+ // binary was built with -trimpath).
+ //
+ // Since this is internal/testenv, we can cheat and assume that the caller
+ // is a test of some package in a subdirectory of GOROOT/src. ('go test'
+ // runs the test in the directory containing the packaged under test.) That
+ // means that if we start walking up the tree, we should eventually find
+ // GOROOT/src/go.mod, and we can report the parent directory of that.
+ //
+ // Notably, this works even if we can't run 'go env GOROOT' as a
+ // subprocess.
+
+ cwd, err := os.Getwd()
+ if err != nil {
+ return "", fmt.Errorf("finding GOROOT: %w", err)
+ }
+
+ dir := cwd
+ for {
+ parent := filepath.Dir(dir)
+ if parent == dir {
+ // dir is either "." or only a volume name.
+ return "", fmt.Errorf("failed to locate GOROOT/src in any parent directory")
}
- // runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
- // binary was built with -trimpath, or perhaps because GOROOT_FINAL was set
- // without GOROOT and the tree hasn't been moved there yet).
- //
- // Since this is internal/testenv, we can cheat and assume that the caller
- // is a test of some package in a subdirectory of GOROOT/src. ('go test'
- // runs the test in the directory containing the packaged under test.) That
- // means that if we start walking up the tree, we should eventually find
- // GOROOT/src/go.mod, and we can report the parent directory of that.
- //
- // Notably, this works even if we can't run 'go env GOROOT' as a
- // subprocess.
+ if base := filepath.Base(dir); base != "src" {
+ dir = parent
+ continue // dir cannot be GOROOT/src if it doesn't end in "src".
+ }
- cwd, err := os.Getwd()
+ b, err := os.ReadFile(filepath.Join(dir, "go.mod"))
if err != nil {
- gorootErr = fmt.Errorf("finding GOROOT: %w", err)
- return
- }
-
- dir := cwd
- for {
- parent := filepath.Dir(dir)
- if parent == dir {
- // dir is either "." or only a volume name.
- gorootErr = fmt.Errorf("failed to locate GOROOT/src in any parent directory")
- return
- }
-
- if base := filepath.Base(dir); base != "src" {
+ if os.IsNotExist(err) {
dir = parent
- continue // dir cannot be GOROOT/src if it doesn't end in "src".
+ continue
}
+ return "", fmt.Errorf("finding GOROOT: %w", err)
+ }
+ goMod := string(b)
- b, err := os.ReadFile(filepath.Join(dir, "go.mod"))
- if err != nil {
- if os.IsNotExist(err) {
- dir = parent
- continue
- }
- gorootErr = fmt.Errorf("finding GOROOT: %w", err)
- return
- }
- goMod := string(b)
-
- for goMod != "" {
- var line string
- line, goMod, _ = strings.Cut(goMod, "\n")
- fields := strings.Fields(line)
- if len(fields) >= 2 && fields[0] == "module" && fields[1] == "std" {
- // Found "module std", which is the module declaration in GOROOT/src!
- gorootPath = parent
- return
- }
+ for goMod != "" {
+ var line string
+ line, goMod, _ = strings.Cut(goMod, "\n")
+ fields := strings.Fields(line)
+ if len(fields) >= 2 && fields[0] == "module" && fields[1] == "std" {
+ // Found "module std", which is the module declaration in GOROOT/src!
+ return parent, nil
}
}
- })
-
- return gorootPath, gorootErr
-}
+ }
+})
// GOROOT reports the path to the directory containing the root of the Go
// project source tree. This is normally equivalent to runtime.GOROOT, but
@@ -219,28 +210,22 @@ func GOROOT(t testing.TB) string {
// GoTool reports the path to the Go tool.
func GoTool() (string, error) {
- if !HasGoBuild() {
- return "", errors.New("platform cannot run go tool")
- }
- goToolOnce.Do(func() {
- goToolPath, goToolErr = exec.LookPath("go")
- })
- return goToolPath, goToolErr
+ // Removed by Hugo, not used.
+ return "", nil
}
-var (
- goToolOnce sync.Once
- goToolPath string
- goToolErr error
-)
+var goTool = sync.OnceValues(func() (string, error) {
+ return exec.LookPath("go")
+})
-// HasSrc reports whether the entire source tree is available under GOROOT.
-func HasSrc() bool {
+// MustHaveSource checks that the entire source tree is available under GOROOT.
+// If not, it calls t.Skip with an explanation.
+func MustHaveSource(t testing.TB) {
switch runtime.GOOS {
case "ios":
- return false
+ t.Helper()
+ t.Skip("skipping test: no source tree on " + runtime.GOOS)
}
- return true
}
// HasExternalNetwork reports whether the current system can use
@@ -265,41 +250,39 @@ func MustHaveExternalNetwork(t testing.TB) {
// HasCGO reports whether the current system can use cgo.
func HasCGO() bool {
- hasCgoOnce.Do(func() {
- goTool, err := GoTool()
- if err != nil {
- return
- }
- cmd := exec.Command(goTool, "env", "CGO_ENABLED")
- cmd.Env = origEnv
- out, err := cmd.Output()
- if err != nil {
- panic(fmt.Sprintf("%v: %v", cmd, out))
- }
- hasCgo, err = strconv.ParseBool(string(bytes.TrimSpace(out)))
- if err != nil {
- panic(fmt.Sprintf("%v: non-boolean output %q", cmd, out))
- }
- })
- return hasCgo
+ return hasCgo()
}
-var (
- hasCgoOnce sync.Once
- hasCgo bool
-)
+var hasCgo = sync.OnceValue(func() bool {
+ goTool, err := goTool()
+ if err != nil {
+ return false
+ }
+ cmd := exec.Command(goTool, "env", "CGO_ENABLED")
+ cmd.Env = origEnv
+ out, err := cmd.Output()
+ if err != nil {
+ panic(fmt.Sprintf("%v: %v", cmd, out))
+ }
+ ok, err := strconv.ParseBool(string(bytes.TrimSpace(out)))
+ if err != nil {
+ panic(fmt.Sprintf("%v: non-boolean output %q", cmd, out))
+ }
+ return ok
+})
// MustHaveCGO calls t.Skip if cgo is not available.
func MustHaveCGO(t testing.TB) {
if !HasCGO() {
+ t.Helper()
t.Skipf("skipping test: no cgo")
}
}
// CanInternalLink reports whether the current system can link programs with
// internal linking.
-// Modified by Hugo (not needed)
func CanInternalLink(withCgo bool) bool {
+ // Removed by Hugo, not used.
return false
}
@@ -308,6 +291,7 @@ func CanInternalLink(withCgo bool) bool {
// If not, MustInternalLink calls t.Skip with an explanation.
func MustInternalLink(t testing.TB, withCgo bool) {
if !CanInternalLink(withCgo) {
+ t.Helper()
if withCgo && CanInternalLink(false) {
t.Skipf("skipping test: internal linking on %s/%s is not supported with cgo", runtime.GOOS, runtime.GOARCH)
}
@@ -315,11 +299,18 @@ func MustInternalLink(t testing.TB, withCgo bool) {
}
}
+// MustInternalLinkPIE checks whether the current system can link PIE binary using
+// internal linking.
+// If not, MustInternalLinkPIE calls t.Skip with an explanation.
+func MustInternalLinkPIE(t testing.TB) {
+ // Removed by Hugo, not used.
+}
+
// MustHaveBuildMode reports whether the current system can build programs in
// the given build mode.
// If not, MustHaveBuildMode calls t.Skip with an explanation.
-// Modified by Hugo (not needed)
func MustHaveBuildMode(t testing.TB, buildmode string) {
+ // Removed by Hugo, not used.
}
// HasSymlink reports whether the current system can use os.Symlink.
@@ -333,6 +324,7 @@ func HasSymlink() bool {
func MustHaveSymlink(t testing.TB) {
ok, reason := hasSymlink()
if !ok {
+ t.Helper()
t.Skipf("skipping test: cannot make symlinks on %s/%s: %s", runtime.GOOS, runtime.GOARCH, reason)
}
}
@@ -349,6 +341,7 @@ func HasLink() bool {
// If not, MustHaveLink calls t.Skip with an explanation.
func MustHaveLink(t testing.TB) {
if !HasLink() {
+ t.Helper()
t.Skipf("skipping test: hardlinks are not supported on %s/%s", runtime.GOOS, runtime.GOARCH)
}
}
@@ -356,15 +349,15 @@ func MustHaveLink(t testing.TB) {
var flaky = flag.Bool("flaky", false, "run known-flaky tests too")
func SkipFlaky(t testing.TB, issue int) {
- t.Helper()
if !*flaky {
+ t.Helper()
t.Skipf("skipping known flaky test without the -flaky flag; see golang.org/issue/%d", issue)
}
}
func SkipFlakyNet(t testing.TB) {
- t.Helper()
if v, _ := strconv.ParseBool(os.Getenv("GO_BUILDER_FLAKY_NET")); v {
+ t.Helper()
t.Skip("skipping test on builder known to have frequent network failures")
}
}
@@ -446,3 +439,10 @@ func WriteImportcfg(t testing.TB, dstPath string, packageFiles map[string]string
func SyscallIsNotSupported(err error) bool {
return syscallIsNotSupported(err)
}
+
+// ParallelOn64Bit calls t.Parallel() unless there is a case that cannot be parallel.
+// This function should be used when it is necessary to avoid t.Parallel on
+// 32-bit machines, typically because the test uses lots of memory.
+func ParallelOn64Bit(t *testing.T) {
+ // Removed by Hugo, not used.
+}
diff --git a/tpl/internal/go_templates/testenv/testenv_notwin.go b/tpl/internal/go_templates/testenv/testenv_notwin.go
index 30e159a6e..9dddea94d 100644
--- a/tpl/internal/go_templates/testenv/testenv_notwin.go
+++ b/tpl/internal/go_templates/testenv/testenv_notwin.go
@@ -11,9 +11,10 @@ import (
"os"
"path/filepath"
"runtime"
+ "sync"
)
-func hasSymlink() (ok bool, reason string) {
+var hasSymlink = sync.OnceValues(func() (ok bool, reason string) {
switch runtime.GOOS {
case "plan9":
return false, ""
@@ -43,4 +44,4 @@ func hasSymlink() (ok bool, reason string) {
}
return true, ""
-}
+})
diff --git a/tpl/internal/go_templates/testenv/testenv_test.go b/tpl/internal/go_templates/testenv/testenv_test.go
index d4b2b368f..afe159531 100644
--- a/tpl/internal/go_templates/testenv/testenv_test.go
+++ b/tpl/internal/go_templates/testenv/testenv_test.go
@@ -15,6 +15,7 @@ import (
)
func TestGoToolLocation(t *testing.T) {
+ t.Skip("This test is not relevant for Hugo")
testenv.MustHaveGoBuild(t)
var exeSuffix string
@@ -54,8 +55,83 @@ func TestGoToolLocation(t *testing.T) {
}
}
-// Modified by Hugo.
func TestHasGoBuild(t *testing.T) {
+ if !testenv.HasGoBuild() {
+ switch runtime.GOOS {
+ case "js", "wasip1":
+ // No exec syscall, so these shouldn't be able to 'go build'.
+ t.Logf("HasGoBuild is false on %s", runtime.GOOS)
+ return
+ }
+
+ b := testenv.Builder()
+ if b == "" {
+ // We shouldn't make assumptions about what kind of sandbox or build
+ // environment external Go users may be running in.
+ t.Skipf("skipping: 'go build' unavailable")
+ }
+
+ // Since we control the Go builders, we know which ones ought
+ // to be able to run 'go build'. Check that they can.
+ //
+ // (Note that we don't verify that any builders *can't* run 'go build'.
+ // If a builder starts running 'go build' tests when it shouldn't,
+ // we will presumably find out about it when those tests fail.)
+ switch runtime.GOOS {
+ case "ios":
+ if isCorelliumBuilder(b) {
+ // The corellium environment is self-hosting, so it should be able
+ // to build even though real "ios" devices can't exec.
+ } else {
+ // The usual iOS sandbox does not allow the app to start another
+ // process. If we add builders on stock iOS devices, they presumably
+ // will not be able to exec, so we may as well allow that now.
+ t.Logf("HasGoBuild is false on %s", b)
+ return
+ }
+ case "android":
+ panic("Removed by Hugo, should not be used")
+ }
+
+ if strings.Contains(b, "-noopt") {
+ // The -noopt builder sets GO_GCFLAGS, which causes tests of 'go build' to
+ // be skipped.
+ t.Logf("HasGoBuild is false on %s", b)
+ return
+ }
+
+ t.Fatalf("HasGoBuild unexpectedly false on %s", b)
+ }
+
+ t.Logf("HasGoBuild is true; checking consistency with other functions")
+
+ hasExec := false
+ hasExecGo := false
+ t.Run("MustHaveExec", func(t *testing.T) {
+ testenv.MustHaveExec(t)
+ hasExec = true
+ })
+ t.Run("MustHaveExecPath", func(t *testing.T) {
+ testenv.MustHaveExecPath(t, "go")
+ hasExecGo = true
+ })
+ if !hasExec {
+ t.Errorf(`MustHaveExec(t) skipped unexpectedly`)
+ }
+ if !hasExecGo {
+ t.Errorf(`MustHaveExecPath(t, "go") skipped unexpectedly`)
+ }
+
+ dir := t.TempDir()
+ mainGo := filepath.Join(dir, "main.go")
+ if err := os.WriteFile(mainGo, []byte("package main\nfunc main() {}\n"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ cmd := testenv.Command(t, "go", "build", "-o", os.DevNull, mainGo)
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("%v: %v\n%s", cmd, err, out)
+ }
}
func TestMustHaveExec(t *testing.T) {
diff --git a/tpl/internal/go_templates/testenv/testenv_windows.go b/tpl/internal/go_templates/testenv/testenv_windows.go
index 4802b1395..eed53cdfb 100644
--- a/tpl/internal/go_templates/testenv/testenv_windows.go
+++ b/tpl/internal/go_templates/testenv/testenv_windows.go
@@ -5,16 +5,14 @@
package testenv
import (
+ "errors"
"os"
"path/filepath"
"sync"
"syscall"
)
-var symlinkOnce sync.Once
-var winSymlinkErr error
-
-func initWinHasSymlink() {
+var hasSymlink = sync.OnceValues(func() (bool, string) {
tmpdir, err := os.MkdirTemp("", "symtest")
if err != nil {
panic("failed to create temp directory: " + err.Error())
@@ -22,26 +20,13 @@ func initWinHasSymlink() {
defer os.RemoveAll(tmpdir)
err = os.Symlink("target", filepath.Join(tmpdir, "symlink"))
- if err != nil {
- err = err.(*os.LinkError).Err
- switch err {
- case syscall.EWINDOWS, syscall.ERROR_PRIVILEGE_NOT_HELD:
- winSymlinkErr = err
- }
- }
-}
-
-func hasSymlink() (ok bool, reason string) {
- symlinkOnce.Do(initWinHasSymlink)
-
- switch winSymlinkErr {
- case nil:
+ switch {
+ case err == nil:
return true, ""
- case syscall.EWINDOWS:
+ case errors.Is(err, syscall.EWINDOWS):
return false, ": symlinks are not supported on your version of Windows"
- case syscall.ERROR_PRIVILEGE_NOT_HELD:
+ case errors.Is(err, syscall.ERROR_PRIVILEGE_NOT_HELD):
return false, ": you don't have enough privileges to create symlinks"
}
-
return false, ""
-}
+})
diff --git a/tpl/internal/go_templates/texttemplate/doc.go b/tpl/internal/go_templates/texttemplate/doc.go
index 032784bc3..7b63bb76a 100644
--- a/tpl/internal/go_templates/texttemplate/doc.go
+++ b/tpl/internal/go_templates/texttemplate/doc.go
@@ -98,7 +98,8 @@ data, defined in detail in the corresponding sections that follow.
{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
{{range pipeline}} T1 {{end}}
- The value of the pipeline must be an array, slice, map, or channel.
+ The value of the pipeline must be an array, slice, map, iter.Seq,
+ iter.Seq2, integer or channel.
If the value of the pipeline has length zero, nothing is output;
otherwise, dot is set to the successive elements of the array,
slice, or map and T1 is executed. If the value is a map and the
@@ -106,7 +107,8 @@ data, defined in detail in the corresponding sections that follow.
visited in sorted key order.
{{range pipeline}} T1 {{else}} T0 {{end}}
- The value of the pipeline must be an array, slice, map, or channel.
+ The value of the pipeline must be an array, slice, map, iter.Seq,
+ iter.Seq2, integer or channel.
If the value of the pipeline has length zero, dot is unaffected and
T0 is executed; otherwise, dot is set to the successive elements
of the array, slice, or map and T1 is executed.
@@ -144,6 +146,13 @@ data, defined in detail in the corresponding sections that follow.
is executed; otherwise, dot is set to the value of the pipeline
and T1 is executed.
+ {{with pipeline}} T1 {{else with pipeline}} T0 {{end}}
+ To simplify the appearance of with-else chains, the else action
+ of a with may include another with directly; the effect is exactly
+ the same as writing
+ {{with pipeline}} T1 {{else}}{{with pipeline}} T0 {{end}}{{end}}
+
+
Arguments
An argument is a simple value, denoted by one of the following.
@@ -155,37 +164,55 @@ An argument is a simple value, denoted by one of the following.
the host machine's ints are 32 or 64 bits.
- The keyword nil, representing an untyped Go nil.
- The character '.' (period):
+
.
+
The result is the value of dot.
- A variable name, which is a (possibly empty) alphanumeric string
preceded by a dollar sign, such as
+
$piOver2
+
or
+
$
+
The result is the value of the variable.
Variables are described below.
- The name of a field of the data, which must be a struct, preceded
by a period, such as
+
.Field
+
The result is the value of the field. Field invocations may be
chained:
+
.Field1.Field2
+
Fields can also be evaluated on variables, including chaining:
+
$x.Field1.Field2
- The name of a key of the data, which must be a map, preceded
by a period, such as
+
.Key
+
The result is the map element value indexed by the key.
Key invocations may be chained and combined with fields to any
depth:
+
.Field1.Key1.Field2.Key2
+
Although the key must be an alphanumeric identifier, unlike with
field names they do not need to start with an upper case letter.
Keys can also be evaluated on variables, including chaining:
+
$x.key1.key2
- The name of a niladic method of the data, preceded by a period,
such as
+
.Method
+
The result is the value of invoking the method with dot as the
receiver, dot.Method(). Such a method must have one return value (of
any type) or two return values, the second of which is an error.
@@ -193,16 +220,22 @@ An argument is a simple value, denoted by one of the following.
and an error is returned to the caller as the value of Execute.
Method invocations may be chained and combined with fields and keys
to any depth:
+
.Field1.Key1.Method1.Field2.Key2.Method2
+
Methods can also be evaluated on variables, including chaining:
+
$x.Method1.Field
- The name of a niladic function, such as
+
fun
+
The result is the value of invoking the function, fun(). The return
types and values behave as in methods. Functions and function
names are described below.
- A parenthesized instance of one the above, for grouping. The result
may be accessed by a field or map key invocation.
+
print (.F1 arg1) (.F2 arg2)
(.StructValuedMethod "arg").Field
diff --git a/tpl/internal/go_templates/texttemplate/example_test.go b/tpl/internal/go_templates/texttemplate/example_test.go
index 295a810b8..975ceea93 100644
--- a/tpl/internal/go_templates/texttemplate/example_test.go
+++ b/tpl/internal/go_templates/texttemplate/example_test.go
@@ -35,7 +35,7 @@ Josie
Name, Gift string
Attended bool
}
- var recipients = []Recipient{
+ recipients := []Recipient{
{"Aunt Mildred", "bone china tea set", true},
{"Uncle John", "moleskin pants", false},
{"Cousin Rodney", "", false},
diff --git a/tpl/internal/go_templates/texttemplate/exec.go b/tpl/internal/go_templates/texttemplate/exec.go
index 73153c764..f710a25ec 100644
--- a/tpl/internal/go_templates/texttemplate/exec.go
+++ b/tpl/internal/go_templates/texttemplate/exec.go
@@ -7,13 +7,12 @@ package template
import (
"errors"
"fmt"
+ "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
+ "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
"io"
"reflect"
"runtime"
"strings"
-
- "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
- "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
)
// maxExecDepth specifies the maximum stack depth of templates within
@@ -95,7 +94,7 @@ type missingValType struct{}
var missingVal = reflect.ValueOf(missingValType{})
-var missingValReflectType = reflect.TypeOf(missingValType{})
+var missingValReflectType = reflect.TypeFor[missingValType]()
func isMissing(v reflect.Value) bool {
return v.IsValid() && v.Type() == missingValReflectType
@@ -202,8 +201,8 @@ func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error {
// A template may be executed safely in parallel, although if parallel
// executions share a Writer the output may be interleaved.
//
-// If data is a reflect.Value, the template applies to the concrete
-// value that the reflect.Value holds, as in fmt.Print.
+// If data is a [reflect.Value], the template applies to the concrete
+// value that the reflect.Value holds, as in [fmt.Print].
func (t *Template) Execute(wr io.Writer, data any) error {
return t.execute(wr, data)
}
@@ -229,7 +228,7 @@ func (t *Template) execute(wr io.Writer, data any) (err error) {
// DefinedTemplates returns a string listing the defined templates,
// prefixed by the string "; defined templates are: ". If there are none,
// it returns the empty string. For generating an error message here
-// and in html/template.
+// and in [html/template].
func (t *Template) DefinedTemplates() string {
if t.common == nil {
return ""
@@ -396,6 +395,22 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
s.walk(elem, r.List)
}
switch val.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
+ reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ if len(r.Pipe.Decl) > 1 {
+ s.errorf("can't use %v to iterate over more than one variable", val)
+ break
+ }
+ run := false
+ for v := range val.Seq() {
+ run = true
+ // Pass element as second value, as we do for channels.
+ oneIteration(reflect.Value{}, v)
+ }
+ if !run {
+ break
+ }
+ return
case reflect.Array, reflect.Slice:
if val.Len() == 0 {
break
@@ -409,8 +424,8 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
break
}
om := fmtsort.Sort(val)
- for i, key := range om.Key {
- oneIteration(key, om.Value[i])
+ for _, m := range om {
+ oneIteration(m.Key, m.Value)
}
return
case reflect.Chan:
@@ -435,6 +450,43 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
return
case reflect.Invalid:
break // An invalid value is likely a nil map, etc. and acts like an empty map.
+ case reflect.Func:
+ if val.Type().CanSeq() {
+ if len(r.Pipe.Decl) > 1 {
+ s.errorf("can't use %v iterate over more than one variable", val)
+ break
+ }
+ run := false
+ for v := range val.Seq() {
+ run = true
+ // Pass element as second value,
+ // as we do for channels.
+ oneIteration(reflect.Value{}, v)
+ }
+ if !run {
+ break
+ }
+ return
+ }
+ if val.Type().CanSeq2() {
+ run := false
+ for i, v := range val.Seq2() {
+ run = true
+ if len(r.Pipe.Decl) > 1 {
+ oneIteration(i, v)
+ } else {
+ // If there is only one range variable,
+ // oneIteration will use the
+ // second value.
+ oneIteration(reflect.Value{}, i)
+ }
+ }
+ if !run {
+ break
+ }
+ return
+ }
+ fallthrough
default:
s.errorf("range can't iterate over %v", val)
}
@@ -480,7 +532,7 @@ func (s *state) evalPipeline(dot reflect.Value, pipe *parse.PipeNode) (value ref
value = s.evalCommand(dot, cmd, value) // previous value is this one's final arg.
// If the object has type interface{}, dig down one level to the thing inside.
if value.Kind() == reflect.Interface && value.Type().NumMethod() == 0 {
- value = reflect.ValueOf(value.Interface()) // lovely!
+ value = value.Elem()
}
}
for _, variable := range pipe.Decl {
@@ -709,9 +761,9 @@ func (s *state) evalFieldOld(dot reflect.Value, fieldName string, node parse.Nod
}
var (
- errorType = reflect.TypeOf((*error)(nil)).Elem()
- fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
- reflectValueType = reflect.TypeOf((*reflect.Value)(nil)).Elem()
+ errorType = reflect.TypeFor[error]()
+ fmtStringerType = reflect.TypeFor[fmt.Stringer]()
+ reflectValueType = reflect.TypeFor[reflect.Value]()
)
// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
@@ -735,9 +787,8 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
} else if numIn != typ.NumIn() {
s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
}
- if !goodFunc(typ) {
- // TODO: This could still be a confusing error; maybe goodFunc should provide info.
- s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
+ if err := goodFunc(name, typ); err != nil {
+ s.errorf("%v", err)
}
unwrap := func(v reflect.Value) reflect.Value {
@@ -759,7 +810,7 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
return v
}
}
- if final != missingVal {
+ if !final.Equal(missingVal) {
// The last argument to and/or is coming from
// the pipeline. We didn't short circuit on an earlier
// argument, so we are going to return this one.
@@ -801,6 +852,21 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
}
argv[i] = s.validateType(final, t)
}
+
+ // Special case for the "call" builtin.
+ // Insert the name of the callee function as the first argument.
+ if isBuiltin && name == "call" {
+ var calleeName string
+ if len(args) == 0 {
+ // final must be present or we would have errored out above.
+ calleeName = final.String()
+ } else {
+ calleeName = args[0].String()
+ }
+ argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
+ fun = reflect.ValueOf(call)
+ }
+
v, err := safeCall(fun, argv)
// If we have an error that is not nil, stop execution and return that
// error to the caller.
diff --git a/tpl/internal/go_templates/texttemplate/exec_test.go b/tpl/internal/go_templates/texttemplate/exec_test.go
index 030e20ca0..742e85605 100644
--- a/tpl/internal/go_templates/texttemplate/exec_test.go
+++ b/tpl/internal/go_templates/texttemplate/exec_test.go
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:build go1.13 && !windows
-// +build go1.13,!windows
+//go:build !windows
+// +build !windows
package template
@@ -13,6 +13,7 @@ import (
"flag"
"fmt"
"io"
+ "iter"
"reflect"
"strings"
"sync"
@@ -78,12 +79,15 @@ type T struct {
PSI *[]int
NIL *int
// Function (not method)
- BinaryFunc func(string, string) string
- VariadicFunc func(...string) string
- VariadicFuncInt func(int, ...string) string
- NilOKFunc func(*int) bool
- ErrFunc func() (string, error)
- PanicFunc func() string
+ BinaryFunc func(string, string) string
+ VariadicFunc func(...string) string
+ VariadicFuncInt func(int, ...string) string
+ NilOKFunc func(*int) bool
+ ErrFunc func() (string, error)
+ PanicFunc func() string
+ TooFewReturnCountFunc func()
+ TooManyReturnCountFunc func() (string, error, int)
+ InvalidReturnTypeFunc func() (string, bool)
// Template to test evaluation of templates.
Tmpl *Template
// Unexported field; cannot be accessed by template.
@@ -171,6 +175,9 @@ var tVal = &T{
NilOKFunc: func(s *int) bool { return s == nil },
ErrFunc: func() (string, error) { return "bla", nil },
PanicFunc: func() string { panic("test panic") },
+ TooFewReturnCountFunc: func() {},
+ TooManyReturnCountFunc: func() (string, error, int) { return "", nil, 0 },
+ InvalidReturnTypeFunc: func() (string, bool) { return "", false },
Tmpl: Must(New("x").Parse("test template")), // "x" is the value of .X
}
@@ -268,8 +275,8 @@ type execTest struct {
// of the max int boundary.
// We do it this way so the test doesn't depend on ints being 32 bits.
var (
- bigInt = fmt.Sprintf("0x%x", int(1<{{end}}", "<21><22><23>", tVal, true},
{"range count", `{{range $i, $x := count 5}}[{{$i}}]{{$x}}{{end}}`, "[0]a[1]b[2]c[3]d[4]e", tVal, true},
{"range nil count", `{{range $i, $x := count 0}}{{else}}empty{{end}}`, "empty", tVal, true},
+ {"range iter.Seq[int]", `{{range $i := .}}{{$i}}{{end}}`, "01", fVal1(2), true},
+ {"i = range iter.Seq[int]", `{{$i := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal1(2), true},
+ {"range iter.Seq[int] over two var", `{{range $i, $c := .}}{{$c}}{{end}}`, "", fVal1(2), false},
+ {"i, c := range iter.Seq2[int,int]", `{{range $i, $c := .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
+ {"i, c = range iter.Seq2[int,int]", `{{$i := 0}}{{$c := 0}}{{range $i, $c = .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
+ {"i = range iter.Seq2[int,int]", `{{$i := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal2(2), true},
+ {"i := range iter.Seq2[int,int]", `{{range $i := .}}{{$i}}{{end}}`, "01", fVal2(2), true},
+ {"i,c,x range iter.Seq2[int,int]", `{{$i := 0}}{{$c := 0}}{{$x := 0}}{{range $i, $c = .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
+ {"i,x range iter.Seq[int]", `{{$i := 0}}{{$x := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal1(2), true},
+ {"range iter.Seq[int] else", `{{range $i := .}}{{$i}}{{else}}empty{{end}}`, "empty", fVal1(0), true},
+ {"range iter.Seq2[int,int] else", `{{range $i := .}}{{$i}}{{else}}empty{{end}}`, "empty", fVal2(0), true},
+ {"range int8", rangeTestInt, rangeTestData[int8](), int8(5), true},
+ {"range int16", rangeTestInt, rangeTestData[int16](), int16(5), true},
+ {"range int32", rangeTestInt, rangeTestData[int32](), int32(5), true},
+ {"range int64", rangeTestInt, rangeTestData[int64](), int64(5), true},
+ {"range int", rangeTestInt, rangeTestData[int](), int(5), true},
+ {"range uint8", rangeTestInt, rangeTestData[uint8](), uint8(5), true},
+ {"range uint16", rangeTestInt, rangeTestData[uint16](), uint16(5), true},
+ {"range uint32", rangeTestInt, rangeTestData[uint32](), uint32(5), true},
+ {"range uint64", rangeTestInt, rangeTestData[uint64](), uint64(5), true},
+ {"range uint", rangeTestInt, rangeTestData[uint](), uint(5), true},
+ {"range uintptr", rangeTestInt, rangeTestData[uintptr](), uintptr(5), true},
+ {"range uintptr(0)", `{{range $v := .}}{{print $v}}{{else}}empty{{end}}`, "empty", uintptr(0), true},
+ {"range 5", `{{range $v := 5}}{{printf "%T%d" $v $v}}{{end}}`, rangeTestData[int](), nil, true},
// Cute examples.
{"or as if true", `{{or .SI "slice is empty"}}`, "[3 4 5]", tVal, true},
@@ -714,6 +750,37 @@ var execTests = []execTest{
{"issue60801", "{{$k := 0}}{{$v := 0}}{{range $k, $v = .AI}}{{$k}}={{$v}} {{end}}", "0=3 1=4 2=5 ", tVal, true},
}
+func fVal1(i int) iter.Seq[int] {
+ return func(yield func(int) bool) {
+ for v := range i {
+ if !yield(v) {
+ break
+ }
+ }
+ }
+}
+
+func fVal2(i int) iter.Seq2[int, int] {
+ return func(yield func(int, int) bool) {
+ for v := range i {
+ if !yield(v, v+1) {
+ break
+ }
+ }
+ }
+}
+
+const rangeTestInt = `{{range $v := .}}{{printf "%T%d" $v $v}}{{end}}`
+
+func rangeTestData[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | uintptr]() string {
+ I := T(5)
+ var buf strings.Builder
+ for i := T(0); i < I; i++ {
+ fmt.Fprintf(&buf, "%T%d", i, i)
+ }
+ return buf.String()
+}
+
func zeroArgs() string {
return "zeroArgs"
}
@@ -1726,6 +1793,81 @@ func TestExecutePanicDuringCall(t *testing.T) {
}
}
+func TestFunctionCheckDuringCall(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ data any
+ wantErr string
+ }{
+ {
+ name: "call nothing",
+ input: `{{call}}`,
+ data: tVal,
+ wantErr: "wrong number of args for call: want at least 1 got 0",
+ },
+ {
+ name: "call non-function",
+ input: "{{call .True}}",
+ data: tVal,
+ wantErr: "error calling call: non-function .True of type bool",
+ },
+ {
+ name: "call func with wrong argument",
+ input: "{{call .BinaryFunc 1}}",
+ data: tVal,
+ wantErr: "error calling call: wrong number of args for .BinaryFunc: got 1 want 2",
+ },
+ {
+ name: "call variadic func with wrong argument",
+ input: `{{call .VariadicFuncInt}}`,
+ data: tVal,
+ wantErr: "error calling call: wrong number of args for .VariadicFuncInt: got 0 want at least 1",
+ },
+ {
+ name: "call too few return number func",
+ input: `{{call .TooFewReturnCountFunc}}`,
+ data: tVal,
+ wantErr: "error calling call: function .TooFewReturnCountFunc has 0 return values; should be 1 or 2",
+ },
+ {
+ name: "call too many return number func",
+ input: `{{call .TooManyReturnCountFunc}}`,
+ data: tVal,
+ wantErr: "error calling call: function .TooManyReturnCountFunc has 3 return values; should be 1 or 2",
+ },
+ {
+ name: "call invalid return type func",
+ input: `{{call .InvalidReturnTypeFunc}}`,
+ data: tVal,
+ wantErr: "error calling call: invalid function signature for .InvalidReturnTypeFunc: second return value should be error; is bool",
+ },
+ {
+ name: "call pipeline",
+ input: `{{call (len "test")}}`,
+ data: nil,
+ wantErr: "error calling call: non-function len \"test\" of type int",
+ },
+ }
+
+ for _, tc := range tests {
+ b := new(bytes.Buffer)
+ tmpl, err := New("t").Parse(tc.input)
+ if err != nil {
+ t.Fatalf("parse error: %s", err)
+ }
+ err = tmpl.Execute(b, tc.data)
+ if err == nil {
+ t.Errorf("%s: expected error; got none", tc.name)
+ } else if tc.wantErr == "" || !strings.Contains(err.Error(), tc.wantErr) {
+ if *debug {
+ fmt.Printf("%s: test execute error: %s\n", tc.name, err)
+ }
+ t.Errorf("%s: expected error:\n%s\ngot:\n%s", tc.name, tc.wantErr, err)
+ }
+ }
+}
+
// Issue 31810. Check that a parenthesized first argument behaves properly.
func TestIssue31810(t *testing.T) {
// A simple value with no arguments is fine.
diff --git a/tpl/internal/go_templates/texttemplate/funcs.go b/tpl/internal/go_templates/texttemplate/funcs.go
index a949f896f..7d63cf8b7 100644
--- a/tpl/internal/go_templates/texttemplate/funcs.go
+++ b/tpl/internal/go_templates/texttemplate/funcs.go
@@ -22,14 +22,14 @@ import (
// return value evaluates to non-nil during execution, execution terminates and
// Execute returns that error.
//
-// Errors returned by Execute wrap the underlying error; call errors.As to
+// Errors returned by Execute wrap the underlying error; call [errors.As] to
// unwrap them.
//
// When template execution invokes a function with an argument list, that list
// must be assignable to the function's parameter types. Functions meant to
// apply to arguments of arbitrary type can use parameters of type interface{} or
-// of type reflect.Value. Similarly, functions meant to return a result of arbitrary
-// type can return interface{} or reflect.Value.
+// of type [reflect.Value]. Similarly, functions meant to return a result of arbitrary
+// type can return interface{} or [reflect.Value].
type FuncMap map[string]any
// builtins returns the FuncMap.
@@ -39,7 +39,7 @@ type FuncMap map[string]any
func builtins() FuncMap {
return FuncMap{
"and": and,
- "call": call,
+ "call": emptyCall,
"html": HTMLEscaper,
"index": index,
"slice": slice,
@@ -93,8 +93,8 @@ func addValueFuncs(out map[string]reflect.Value, in FuncMap) {
if v.Kind() != reflect.Func {
panic("value for " + name + " not a function")
}
- if !goodFunc(v.Type()) {
- panic(fmt.Errorf("can't install method/function %q with %d results", name, v.Type().NumOut()))
+ if err := goodFunc(name, v.Type()); err != nil {
+ panic(err)
}
out[name] = v
}
@@ -109,15 +109,18 @@ func addFuncs(out, in FuncMap) {
}
// goodFunc reports whether the function or method has the right result signature.
-func goodFunc(typ reflect.Type) bool {
+func goodFunc(name string, typ reflect.Type) error {
// We allow functions with 1 result or 2 results where the second is an error.
- switch {
- case typ.NumOut() == 1:
- return true
- case typ.NumOut() == 2 && typ.Out(1) == errorType:
- return true
+ switch numOut := typ.NumOut(); {
+ case numOut == 1:
+ return nil
+ case numOut == 2 && typ.Out(1) == errorType:
+ return nil
+ case numOut == 2:
+ return fmt.Errorf("invalid function signature for %s: second return value should be error; is %s", name, typ.Out(1))
+ default:
+ return fmt.Errorf("function %s has %d return values; should be 1 or 2", name, typ.NumOut())
}
- return false
}
// goodName reports whether the function name is a valid identifier.
@@ -309,30 +312,35 @@ func length(item reflect.Value) (int, error) {
// Function invocation
+func emptyCall(fn reflect.Value, args ...reflect.Value) reflect.Value {
+ panic("unreachable") // implemented as a special case in evalCall
+}
+
// call returns the result of evaluating the first argument as a function.
// The function must return 1 result, or 2 results, the second of which is an error.
-func call(fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
+func call(name string, fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
fn = indirectInterface(fn)
if !fn.IsValid() {
return reflect.Value{}, fmt.Errorf("call of nil")
}
typ := fn.Type()
if typ.Kind() != reflect.Func {
- return reflect.Value{}, fmt.Errorf("non-function of type %s", typ)
+ return reflect.Value{}, fmt.Errorf("non-function %s of type %s", name, typ)
}
- if !goodFunc(typ) {
- return reflect.Value{}, fmt.Errorf("function called with %d args; should be 1 or 2", typ.NumOut())
+
+ if err := goodFunc(name, typ); err != nil {
+ return reflect.Value{}, err
}
numIn := typ.NumIn()
var dddType reflect.Type
if typ.IsVariadic() {
if len(args) < numIn-1 {
- return reflect.Value{}, fmt.Errorf("wrong number of args: got %d want at least %d", len(args), numIn-1)
+ return reflect.Value{}, fmt.Errorf("wrong number of args for %s: got %d want at least %d", name, len(args), numIn-1)
}
dddType = typ.In(numIn - 1).Elem()
} else {
if len(args) != numIn {
- return reflect.Value{}, fmt.Errorf("wrong number of args: got %d want %d", len(args), numIn)
+ return reflect.Value{}, fmt.Errorf("wrong number of args for %s: got %d want %d", name, len(args), numIn)
}
}
argv := make([]reflect.Value, len(args))
diff --git a/tpl/internal/go_templates/texttemplate/helper.go b/tpl/internal/go_templates/texttemplate/helper.go
index 48af3928b..81b55538e 100644
--- a/tpl/internal/go_templates/texttemplate/helper.go
+++ b/tpl/internal/go_templates/texttemplate/helper.go
@@ -16,7 +16,7 @@ import (
// Functions and methods to parse templates.
-// Must is a helper that wraps a call to a function returning (*Template, error)
+// Must is a helper that wraps a call to a function returning ([*Template], error)
// and panics if the error is non-nil. It is intended for use in variable
// initializations such as
//
@@ -28,7 +28,7 @@ func Must(t *Template, err error) *Template {
return t
}
-// ParseFiles creates a new Template and parses the template definitions from
+// ParseFiles creates a new [Template] and parses the template definitions from
// the named files. The returned template's name will have the base name and
// parsed contents of the first file. There must be at least one file.
// If an error occurs, parsing stops and the returned *Template is nil.
@@ -45,9 +45,9 @@ func ParseFiles(filenames ...string) (*Template, error) {
// t. If an error occurs, parsing stops and the returned template is nil;
// otherwise it is t. There must be at least one file.
// Since the templates created by ParseFiles are named by the base
-// names of the argument files, t should usually have the name of one
-// of the (base) names of the files. If it does not, depending on t's
-// contents before calling ParseFiles, t.Execute may fail. In that
+// (see [filepath.Base]) names of the argument files, t should usually have the
+// name of one of the (base) names of the files. If it does not, depending on
+// t's contents before calling ParseFiles, t.Execute may fail. In that
// case use t.ExecuteTemplate to execute a valid template.
//
// When parsing multiple files with the same name in different directories,
@@ -93,12 +93,12 @@ func parseFiles(t *Template, readFile func(string) (string, []byte, error), file
return t, nil
}
-// ParseGlob creates a new Template and parses the template definitions from
+// ParseGlob creates a new [Template] and parses the template definitions from
// the files identified by the pattern. The files are matched according to the
-// semantics of filepath.Match, and the pattern must match at least one file.
-// The returned template will have the (base) name and (parsed) contents of the
-// first file matched by the pattern. ParseGlob is equivalent to calling
-// ParseFiles with the list of files matched by the pattern.
+// semantics of [filepath.Match], and the pattern must match at least one file.
+// The returned template will have the [filepath.Base] name and (parsed)
+// contents of the first file matched by the pattern. ParseGlob is equivalent to
+// calling [ParseFiles] with the list of files matched by the pattern.
//
// When parsing multiple files with the same name in different directories,
// the last one mentioned will be the one that results.
@@ -108,9 +108,9 @@ func ParseGlob(pattern string) (*Template, error) {
// ParseGlob parses the template definitions in the files identified by the
// pattern and associates the resulting templates with t. The files are matched
-// according to the semantics of filepath.Match, and the pattern must match at
-// least one file. ParseGlob is equivalent to calling t.ParseFiles with the
-// list of files matched by the pattern.
+// according to the semantics of [filepath.Match], and the pattern must match at
+// least one file. ParseGlob is equivalent to calling [Template.ParseFiles] with
+// the list of files matched by the pattern.
//
// When parsing multiple files with the same name in different directories,
// the last one mentioned will be the one that results.
@@ -131,17 +131,17 @@ func parseGlob(t *Template, pattern string) (*Template, error) {
return parseFiles(t, readFileOS, filenames...)
}
-// ParseFS is like ParseFiles or ParseGlob but reads from the file system fsys
+// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fsys
// instead of the host operating system's file system.
-// It accepts a list of glob patterns.
+// It accepts a list of glob patterns (see [path.Match]).
// (Note that most file names serve as glob patterns matching only themselves.)
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
return parseFS(nil, fsys, patterns)
}
-// ParseFS is like ParseFiles or ParseGlob but reads from the file system fsys
+// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fsys
// instead of the host operating system's file system.
-// It accepts a list of glob patterns.
+// It accepts a list of glob patterns (see [path.Match]).
// (Note that most file names serve as glob patterns matching only themselves.)
func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
t.init()
diff --git a/tpl/internal/go_templates/texttemplate/hugo_template.go b/tpl/internal/go_templates/texttemplate/hugo_template.go
index 276367a7c..4f505d8c5 100644
--- a/tpl/internal/go_templates/texttemplate/hugo_template.go
+++ b/tpl/internal/go_templates/texttemplate/hugo_template.go
@@ -15,9 +15,12 @@ package template
import (
"context"
+ "fmt"
"io"
+ "iter"
"reflect"
+ "github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hreflect"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
@@ -255,18 +258,61 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node,
panic("not reached")
}
+// newErrorWithCause creates a new error with the given cause.
+func newErrorWithCause(err error) *TryError {
+ return &TryError{Err: err, Cause: herrors.Cause(err)}
+}
+
+// TryError wraps an error with a cause.
+type TryError struct {
+ Err error
+ Cause error
+}
+
+func (e *TryError) Error() string {
+ return e.Err.Error()
+}
+
+func (e *TryError) Unwrap() error {
+ return e.Err
+}
+
+// TryValue is what gets returned when using the "try" keyword.
+type TryValue struct {
+ // Value is the value returned by the function or method wrapped with "try".
+ // This will always be nil if Err is set.
+ Value any
+
+ // Err is the error returned by the function or method wrapped with "try".
+ // This will always be nil if Value is set.
+ Err *TryError
+}
+
// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
// it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0]
// as the function itself.
-func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node, name string, args []parse.Node, final reflect.Value, first ...reflect.Value) reflect.Value {
+func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node, name string, args []parse.Node, final reflect.Value, first ...reflect.Value) (val reflect.Value) {
+ // Added for Hugo.
+ if name == "try" {
+ defer func() {
+ if r := recover(); r != nil {
+ // Cause: herrors.Cause(err)
+ if err, ok := r.(error); ok {
+ val = reflect.ValueOf(TryValue{Value: nil, Err: newErrorWithCause(err)})
+ } else {
+ val = reflect.ValueOf(TryValue{Value: nil, Err: newErrorWithCause(fmt.Errorf("%v", r))})
+ }
+ }
+ }()
+ }
+
if args != nil {
args = args[1:] // Zeroth arg is function name/node; not passed to function.
}
-
typ := fun.Type()
- numFirst := len(first)
+ numFirst := len(first) // Added for Hugo
numIn := len(args) + numFirst // Added for Hugo
- if final != missingVal {
+ if !isMissing(final) {
numIn++
}
numFixed := len(args) + len(first) // Adjusted for Hugo
@@ -278,9 +324,8 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
} else if numIn != typ.NumIn() {
s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
}
- if !goodFunc(typ) {
- // TODO: This could still be a confusing error; maybe goodFunc should provide info.
- s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
+ if err := goodFunc(name, typ); err != nil {
+ s.errorf("%v", err)
}
unwrap := func(v reflect.Value) reflect.Value {
@@ -302,7 +347,7 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
return v
}
}
- if final != missingVal {
+ if !final.Equal(missingVal) {
// The last argument to and/or is coming from
// the pipeline. We didn't short circuit on an earlier
// argument, so we are going to return this one.
@@ -329,7 +374,7 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
}
}
// Add final value if necessary.
- if final != missingVal {
+ if !isMissing(final) {
t := typ.In(typ.NumIn() - 1)
if typ.IsVariadic() {
if numIn-1 < numFixed {
@@ -345,6 +390,20 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
argv[i] = s.validateType(final, t)
}
+ // Special case for the "call" builtin.
+ // Insert the name of the callee function as the first argument.
+ if isBuiltin && name == "call" {
+ var calleeName string
+ if len(args) == 0 {
+ // final must be present or we would have errored out above.
+ calleeName = final.String()
+ } else {
+ calleeName = args[0].String()
+ }
+ argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
+ fun = reflect.ValueOf(call)
+ }
+
// Added for Hugo
for i := 0; i < len(first); i++ {
argv[i] = s.validateType(first[i], typ.In(i))
@@ -364,9 +423,29 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
s.helper.OnCalled(s.ctx, s.prep, name, argv, vv)
}
+ // Added for Hugo.
+ if name == "try" {
+ return reflect.ValueOf(TryValue{Value: vv.Interface()})
+ }
+
return vv
}
func isTrue(val reflect.Value) (truth, ok bool) {
return hreflect.IsTruthfulValue(val), true
}
+
+func (t *Template) All() iter.Seq[*Template] {
+ return func(yield func(t *Template) bool) {
+ if t.common == nil {
+ return
+ }
+ t.muTmpl.RLock()
+ defer t.muTmpl.RUnlock()
+ for _, v := range t.tmpl {
+ if !yield(v) {
+ return
+ }
+ }
+ }
+}
diff --git a/tpl/internal/go_templates/texttemplate/parse/lex.go b/tpl/internal/go_templates/texttemplate/parse/lex.go
index 70fc86b63..a00f48e65 100644
--- a/tpl/internal/go_templates/texttemplate/parse/lex.go
+++ b/tpl/internal/go_templates/texttemplate/parse/lex.go
@@ -352,6 +352,7 @@ func lexComment(l *lexer) stateFn {
if !delim {
return l.errorf("comment ends before closing delimiter")
}
+ l.line += strings.Count(l.input[l.start:l.pos], "\n")
i := l.thisItem(itemComment)
if trimSpace {
l.pos += trimMarkerLen
diff --git a/tpl/internal/go_templates/texttemplate/parse/lex_test.go b/tpl/internal/go_templates/texttemplate/parse/lex_test.go
index 11e88792e..b1ea69967 100644
--- a/tpl/internal/go_templates/texttemplate/parse/lex_test.go
+++ b/tpl/internal/go_templates/texttemplate/parse/lex_test.go
@@ -548,6 +548,16 @@ var lexPosTests = []lexTest{
{itemRightDelim, 11, "}}", 2},
{itemEOF, 13, "", 2},
}},
+ {"longcomment", "{{/*\n*/}}\n{{undefinedFunction \"test\"}}", []item{
+ {itemComment, 2, "/*\n*/", 1},
+ {itemText, 9, "\n", 2},
+ {itemLeftDelim, 10, "{{", 3},
+ {itemIdentifier, 12, "undefinedFunction", 3},
+ {itemSpace, 29, " ", 3},
+ {itemString, 30, "\"test\"", 3},
+ {itemRightDelim, 36, "}}", 3},
+ {itemEOF, 38, "", 3},
+ }},
}
// The other tests don't check position, to make the test cases easier to construct.
diff --git a/tpl/internal/go_templates/texttemplate/parse/node.go b/tpl/internal/go_templates/texttemplate/parse/node.go
index c36688825..a31309874 100644
--- a/tpl/internal/go_templates/texttemplate/parse/node.go
+++ b/tpl/internal/go_templates/texttemplate/parse/node.go
@@ -217,7 +217,11 @@ func (p *PipeNode) writeTo(sb *strings.Builder) {
}
v.writeTo(sb)
}
- sb.WriteString(" := ")
+ if p.IsAssign {
+ sb.WriteString(" = ")
+ } else {
+ sb.WriteString(" := ")
+ }
}
for i, c := range p.Cmds {
if i > 0 {
@@ -346,12 +350,12 @@ type IdentifierNode struct {
Ident string // The identifier's name.
}
-// NewIdentifier returns a new IdentifierNode with the given identifier name.
+// NewIdentifier returns a new [IdentifierNode] with the given identifier name.
func NewIdentifier(ident string) *IdentifierNode {
return &IdentifierNode{NodeType: NodeIdentifier, Ident: ident}
}
-// SetPos sets the position. NewIdentifier is a public method so we can't modify its signature.
+// SetPos sets the position. [NewIdentifier] is a public method so we can't modify its signature.
// Chained for convenience.
// TODO: fix one day?
func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode {
@@ -359,7 +363,7 @@ func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode {
return i
}
-// SetTree sets the parent tree for the node. NewIdentifier is a public method so we can't modify its signature.
+// SetTree sets the parent tree for the node. [NewIdentifier] is a public method so we can't modify its signature.
// Chained for convenience.
// TODO: fix one day?
func (i *IdentifierNode) SetTree(t *Tree) *IdentifierNode {
diff --git a/tpl/internal/go_templates/texttemplate/parse/parse.go b/tpl/internal/go_templates/texttemplate/parse/parse.go
index d43d5334b..e05a33d6f 100644
--- a/tpl/internal/go_templates/texttemplate/parse/parse.go
+++ b/tpl/internal/go_templates/texttemplate/parse/parse.go
@@ -42,7 +42,7 @@ const (
SkipFuncCheck // do not check that functions are defined
)
-// Copy returns a copy of the Tree. Any parsing state is discarded.
+// Copy returns a copy of the [Tree]. Any parsing state is discarded.
func (t *Tree) Copy() *Tree {
if t == nil {
return nil
@@ -55,7 +55,7 @@ func (t *Tree) Copy() *Tree {
}
}
-// Parse returns a map from template name to parse.Tree, created by parsing the
+// Parse returns a map from template name to [Tree], created by parsing the
// templates described in the argument string. The top-level template will be
// given the specified name. If an error is encountered, parsing stops and an
// empty map is returned with the error.
@@ -521,7 +521,7 @@ func (t *Tree) checkPipeline(pipe *PipeNode, context string) {
}
}
-func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
+func (t *Tree) parseControl(context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
defer t.popVars(len(t.vars))
pipe = t.pipeline(context, itemRightDelim)
if context == "range" {
@@ -533,29 +533,32 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int
t.rangeDepth--
}
switch next.Type() {
- case nodeEnd: //done
+ case nodeEnd: // done
case nodeElse:
- if allowElseIf {
- // Special case for "else if". If the "else" is followed immediately by an "if",
- // the elseControl will have left the "if" token pending. Treat
- // {{if a}}_{{else if b}}_{{end}}
- // as
- // {{if a}}_{{else}}{{if b}}_{{end}}{{end}}.
- // To do this, parse the if as usual and stop at it {{end}}; the subsequent{{end}}
- // is assumed. This technique works even for long if-else-if chains.
- // TODO: Should we allow else-if in with and range?
- if t.peek().typ == itemIf {
- t.next() // Consume the "if" token.
- elseList = t.newList(next.Position())
- elseList.append(t.ifControl())
- // Do not consume the next item - only one {{end}} required.
- break
+ // Special case for "else if" and "else with".
+ // If the "else" is followed immediately by an "if" or "with",
+ // the elseControl will have left the "if" or "with" token pending. Treat
+ // {{if a}}_{{else if b}}_{{end}}
+ // {{with a}}_{{else with b}}_{{end}}
+ // as
+ // {{if a}}_{{else}}{{if b}}_{{end}}{{end}}
+ // {{with a}}_{{else}}{{with b}}_{{end}}{{end}}.
+ // To do this, parse the "if" or "with" as usual and stop at it {{end}};
+ // the subsequent{{end}} is assumed. This technique works even for long if-else-if chains.
+ if context == "if" && t.peek().typ == itemIf {
+ t.next() // Consume the "if" token.
+ elseList = t.newList(next.Position())
+ elseList.append(t.ifControl())
+ } else if context == "with" && t.peek().typ == itemWith {
+ t.next()
+ elseList = t.newList(next.Position())
+ elseList.append(t.withControl())
+ } else {
+ elseList, next = t.itemList()
+ if next.Type() != nodeEnd {
+ t.errorf("expected end; found %s", next)
}
}
- elseList, next = t.itemList()
- if next.Type() != nodeEnd {
- t.errorf("expected end; found %s", next)
- }
}
return pipe.Position(), pipe.Line, pipe, list, elseList
}
@@ -567,7 +570,7 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int
//
// If keyword is past.
func (t *Tree) ifControl() Node {
- return t.newIf(t.parseControl(true, "if"))
+ return t.newIf(t.parseControl("if"))
}
// Range:
@@ -577,7 +580,7 @@ func (t *Tree) ifControl() Node {
//
// Range keyword is past.
func (t *Tree) rangeControl() Node {
- r := t.newRange(t.parseControl(false, "range"))
+ r := t.newRange(t.parseControl("range"))
return r
}
@@ -588,7 +591,7 @@ func (t *Tree) rangeControl() Node {
//
// If keyword is past.
func (t *Tree) withControl() Node {
- return t.newWith(t.parseControl(false, "with"))
+ return t.newWith(t.parseControl("with"))
}
// End:
@@ -606,10 +609,11 @@ func (t *Tree) endControl() Node {
//
// Else keyword is past.
func (t *Tree) elseControl() Node {
- // Special case for "else if".
peek := t.peekNonSpace()
- if peek.typ == itemIf {
- // We see "{{else if ... " but in effect rewrite it to {{else}}{{if ... ".
+ // The "{{else if ... " and "{{else with ..." will be
+ // treated as "{{else}}{{if ..." and "{{else}}{{with ...".
+ // So return the else node here.
+ if peek.typ == itemIf || peek.typ == itemWith {
return t.newElse(peek.pos, peek.line)
}
token := t.expect(itemRightDelim, "else")
diff --git a/tpl/internal/go_templates/texttemplate/parse/parse_test.go b/tpl/internal/go_templates/texttemplate/parse/parse_test.go
index 080eea2f9..47951a9c9 100644
--- a/tpl/internal/go_templates/texttemplate/parse/parse_test.go
+++ b/tpl/internal/go_templates/texttemplate/parse/parse_test.go
@@ -247,6 +247,10 @@ var parseTests = []parseTest{
`{{with .X}}"hello"{{end}}`},
{"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
`{{with .X}}"hello"{{else}}"goodbye"{{end}}`},
+ {"with with else with", "{{with .X}}hello{{else with .Y}}goodbye{{end}}", noError,
+ `{{with .X}}"hello"{{else}}{{with .Y}}"goodbye"{{end}}{{end}}`},
+ {"with else chain", "{{with .X}}X{{else with .Y}}Y{{else with .Z}}Z{{end}}", noError,
+ `{{with .X}}"X"{{else}}{{with .Y}}"Y"{{else}}{{with .Z}}"Z"{{end}}{{end}}{{end}}`},
// Trimming spaces.
{"trim left", "x \r\n\t{{- 3}}", noError, `"x"{{3}}`},
{"trim right", "{{3 -}}\n\n\ty", noError, `{{3}}"y"`},
@@ -305,6 +309,9 @@ var parseTests = []parseTest{
{"bug1a", "{{$x:=.}}{{$x!2}}", hasError, ""}, // ! is just illegal here.
{"bug1b", "{{$x:=.}}{{$x+2}}", hasError, ""}, // $x+2 should not parse as ($x) (+2).
{"bug1c", "{{$x:=.}}{{$x +2}}", noError, "{{$x := .}}{{$x +2}}"}, // It's OK with a space.
+ // Check the range handles assignment vs. declaration properly.
+ {"bug2a", "{{range $x := 0}}{{$x}}{{end}}", noError, "{{range $x := 0}}{{$x}}{{end}}"},
+ {"bug2b", "{{range $x = 0}}{{$x}}{{end}}", noError, "{{range $x = 0}}{{$x}}{{end}}"},
// dot following a literal value
{"dot after integer", "{{1.E}}", hasError, ""},
{"dot after float", "{{0.1.E}}", hasError, ""},
diff --git a/tpl/internal/go_templates/texttemplate/template.go b/tpl/internal/go_templates/texttemplate/template.go
index 1ba72c194..0be64eee2 100644
--- a/tpl/internal/go_templates/texttemplate/template.go
+++ b/tpl/internal/go_templates/texttemplate/template.go
@@ -6,6 +6,7 @@ package template
import (
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
+ "maps"
"reflect"
"sync"
)
@@ -24,7 +25,7 @@ type common struct {
}
// Template is the representation of a parsed template. The *parse.Tree
-// field is exported only for use by html/template and should be treated
+// field is exported only for use by [html/template] and should be treated
// as unexported by all other clients.
type Template struct {
name string
@@ -79,7 +80,7 @@ func (t *Template) init() {
// Clone returns a duplicate of the template, including all associated
// templates. The actual representation is not copied, but the name space of
-// associated templates is, so further calls to Parse in the copy will add
+// associated templates is, so further calls to [Template.Parse] in the copy will add
// templates to the copy but not to the original. Clone can be used to prepare
// common templates and use them with variant definitions for other templates
// by adding the variants after the clone is made.
@@ -102,12 +103,8 @@ func (t *Template) Clone() (*Template, error) {
}
t.muFuncs.RLock()
defer t.muFuncs.RUnlock()
- for k, v := range t.parseFuncs {
- nt.parseFuncs[k] = v
- }
- for k, v := range t.execFuncs {
- nt.execFuncs[k] = v
- }
+ maps.Copy(nt.parseFuncs, t.parseFuncs)
+ maps.Copy(nt.execFuncs, t.execFuncs)
return nt, nil
}
@@ -157,7 +154,7 @@ func (t *Template) Templates() []*Template {
}
// Delims sets the action delimiters to the specified strings, to be used in
-// subsequent calls to Parse, ParseFiles, or ParseGlob. Nested template
+// subsequent calls to [Template.Parse], [Template.ParseFiles], or [Template.ParseGlob]. Nested template
// definitions will inherit the settings. An empty delimiter stands for the
// corresponding default: {{ or }}.
// The return value is the template, so calls can be chained.
diff --git a/tpl/internal/templatefuncsRegistry.go b/tpl/internal/templatefuncsRegistry.go
index fc02a6ef9..425938b07 100644
--- a/tpl/internal/templatefuncsRegistry.go
+++ b/tpl/internal/templatefuncsRegistry.go
@@ -51,6 +51,9 @@ type TemplateFuncsNamespace struct {
// This is the method receiver.
Context func(ctx context.Context, v ...any) (any, error)
+ // OnCreated is called when all the namespaces are ready.
+ OnCreated func(namespaces map[string]any)
+
// Additional info, aliases and examples, per method name.
MethodMappings map[string]TemplateFuncMethodMapping
}
@@ -210,7 +213,7 @@ func (t *TemplateFuncsNamespace) toJSON(ctx context.Context) ([]byte, error) {
return nil, nil
}
ctxType := reflect.TypeOf(tctx)
- for i := 0; i < ctxType.NumMethod(); i++ {
+ for i := range ctxType.NumMethod() {
method := ctxType.Method(i)
if ignoreFuncs[method.Name] {
continue
diff --git a/tpl/js/init.go b/tpl/js/init.go
index 16e6c7efa..97e76c33d 100644
--- a/tpl/js/init.go
+++ b/tpl/js/init.go
@@ -24,13 +24,21 @@ const name = "js"
func init() {
f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
- ctx := New(d)
+ ctx, err := New(d)
+ if err != nil {
+ panic(err)
+ }
ns := &internal.TemplateFuncsNamespace{
Name: name,
Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
}
+ ns.AddMethodMapping(ctx.Babel,
+ []string{"babel"},
+ [][2]string{},
+ )
+
return ns
}
diff --git a/tpl/js/js.go b/tpl/js/js.go
index 63a676532..dfd0a3581 100644
--- a/tpl/js/js.go
+++ b/tpl/js/js.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Hugo Authors. All rights reserved.
+// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -15,26 +15,40 @@
package js
import (
+ "errors"
+
"github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/internal/js"
+ "github.com/gohugoio/hugo/internal/js/esbuild"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource"
- "github.com/gohugoio/hugo/resources/resource_transformers/js"
+ "github.com/gohugoio/hugo/resources/resource_factories/create"
+ "github.com/gohugoio/hugo/resources/resource_transformers/babel"
+ jstransform "github.com/gohugoio/hugo/resources/resource_transformers/js"
"github.com/gohugoio/hugo/tpl/internal/resourcehelpers"
)
// New returns a new instance of the js-namespaced template functions.
-func New(deps *deps.Deps) *Namespace {
- if deps.ResourceSpec == nil {
- return &Namespace{}
+func New(d *deps.Deps) (*Namespace, error) {
+ if d.ResourceSpec == nil {
+ return &Namespace{}, nil
}
+
return &Namespace{
- client: js.New(deps.BaseFs.Assets, deps.ResourceSpec),
- }
+ d: d,
+ jsTransformClient: jstransform.New(d.BaseFs.Assets, d.ResourceSpec),
+ createClient: create.New(d.ResourceSpec),
+ babelClient: babel.New(d.ResourceSpec),
+ }, nil
}
// Namespace provides template functions for the "js" namespace.
type Namespace struct {
- client *js.Client
+ d *deps.Deps
+
+ jsTransformClient *jstransform.Client
+ createClient *create.Client
+ babelClient *babel.Client
}
// Build processes the given Resource with ESBuild.
@@ -60,5 +74,44 @@ func (ns *Namespace) Build(args ...any) (resource.Resource, error) {
m = map[string]any{"targetPath": targetPath}
}
- return ns.client.Process(r, m)
+ return ns.jsTransformClient.Process(r, m)
+}
+
+// Batch creates a new Batcher with the given ID.
+// Repeated calls with the same ID will return the same Batcher.
+// The ID will be used to name the root directory of the batch.
+// Forward slashes in the ID is allowed.
+func (ns *Namespace) Batch(id string) (js.Batcher, error) {
+ if err := esbuild.ValidateBatchID(id, true); err != nil {
+ return nil, err
+ }
+
+ b, err := ns.d.JSBatcherClient.Store().GetOrCreate(id, func() (js.Batcher, error) {
+ return ns.d.JSBatcherClient.New(id)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return b, nil
+}
+
+// Babel processes the given Resource with Babel.
+func (ns *Namespace) Babel(args ...any) (resource.Resource, error) {
+ if len(args) > 2 {
+ return nil, errors.New("must not provide more arguments than resource object and options")
+ }
+
+ r, m, err := resourcehelpers.ResolveArgs(args)
+ if err != nil {
+ return nil, err
+ }
+ var options babel.Options
+ if m != nil {
+ options, err = babel.DecodeOptions(m)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return ns.babelClient.Process(r, options)
}
diff --git a/tpl/lang/lang.go b/tpl/lang/lang.go
index b4ff98684..4cbd661af 100644
--- a/tpl/lang/lang.go
+++ b/tpl/lang/lang.go
@@ -26,7 +26,6 @@ import (
translators "github.com/gohugoio/localescompressed"
"github.com/gohugoio/hugo/common/hreflect"
- "github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/deps"
"github.com/spf13/cast"
)
@@ -240,12 +239,6 @@ func (ns *Namespace) FormatNumberCustom(precision, number any, options ...any) (
return string(b), nil
}
-// Deprecated: Use lang.FormatNumberCustom instead.
-func (ns *Namespace) NumFmt(precision, number any, options ...any) (string, error) {
- hugo.Deprecate("lang.NumFmt", "Use lang.FormatNumberCustom instead.", "v0.120.0")
- return ns.FormatNumberCustom(precision, number, options...)
-}
-
type pagesLanguageMerger interface {
MergeByLanguageInterface(other any) (any, error)
}
diff --git a/tpl/math/init.go b/tpl/math/init.go
index fa1367671..bb3c02bd6 100644
--- a/tpl/math/init.go
+++ b/tpl/math/init.go
@@ -24,7 +24,7 @@ const name = "math"
func init() {
f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
- ctx := New()
+ ctx := New(d)
ns := &internal.TemplateFuncsNamespace{
Name: name,
@@ -38,6 +38,13 @@ func init() {
},
)
+ ns.AddMethodMapping(ctx.Acos,
+ nil,
+ [][2]string{
+ {"{{ math.Acos 1 }}", "0"},
+ },
+ )
+
ns.AddMethodMapping(ctx.Add,
[]string{"add"},
[][2]string{
@@ -45,6 +52,27 @@ func init() {
},
)
+ ns.AddMethodMapping(ctx.Asin,
+ nil,
+ [][2]string{
+ {"{{ math.Asin 1 }}", "1.5707963267948966"},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.Atan,
+ nil,
+ [][2]string{
+ {"{{ math.Atan 1 }}", "0.7853981633974483"},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.Atan2,
+ nil,
+ [][2]string{
+ {"{{ math.Atan2 1 2 }}", "0.4636476090008061"},
+ },
+ )
+
ns.AddMethodMapping(ctx.Ceil,
nil,
[][2]string{
@@ -52,6 +80,13 @@ func init() {
},
)
+ ns.AddMethodMapping(ctx.Cos,
+ nil,
+ [][2]string{
+ {"{{ math.Cos 1 }}", "0.5403023058681398"},
+ },
+ )
+
ns.AddMethodMapping(ctx.Div,
[]string{"div"},
[][2]string{
@@ -80,6 +115,13 @@ func init() {
},
)
+ ns.AddMethodMapping(ctx.MaxInt64,
+ nil,
+ [][2]string{
+ {"{{ math.MaxInt64 }}", "9223372036854775807"},
+ },
+ )
+
ns.AddMethodMapping(ctx.Min,
nil,
[][2]string{
@@ -108,6 +150,13 @@ func init() {
},
)
+ ns.AddMethodMapping(ctx.Pi,
+ nil,
+ [][2]string{
+ {"{{ math.Pi }}", "3.141592653589793"},
+ },
+ )
+
ns.AddMethodMapping(ctx.Pow,
[]string{"pow"},
[][2]string{
@@ -129,6 +178,13 @@ func init() {
},
)
+ ns.AddMethodMapping(ctx.Sin,
+ nil,
+ [][2]string{
+ {"{{ math.Sin 1 }}", "0.8414709848078965"},
+ },
+ )
+
ns.AddMethodMapping(ctx.Sqrt,
nil,
[][2]string{
@@ -143,6 +199,27 @@ func init() {
},
)
+ ns.AddMethodMapping(ctx.Tan,
+ nil,
+ [][2]string{
+ {"{{ math.Tan 1 }}", "1.557407724654902"},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.ToDegrees,
+ nil,
+ [][2]string{
+ {"{{ math.ToDegrees 1.5707963267948966 }}", "90"},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.ToRadians,
+ nil,
+ [][2]string{
+ {"{{ math.ToRadians 90 }}", "1.5707963267948966"},
+ },
+ )
+
return ns
}
diff --git a/tpl/math/math.go b/tpl/math/math.go
index 3368f6b73..01e75e9c8 100644
--- a/tpl/math/math.go
+++ b/tpl/math/math.go
@@ -20,9 +20,9 @@ import (
"math"
"math/rand"
"reflect"
- "sync/atomic"
_math "github.com/gohugoio/hugo/common/math"
+ "github.com/gohugoio/hugo/deps"
"github.com/spf13/cast"
)
@@ -32,12 +32,16 @@ var (
)
// New returns a new instance of the math-namespaced template functions.
-func New() *Namespace {
- return &Namespace{}
+func New(d *deps.Deps) *Namespace {
+ return &Namespace{
+ d: d,
+ }
}
// Namespace provides template functions for the "math" namespace.
-type Namespace struct{}
+type Namespace struct {
+ d *deps.Deps
+}
// Abs returns the absolute value of n.
func (ns *Namespace) Abs(n any) (float64, error) {
@@ -49,11 +53,51 @@ func (ns *Namespace) Abs(n any) (float64, error) {
return math.Abs(af), nil
}
+// Acos returns the arccosine, in radians, of n.
+func (ns *Namespace) Acos(n any) (float64, error) {
+ af, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires a numeric argument")
+ }
+ return math.Acos(af), nil
+}
+
// Add adds the multivalued addends n1 and n2 or more values.
func (ns *Namespace) Add(inputs ...any) (any, error) {
return ns.doArithmetic(inputs, '+')
}
+// Asin returns the arcsine, in radians, of n.
+func (ns *Namespace) Asin(n any) (float64, error) {
+ af, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires a numeric argument")
+ }
+ return math.Asin(af), nil
+}
+
+// Atan returns the arctangent, in radians, of n.
+func (ns *Namespace) Atan(n any) (float64, error) {
+ af, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires a numeric argument")
+ }
+ return math.Atan(af), nil
+}
+
+// Atan2 returns the arc tangent of n/m, using the signs of the two to determine the quadrant of the return value.
+func (ns *Namespace) Atan2(n, m any) (float64, error) {
+ afx, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires numeric arguments")
+ }
+ afy, err := cast.ToFloat64E(m)
+ if err != nil {
+ return 0, errors.New("requires numeric arguments")
+ }
+ return math.Atan2(afx, afy), nil
+}
+
// Ceil returns the least integer value greater than or equal to n.
func (ns *Namespace) Ceil(n any) (float64, error) {
xf, err := cast.ToFloat64E(n)
@@ -64,6 +108,15 @@ func (ns *Namespace) Ceil(n any) (float64, error) {
return math.Ceil(xf), nil
}
+// Cos returns the cosine of the radian argument n.
+func (ns *Namespace) Cos(n any) (float64, error) {
+ af, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires a numeric argument")
+ }
+ return math.Cos(af), nil
+}
+
// Div divides n1 by n2.
func (ns *Namespace) Div(inputs ...any) (any, error) {
return ns.doArithmetic(inputs, '/')
@@ -94,27 +147,16 @@ func (ns *Namespace) Max(inputs ...any) (maximum float64, err error) {
return ns.applyOpToScalarsOrSlices("Max", math.Max, inputs...)
}
+// MaxInt64 returns the maximum value for a signed 64-bit integer.
+func (ns *Namespace) MaxInt64() int64 {
+ return math.MaxInt64
+}
+
// Min returns the smaller of all numbers in inputs. Any slices in inputs are flattened.
func (ns *Namespace) Min(inputs ...any) (minimum float64, err error) {
return ns.applyOpToScalarsOrSlices("Min", math.Min, inputs...)
}
-// Sum returns the sum of all numbers in inputs. Any slices in inputs are flattened.
-func (ns *Namespace) Sum(inputs ...any) (sum float64, err error) {
- fn := func(x, y float64) float64 {
- return x + y
- }
- return ns.applyOpToScalarsOrSlices("Sum", fn, inputs...)
-}
-
-// Product returns the product of all numbers in inputs. Any slices in inputs are flattened.
-func (ns *Namespace) Product(inputs ...any) (product float64, err error) {
- fn := func(x, y float64) float64 {
- return x * y
- }
- return ns.applyOpToScalarsOrSlices("Product", fn, inputs...)
-}
-
// Mod returns n1 % n2.
func (ns *Namespace) Mod(n1, n2 any) (int64, error) {
ai, erra := cast.ToInt64E(n1)
@@ -146,6 +188,11 @@ func (ns *Namespace) Mul(inputs ...any) (any, error) {
return ns.doArithmetic(inputs, '*')
}
+// Pi returns the mathematical constant pi.
+func (ns *Namespace) Pi() float64 {
+ return math.Pi
+}
+
// Pow returns n1 raised to the power of n2.
func (ns *Namespace) Pow(n1, n2 any) (float64, error) {
af, erra := cast.ToFloat64E(n1)
@@ -158,6 +205,14 @@ func (ns *Namespace) Pow(n1, n2 any) (float64, error) {
return math.Pow(af, bf), nil
}
+// Product returns the product of all numbers in inputs. Any slices in inputs are flattened.
+func (ns *Namespace) Product(inputs ...any) (product float64, err error) {
+ fn := func(x, y float64) float64 {
+ return x * y
+ }
+ return ns.applyOpToScalarsOrSlices("Product", fn, inputs...)
+}
+
// Rand returns, as a float64, a pseudo-random number in the half-open interval [0.0,1.0).
func (ns *Namespace) Rand() float64 {
return rand.Float64()
@@ -173,6 +228,15 @@ func (ns *Namespace) Round(n any) (float64, error) {
return _round(xf), nil
}
+// Sin returns the sine of the radian argument n.
+func (ns *Namespace) Sin(n any) (float64, error) {
+ af, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires a numeric argument")
+ }
+ return math.Sin(af), nil
+}
+
// Sqrt returns the square root of the number n.
func (ns *Namespace) Sqrt(n any) (float64, error) {
af, err := cast.ToFloat64E(n)
@@ -188,6 +252,43 @@ func (ns *Namespace) Sub(inputs ...any) (any, error) {
return ns.doArithmetic(inputs, '-')
}
+// Sum returns the sum of all numbers in inputs. Any slices in inputs are flattened.
+func (ns *Namespace) Sum(inputs ...any) (sum float64, err error) {
+ fn := func(x, y float64) float64 {
+ return x + y
+ }
+ return ns.applyOpToScalarsOrSlices("Sum", fn, inputs...)
+}
+
+// Tan returns the tangent of the radian argument n.
+func (ns *Namespace) Tan(n any) (float64, error) {
+ af, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires a numeric argument")
+ }
+ return math.Tan(af), nil
+}
+
+// ToDegrees converts radians into degrees.
+func (ns *Namespace) ToDegrees(n any) (float64, error) {
+ af, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires a numeric argument")
+ }
+
+ return af * 180 / math.Pi, nil
+}
+
+// ToRadians converts degrees into radians.
+func (ns *Namespace) ToRadians(n any) (float64, error) {
+ af, err := cast.ToFloat64E(n)
+ if err != nil {
+ return 0, errors.New("requires a numeric argument")
+ }
+
+ return af * math.Pi / 180, nil
+}
+
func (ns *Namespace) applyOpToScalarsOrSlices(opName string, op func(x, y float64) float64, inputs ...any) (result float64, err error) {
var i int
var hasValue bool
@@ -222,7 +323,7 @@ func (ns *Namespace) toFloatsE(v any) ([]float64, bool, error) {
switch vv.Kind() {
case reflect.Slice, reflect.Array:
var floats []float64
- for i := 0; i < vv.Len(); i++ {
+ for i := range vv.Len() {
f, err := cast.ToFloat64E(vv.Index(i).Interface())
if err != nil {
return nil, true, err
@@ -253,8 +354,6 @@ func (ns *Namespace) doArithmetic(inputs []any, operation rune) (value any, err
return
}
-var counter uint64
-
// Counter increments and returns a global counter.
// This was originally added to be used in tests where now.UnixNano did not
// have the needed precision (especially on Windows).
@@ -262,5 +361,5 @@ var counter uint64
// and the counter will reset on new builds.
// {"identifiers": ["now.UnixNano"] }
func (ns *Namespace) Counter() uint64 {
- return atomic.AddUint64(&counter, uint64(1))
+ return ns.d.Counters.MathCounter.Add(1)
}
diff --git a/tpl/math/math_test.go b/tpl/math/math_test.go
index 4cde3fb85..228570509 100644
--- a/tpl/math/math_test.go
+++ b/tpl/math/math_test.go
@@ -24,7 +24,7 @@ func TestBasicNSArithmetic(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
type TestCase struct {
fn func(inputs ...any) (any, error)
@@ -66,7 +66,7 @@ func TestBasicNSArithmetic(t *testing.T) {
func TestAbs(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
@@ -93,7 +93,7 @@ func TestAbs(t *testing.T) {
func TestCeil(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
@@ -126,7 +126,7 @@ func TestFloor(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
@@ -159,7 +159,7 @@ func TestLog(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
@@ -200,7 +200,7 @@ func TestSqrt(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
@@ -239,7 +239,7 @@ func TestMod(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
@@ -258,7 +258,7 @@ func TestMod(t *testing.T) {
{int32(3), int32(2), int64(1)},
{int64(3), int64(2), int64(1)},
{"3", "2", int64(1)},
- {"3.1", "2", false},
+ {"3.1", "2", int64(1)},
{"aaa", "0", false},
{"3", "aaa", false},
} {
@@ -279,7 +279,7 @@ func TestModBool(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
@@ -304,7 +304,7 @@ func TestModBool(t *testing.T) {
{int64(3), int64(2), false},
{"3", "3", true},
{"3", "2", false},
- {"3.1", "2", nil},
+ {"3.1", "2", false},
{"aaa", "0", nil},
{"3", "aaa", nil},
} {
@@ -325,7 +325,7 @@ func TestRound(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
x any
@@ -358,7 +358,7 @@ func TestPow(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
for _, test := range []struct {
a any
@@ -398,7 +398,7 @@ func TestMax(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
type TestCase struct {
values []any
@@ -452,7 +452,7 @@ func TestMin(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
type TestCase struct {
values []any
@@ -507,7 +507,7 @@ func TestSum(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
mustSum := func(values ...any) any {
result, err := ns.Sum(values...)
@@ -530,7 +530,7 @@ func TestProduct(t *testing.T) {
t.Parallel()
c := qt.New(t)
- ns := New()
+ ns := New(nil)
mustProduct := func(values ...any) any {
result, err := ns.Product(values...)
@@ -547,3 +547,346 @@ func TestProduct(t *testing.T) {
_, err := ns.Product()
c.Assert(err, qt.Not(qt.IsNil))
}
+
+// Test trigonometric functions
+
+func TestPi(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+
+ ns := New(nil)
+
+ expect := 3.1415
+ result := ns.Pi()
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(result, qt.Equals, expect)
+}
+
+func TestSin(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+
+ ns := New(nil)
+
+ for _, test := range []struct {
+ a any
+ expect any
+ }{
+ {0, 0.0},
+ {1, 0.8414},
+ {math.Pi / 2, 1.0},
+ {math.Pi, 0.0},
+ {-1.0, -0.8414},
+ {"abc", false},
+ } {
+
+ result, err := ns.Sin(test.a)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+}
+
+func TestCos(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+
+ ns := New(nil)
+
+ for _, test := range []struct {
+ a any
+ expect any
+ }{
+ {0, 1.0},
+ {1, 0.5403},
+ {math.Pi / 2, 0.0},
+ {math.Pi, -1.0},
+ {-1.0, 0.5403},
+ {"abc", false},
+ } {
+
+ result, err := ns.Cos(test.a)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+}
+
+func TestTan(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+
+ ns := New(nil)
+
+ for _, test := range []struct {
+ a any
+ expect any
+ }{
+ {0, 0.0},
+ {1, 1.5574},
+ // {math.Pi / 2, math.Inf(1)},
+ {math.Pi, 0.0},
+ {-1.0, -1.5574},
+ {"abc", false},
+ } {
+
+ result, err := ns.Tan(test.a)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ if result != math.Inf(1) {
+ result = float64(int(result*10000)) / 10000
+ }
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+
+ // Separate test for Tan(oo) -- returns NaN
+ result, err := ns.Tan(math.Inf(1))
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Satisfies, math.IsNaN)
+}
+
+// Test inverse trigonometric functions
+
+func TestAsin(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+ ns := New(nil)
+
+ for _, test := range []struct {
+ x any
+ expect any
+ }{
+ {0.0, 0.0},
+ {1.0, 1.5707},
+ {-1.0, -1.5707},
+ {0.5, 0.5235},
+ {"abc", false},
+ } {
+ result, err := ns.Asin(test.x)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+
+ // Separate test for Asin(2) -- returns NaN
+ result, err := ns.Asin(2)
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Satisfies, math.IsNaN)
+}
+
+func TestAcos(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+ ns := New(nil)
+
+ for _, test := range []struct {
+ x any
+ expect any
+ }{
+ {1.0, 0.0},
+ {0.0, 1.5707},
+ {-1.0, 3.1415},
+ {0.5, 1.0471},
+ {"abc", false},
+ } {
+ result, err := ns.Acos(test.x)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+
+ // Separate test for Acos(2) -- returns NaN
+ result, err := ns.Acos(2)
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Satisfies, math.IsNaN)
+}
+
+func TestAtan(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+ ns := New(nil)
+
+ for _, test := range []struct {
+ x any
+ expect any
+ }{
+ {0.0, 0.0},
+ {1, 0.7853},
+ {-1.0, -0.7853},
+ {math.Inf(1), 1.5707},
+ {"abc", false},
+ } {
+ result, err := ns.Atan(test.x)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+}
+
+func TestAtan2(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+ ns := New(nil)
+
+ for _, test := range []struct {
+ x any
+ y any
+ expect any
+ }{
+ {1.0, 1.0, 0.7853},
+ {-1.0, 1.0, -0.7853},
+ {1.0, -1.0, 2.3561},
+ {-1.0, -1.0, -2.3561},
+ {1, 0, 1.5707},
+ {-1, 0, -1.5707},
+ {0, 1, 0.0},
+ {0, -1, 3.1415},
+ {0.0, 0.0, 0.0},
+ {"abc", "def", false},
+ } {
+ result, err := ns.Atan2(test.x, test.y)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+}
+
+// Test angle helper functions
+
+func TestToDegrees(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+ ns := New(nil)
+
+ for _, test := range []struct {
+ x any
+ expect any
+ }{
+ {0.0, 0.0},
+ {1, 57.2957},
+ {math.Pi / 2, 90.0},
+ {math.Pi, 180.0},
+ {"abc", false},
+ } {
+ result, err := ns.ToDegrees(test.x)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+}
+
+func TestToRadians(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+ ns := New(nil)
+
+ for _, test := range []struct {
+ x any
+ expect any
+ }{
+ {0, 0.0},
+ {57.29577951308232, 1.0},
+ {90, 1.5707},
+ {180.0, 3.1415},
+ {"abc", false},
+ } {
+ result, err := ns.ToRadians(test.x)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ result = float64(int(result*10000)) / 10000
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+}
+
+func TestMaxInt64(t *testing.T) {
+ t.Parallel()
+ ns := New(nil)
+
+ var want int64 = 9223372036854775807
+ got := ns.MaxInt64()
+ if want != got {
+ t.Errorf("want %d, got %d", want, got)
+ }
+}
diff --git a/tpl/page/init.go b/tpl/page/init.go
index 826aa45d3..106552630 100644
--- a/tpl/page/init.go
+++ b/tpl/page/init.go
@@ -31,7 +31,7 @@ func init() {
f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
ns := &internal.TemplateFuncsNamespace{
Name: name,
- Context: func(ctx context.Context, args ...interface{}) (interface{}, error) {
+ Context: func(ctx context.Context, args ...any) (any, error) {
v := tpl.Context.Page.Get(ctx)
if v == nil {
// The multilingual sitemap does not have a page as its context.
diff --git a/tpl/page/page_integration_test.go b/tpl/page/page_integration_test.go
index 22f6323dd..0f48ecd28 100644
--- a/tpl/page/page_integration_test.go
+++ b/tpl/page/page_integration_test.go
@@ -29,8 +29,9 @@ func TestThatPageIsAvailableEverywhere(t *testing.T) {
baseURL = 'http://example.com/'
disableKinds = ["taxonomy", "term"]
enableInlineShortcodes = true
-paginate = 1
enableRobotsTXT = true
+[pagination]
+pagerSize = 1
LANG_CONFIG
-- content/_index.md --
---
@@ -191,7 +192,7 @@ title: "P1"
# Heading 1
-- layouts/shortcodes/toc.html --
-{{ page.TableOfContents }}
+{{ page.TableOfContents }}
-- layouts/_default/single.html --
{{ .Content }}
`
diff --git a/tpl/partials/init.go b/tpl/partials/init.go
index e9d901bbf..6f7c0ffc1 100644
--- a/tpl/partials/init.go
+++ b/tpl/partials/init.go
@@ -38,7 +38,7 @@ func init() {
},
)
- // TODO(bep) we need the return to be a valid identifier, but
+ // TODO(bep) we need the return to be a valid identifiers, but
// should consider another way of adding it.
ns.AddMethodMapping(func() string { return "" },
[]string{"return"},
diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go
index 8e36e21b9..57a2aa280 100644
--- a/tpl/partials/partials.go
+++ b/tpl/partials/partials.go
@@ -24,12 +24,13 @@ import (
"time"
"github.com/bep/lazycache"
-
+ "github.com/gohugoio/hugo/common/constants"
+ "github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/identity"
-
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
"github.com/gohugoio/hugo/tpl"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/deps"
@@ -50,14 +51,7 @@ func (k partialCacheKey) Key() string {
if k.Variants == nil {
return k.Name
}
- return identity.HashString(append([]any{k.Name}, k.Variants...)...)
-}
-
-func (k partialCacheKey) templateName() string {
- if !strings.HasPrefix(k.Name, "partials/") {
- return "partials/" + k.Name
- }
- return k.Name
+ return hashing.HashString(append([]any{k.Name}, k.Variants...)...)
}
// partialCache represents a LRU cache of partials.
@@ -80,8 +74,9 @@ func New(deps *deps.Deps) *Namespace {
cache := &partialCache{cache: lru}
deps.BuildStartListeners.Add(
- func() {
+ func(...any) bool {
cache.clear()
+ return false
})
return &Namespace{
@@ -114,7 +109,7 @@ func (c *contextWrapper) Set(in any) string {
// A string if the partial is a text/template, or template.HTML when html/template.
// Note that ctx is provided by Hugo, not the end user.
func (ns *Namespace) Include(ctx context.Context, name string, contextList ...any) (any, error) {
- res := ns.includWithTimeout(ctx, name, contextList...)
+ res := ns.include(ctx, name, contextList...)
if res.err != nil {
return nil, res.err
}
@@ -126,59 +121,36 @@ func (ns *Namespace) Include(ctx context.Context, name string, contextList ...an
return res.result, nil
}
-func (ns *Namespace) includWithTimeout(ctx context.Context, name string, dataList ...any) includeResult {
- // Create a new context with a timeout not connected to the incoming context.
- timeoutCtx, cancel := context.WithTimeout(context.Background(), ns.deps.Conf.Timeout())
- defer cancel()
-
- res := make(chan includeResult, 1)
-
- go func() {
- res <- ns.include(ctx, name, dataList...)
- }()
-
- select {
- case r := <-res:
- return r
- case <-timeoutCtx.Done():
- err := timeoutCtx.Err()
- if err == context.DeadlineExceeded {
- //lint:ignore ST1005 end user message.
- err = fmt.Errorf("partial %q timed out after %s. This is most likely due to infinite recursion. If this is just a slow template, you can try to increase the 'timeout' config setting.", name, ns.deps.Conf.Timeout())
- }
+func (ns *Namespace) include(ctx context.Context, name string, dataList ...any) includeResult {
+ v, err := ns.lookup(name)
+ if err != nil {
return includeResult{err: err}
}
+ return ns.doInclude(ctx, v, dataList...)
+}
+
+func (ns *Namespace) lookup(name string) (*tplimpl.TemplInfo, error) {
+ if strings.HasPrefix(name, "partials/") {
+ // This is most likely not what the user intended.
+ // This worked before Hugo 0.146.0.
+ ns.deps.Log.Warnidf(constants.WarnPartialSuperfluousPrefix, "Doubtful use of partial function in {{ partial \"%s\"}}), this is most likely not what you want. Consider removing superfluous prefix \"partials/\" from template name given as first function argument.", name)
+ }
+ v := ns.deps.TemplateStore.LookupPartial(name)
+ if v == nil {
+ return nil, fmt.Errorf("partial %q not found", name)
+ }
+ return v, nil
}
// include is a helper function that lookups and executes the named partial.
// Returns the final template name and the rendered output.
-func (ns *Namespace) include(ctx context.Context, name string, dataList ...any) includeResult {
+func (ns *Namespace) doInclude(ctx context.Context, templ *tplimpl.TemplInfo, dataList ...any) includeResult {
var data any
if len(dataList) > 0 {
data = dataList[0]
}
- var n string
- if strings.HasPrefix(name, "partials/") {
- n = name
- } else {
- n = "partials/" + name
- }
-
- templ, found := ns.deps.Tmpl().Lookup(n)
- if !found {
- // For legacy reasons.
- templ, found = ns.deps.Tmpl().Lookup(n + ".html")
- }
-
- if !found {
- return includeResult{err: fmt.Errorf("partial %q not found", name)}
- }
-
- var info tpl.ParseInfo
- if ip, ok := templ.(tpl.Info); ok {
- info = ip.ParseInfo()
- }
+ info := templ.ParseInfo
var w io.Writer
@@ -198,7 +170,7 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
w = b
}
- if err := ns.deps.Tmpl().ExecuteWithContext(ctx, templ, w, data); err != nil {
+ if err := ns.deps.GetTemplateStore().ExecuteWithContext(ctx, templ, w, data); err != nil {
return includeResult{err: err}
}
@@ -206,7 +178,7 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
if ctx, ok := data.(*contextWrapper); ok {
result = ctx.Result
- } else if _, ok := templ.(*texttemplate.Template); ok {
+ } else if _, ok := templ.Template.(*texttemplate.Template); ok {
result = w.(fmt.Stringer).String()
} else {
result = template.HTML(w.(fmt.Stringer).String())
@@ -227,6 +199,20 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any
Variants: variants,
}
depsManagerIn := tpl.Context.GetDependencyManagerInCurrentScope(ctx)
+ ti, err := ns.lookup(name)
+ if err != nil {
+ return nil, err
+ }
+
+ if parent := tpl.Context.CurrentTemplate.Get(ctx); parent != nil {
+ for parent != nil {
+ if parent.CurrentTemplateInfoOps == ti {
+ // This will deadlock if we continue.
+ return nil, fmt.Errorf("circular call stack detected in partial %q", ti.Filename())
+ }
+ parent = parent.Parent
+ }
+ }
r, found, err := ns.cachedPartials.cache.GetOrCreate(key.Key(), func(string) (includeResult, error) {
var depsManagerShared identity.Manager
@@ -236,7 +222,7 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any
depsManagerShared = identity.NewManager("partials")
ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, depsManagerShared.(identity.DependencyManagerScopedProvider))
}
- r := ns.includWithTimeout(ctx, key.Name, context)
+ r := ns.doInclude(ctx, ti, context)
if ns.deps.Conf.Watching() {
r.mangager = depsManagerShared
}
@@ -251,9 +237,9 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any
// The templates that gets executed is measured in Execute.
// We need to track the time spent in the cache to
// get the totals correct.
- ns.deps.Metrics.MeasureSince(key.templateName(), start)
+ ns.deps.Metrics.MeasureSince(r.name, start)
}
- ns.deps.Metrics.TrackValue(key.templateName(), r.result, found)
+ ns.deps.Metrics.TrackValue(r.name, r.result, found)
}
if r.mangager != nil && depsManagerIn != nil {
diff --git a/tpl/partials/partials_integration_test.go b/tpl/partials/partials_integration_test.go
index f2bde29c3..0fa47104d 100644
--- a/tpl/partials/partials_integration_test.go
+++ b/tpl/partials/partials_integration_test.go
@@ -170,7 +170,7 @@ D1
got := buf.String()
// Get rid of all the durations, they are never the same.
- durationRe := regexp.MustCompile(`\b[\.\d]*(ms|µs|s)\b`)
+ durationRe := regexp.MustCompile(`\b[\.\d]*(ms|ns|µs|s)\b`)
normalize := func(s string) string {
s = durationRe.ReplaceAllString(s, "")
@@ -193,10 +193,10 @@ D1
expect := `
0 0 0 1 index.html
- 100 0 0 1 partials/static2.html
- 100 50 1 2 partials/static1.html
- 25 50 2 4 partials/dynamic1.html
- 66 33 1 3 partials/halfdynamic1.html
+ 100 0 0 1 _partials/static2.html
+ 100 50 1 2 _partials/static1.html
+ 25 50 2 4 _partials/dynamic1.html
+ 66 33 1 3 _partials/halfdynamic1.html
`
b.Assert(got, hqt.IsSameString, expect)
@@ -256,7 +256,6 @@ func TestIncludeTimeout(t *testing.T) {
files := `
-- config.toml --
baseURL = 'http://example.com/'
-timeout = '200ms'
-- layouts/index.html --
{{ partials.Include "foo.html" . }}
-- layouts/partials/foo.html --
@@ -271,7 +270,7 @@ timeout = '200ms'
).BuildE()
b.Assert(err, qt.Not(qt.IsNil))
- b.Assert(err.Error(), qt.Contains, "timed out")
+ b.Assert(err.Error(), qt.Contains, "maximum template call stack size exceeded")
}
func TestIncludeCachedTimeout(t *testing.T) {
@@ -284,6 +283,8 @@ timeout = '200ms'
-- layouts/index.html --
{{ partials.IncludeCached "foo.html" . }}
-- layouts/partials/foo.html --
+{{ partialCached "bar.html" . }}
+-- layouts/partials/bar.html --
{{ partialCached "foo.html" . }}
`
@@ -295,7 +296,7 @@ timeout = '200ms'
).BuildE()
b.Assert(err, qt.Not(qt.IsNil))
- b.Assert(err.Error(), qt.Contains, "timed out")
+ b.Assert(err.Error(), qt.Contains, `error calling partialCached: circular call stack detected in partial`)
}
// See Issue #10789
diff --git a/tpl/reflect/reflect.go b/tpl/reflect/reflect.go
index 07834be1c..c19c8c178 100644
--- a/tpl/reflect/reflect.go
+++ b/tpl/reflect/reflect.go
@@ -14,7 +14,7 @@
package reflect
import (
- "reflect"
+ "github.com/gohugoio/hugo/common/hreflect"
)
// New returns a new instance of the reflect-namespaced template functions.
@@ -27,10 +27,10 @@ type Namespace struct{}
// IsMap reports whether v is a map.
func (ns *Namespace) IsMap(v any) bool {
- return reflect.ValueOf(v).Kind() == reflect.Map
+ return hreflect.IsMap(v)
}
// IsSlice reports whether v is a slice.
func (ns *Namespace) IsSlice(v any) bool {
- return reflect.ValueOf(v).Kind() == reflect.Slice
+ return hreflect.IsSlice(v)
}
diff --git a/tpl/resources/init.go b/tpl/resources/init.go
index db51b0287..5cea482ac 100644
--- a/tpl/resources/init.go
+++ b/tpl/resources/init.go
@@ -17,7 +17,9 @@ import (
"context"
"github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/tpl/css"
"github.com/gohugoio/hugo/tpl/internal"
+ "github.com/gohugoio/hugo/tpl/js"
)
const name = "resources"
@@ -33,6 +35,22 @@ func init() {
ns := &internal.TemplateFuncsNamespace{
Name: name,
Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
+ OnCreated: func(m map[string]any) {
+ for _, v := range m {
+ switch v := v.(type) {
+ case *css.Namespace:
+ ctx.cssNs = v
+ case *js.Namespace:
+ ctx.jsNs = v
+ }
+ }
+ if ctx.cssNs == nil {
+ panic("css namespace not found")
+ }
+ if ctx.jsNs == nil {
+ panic("js namespace not found")
+ }
+ },
}
ns.AddMethodMapping(ctx.Get,
@@ -57,21 +75,6 @@ func init() {
[][2]string{},
)
- ns.AddMethodMapping(ctx.ToCSS,
- []string{"toCSS"},
- [][2]string{},
- )
-
- ns.AddMethodMapping(ctx.PostCSS,
- []string{"postCSS"},
- [][2]string{},
- )
-
- ns.AddMethodMapping(ctx.Babel,
- []string{"babel"},
- [][2]string{},
- )
-
return ns
}
diff --git a/tpl/resources/resources.go b/tpl/resources/resources.go
index 04af756ef..f28cc36fe 100644
--- a/tpl/resources/resources.go
+++ b/tpl/resources/resources.go
@@ -18,12 +18,12 @@ import (
"context"
"errors"
"fmt"
- "sync"
+ "github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/maps"
- "github.com/gohugoio/hugo/common/paths"
- "github.com/gohugoio/hugo/tpl/internal/resourcehelpers"
+ "github.com/gohugoio/hugo/tpl/css"
+ "github.com/gohugoio/hugo/tpl/js"
"github.com/gohugoio/hugo/resources/postpub"
@@ -33,13 +33,9 @@ import (
"github.com/gohugoio/hugo/resources/resource_factories/bundler"
"github.com/gohugoio/hugo/resources/resource_factories/create"
- "github.com/gohugoio/hugo/resources/resource_transformers/babel"
"github.com/gohugoio/hugo/resources/resource_transformers/integrity"
"github.com/gohugoio/hugo/resources/resource_transformers/minifier"
- "github.com/gohugoio/hugo/resources/resource_transformers/postcss"
"github.com/gohugoio/hugo/resources/resource_transformers/templates"
- "github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass"
- "github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss"
"github.com/spf13/cast"
)
@@ -50,26 +46,18 @@ func New(deps *deps.Deps) (*Namespace, error) {
return &Namespace{}, nil
}
- scssClient, err := scss.New(deps.BaseFs.Assets, deps.ResourceSpec)
- if err != nil {
- return nil, err
- }
-
minifyClient, err := minifier.New(deps.ResourceSpec)
if err != nil {
return nil, err
}
return &Namespace{
- deps: deps,
- scssClientLibSass: scssClient,
- createClient: create.New(deps.ResourceSpec),
- bundlerClient: bundler.New(deps.ResourceSpec),
- integrityClient: integrity.New(deps.ResourceSpec),
- minifyClient: minifyClient,
- postcssClient: postcss.New(deps.ResourceSpec),
- templatesClient: templates.New(deps.ResourceSpec, deps),
- babelClient: babel.New(deps.ResourceSpec),
+ deps: deps,
+ createClient: create.New(deps.ResourceSpec),
+ bundlerClient: bundler.New(deps.ResourceSpec),
+ integrityClient: integrity.New(deps.ResourceSpec),
+ minifyClient: minifyClient,
+ templatesClient: templates.New(deps.ResourceSpec, deps),
}, nil
}
@@ -79,33 +67,16 @@ var _ resource.ResourceFinder = (*Namespace)(nil)
type Namespace struct {
deps *deps.Deps
- createClient *create.Client
- bundlerClient *bundler.Client
- scssClientLibSass *scss.Client
- integrityClient *integrity.Client
- minifyClient *minifier.Client
- postcssClient *postcss.Client
- babelClient *babel.Client
- templatesClient *templates.Client
+ createClient *create.Client
+ bundlerClient *bundler.Client
+ integrityClient *integrity.Client
+ minifyClient *minifier.Client
+ templatesClient *templates.Client
- // The Dart Client requires a os/exec process, so only
- // create it if we really need it.
- // This is mostly to avoid creating one per site build test.
- scssClientDartSassInit sync.Once
- scssClientDartSass *dartsass.Client
-}
-
-func (ns *Namespace) getscssClientDartSass() (*dartsass.Client, error) {
- var err error
- ns.scssClientDartSassInit.Do(func() {
- ns.scssClientDartSass, err = dartsass.New(ns.deps.BaseFs.Assets, ns.deps.ResourceSpec)
- if err != nil {
- return
- }
- ns.deps.BuildClosers.Add(ns.scssClientDartSass)
- })
-
- return ns.scssClientDartSass, err
+ // We moved some CSS and JS related functions to the css and js package in Hugo 0.128.0.
+ // Keep this here until the deprecation period is over.
+ cssNs *css.Namespace
+ jsNs *js.Namespace
}
// Copy copies r to the new targetPath in s.
@@ -144,14 +115,10 @@ func (ns *Namespace) Get(filename any) resource.Resource {
//
// Note: This method does not return any error as a second return value,
// for any error situations the error can be checked in .Err.
-func (ns *Namespace) GetRemote(args ...any) resource.Resource {
+func (ns *Namespace) GetRemote(args ...any) (resource.Resource, error) {
get := func(args ...any) (resource.Resource, error) {
- if len(args) < 1 {
- return nil, errors.New("must provide an URL")
- }
-
- if len(args) > 2 {
- return nil, errors.New("must not provide more arguments than URL and options")
+ if len(args) < 1 || len(args) > 2 {
+ return nil, errors.New("must provide an URL and optionally an options map")
}
urlstr, err := cast.ToStringE(args[0])
@@ -175,12 +142,12 @@ func (ns *Namespace) GetRemote(args ...any) resource.Resource {
if err != nil {
switch v := err.(type) {
case *create.HTTPError:
- return resources.NewErrorResource(resource.NewResourceError(v, v.Data))
+ return nil, resource.NewResourceError(v, v.Data)
default:
- return resources.NewErrorResource(resource.NewResourceError(fmt.Errorf("error calling resources.GetRemote: %w", err), make(map[string]any)))
+ return nil, resource.NewResourceError(err, nil)
}
}
- return r
+ return r, nil
}
// GetMatch finds the first Resource matching the given pattern, or nil if none found.
@@ -253,7 +220,7 @@ func (ns *Namespace) Concat(targetPathIn any, r any) (resource.Resource, error)
case resource.ResourcesConverter:
rr = v.ToResources()
default:
- return nil, fmt.Errorf("slice %T not supported in concat", r)
+ return nil, fmt.Errorf("expected slice of Resource objects, received %T instead", r)
}
if len(rr) == 0 {
@@ -337,89 +304,17 @@ func (ns *Namespace) Minify(r resources.ResourceTransformer) (resource.Resource,
// ToCSS converts the given Resource to CSS. You can optional provide an Options object
// as second argument. As an option, you can e.g. specify e.g. the target path (string)
// for the converted CSS resource.
+// Deprecated: Moved to the css namespace in Hugo 0.128.0.
func (ns *Namespace) ToCSS(args ...any) (resource.Resource, error) {
- if len(args) > 2 {
- return nil, errors.New("must not provide more arguments than resource object and options")
- }
-
- const (
- // Transpiler implementation can be controlled from the client by
- // setting the 'transpiler' option.
- // Default is currently 'libsass', but that may change.
- transpilerDart = "dartsass"
- transpilerLibSass = "libsass"
- )
-
- var (
- r resources.ResourceTransformer
- m map[string]any
- targetPath string
- err error
- ok bool
- transpiler = transpilerLibSass
- )
-
- r, targetPath, ok = resourcehelpers.ResolveIfFirstArgIsString(args)
-
- if !ok {
- r, m, err = resourcehelpers.ResolveArgs(args)
- if err != nil {
- return nil, err
- }
- }
-
- if m != nil {
- if t, found := maps.LookupEqualFold(m, "transpiler"); found {
- switch t {
- case transpilerDart, transpilerLibSass:
- transpiler = cast.ToString(t)
- default:
- return nil, fmt.Errorf("unsupported transpiler %q; valid values are %q or %q", t, transpilerLibSass, transpilerDart)
- }
- }
- }
-
- if transpiler == transpilerLibSass {
- var options scss.Options
- if targetPath != "" {
- options.TargetPath = paths.ToSlashTrimLeading(targetPath)
- } else if m != nil {
- options, err = scss.DecodeOptions(m)
- if err != nil {
- return nil, err
- }
- }
-
- return ns.scssClientLibSass.ToCSS(r, options)
- }
-
- if m == nil {
- m = make(map[string]any)
- }
- if targetPath != "" {
- m["targetPath"] = targetPath
- }
-
- client, err := ns.getscssClientDartSass()
- if err != nil {
- return nil, err
- }
-
- return client.ToCSS(r, m)
+ hugo.Deprecate("resources.ToCSS", "Use css.Sass instead.", "v0.128.0")
+ return ns.cssNs.Sass(args...)
}
-// PostCSS processes the given Resource with PostCSS
+// PostCSS processes the given Resource with PostCSS.
+// Deprecated: Moved to the css namespace in Hugo 0.128.0.
func (ns *Namespace) PostCSS(args ...any) (resource.Resource, error) {
- if len(args) > 2 {
- return nil, errors.New("must not provide more arguments than resource object and options")
- }
-
- r, m, err := resourcehelpers.ResolveArgs(args)
- if err != nil {
- return nil, err
- }
-
- return ns.postcssClient.Process(r, m)
+ hugo.Deprecate("resources.PostCSS", "Use css.PostCSS instead.", "v0.128.0")
+ return ns.cssNs.PostCSS(args...)
}
// PostProcess processes r after the build.
@@ -428,23 +323,8 @@ func (ns *Namespace) PostProcess(r resource.Resource) (postpub.PostPublishedReso
}
// Babel processes the given Resource with Babel.
+// Deprecated: Moved to the js namespace in Hugo 0.128.0.
func (ns *Namespace) Babel(args ...any) (resource.Resource, error) {
- if len(args) > 2 {
- return nil, errors.New("must not provide more arguments than resource object and options")
- }
-
- r, m, err := resourcehelpers.ResolveArgs(args)
- if err != nil {
- return nil, err
- }
- var options babel.Options
- if m != nil {
- options, err = babel.DecodeOptions(m)
-
- if err != nil {
- return nil, err
- }
- }
-
- return ns.babelClient.Process(r, options)
+ hugo.Deprecate("resources.Babel", "Use js.Babel.", "v0.128.0")
+ return ns.jsNs.Babel(args...)
}
diff --git a/tpl/resources/resources_integration_test.go b/tpl/resources/resources_integration_test.go
index 6bc872bca..da3ff1168 100644
--- a/tpl/resources/resources_integration_test.go
+++ b/tpl/resources/resources_integration_test.go
@@ -18,6 +18,8 @@ import (
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass"
+ "github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss"
)
func TestCopy(t *testing.T) {
@@ -58,7 +60,7 @@ Copy3: {{ $copy3.RelPermalink}}|{{ $copy3.MediaType }}|{{ $copy3.Content | safeJ
b.AssertFileContent("public/index.html", `
Image Orig: /blog/images/pixel.png|image/png|1|1|
-Image Copy1: /blog/images/copy_hu8aa3346827e49d756ff4e630147c42b5_70_3x4_resize_box_3.png|image/png|3|4|
+Image Copy1: /blog/images/copy_hu_1d9addfff177f388.png|image/png|3|4|
Image Copy2: /blog/images/copy2.png|image/png|3|4
Image Copy3: image/png|3|4|
Orig: /blog/js/foo.js|text/javascript|let foo;|
@@ -238,3 +240,45 @@ match /files/C*: 2|
b.AssertFileContent("public/files/b.txt", "I am b.txt")
b.AssertFileContent("public/files/C.txt", "I am C.txt")
}
+
+// Issue #12961
+func TestDartSassVars(t *testing.T) {
+ t.Parallel()
+
+ if !scss.Supports() || !dartsass.Supports() {
+ t.Skip()
+ }
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','section','rss','sitemap','taxonomy','term']
+-- layouts/index.html --
+{{ $opts := dict "transpiler" "dartsass" "outputStyle" "compressed" "vars" (dict "color" "red") }}
+{{ with resources.Get "dartsass.scss" | css.Sass $opts }}
+ {{ .Content }}
+{{ end }}
+
+{{ $opts := dict "transpiler" "libsass" "outputStyle" "compressed" "vars" (dict "color" "blue") }}
+{{ with resources.Get "libsass.scss" | css.Sass $opts }}
+ {{ .Content }}
+{{ end }}
+-- assets/dartsass.scss --
+@use "hugo:vars" as v;
+.dartsass {
+ color: v.$color;
+}
+-- assets/libsass.scss --
+@import "hugo:vars";
+.libsass {
+ color: $color;
+}
+`
+
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+ b.AssertFileContent("public/index.html",
+ ".dartsass{color:red}",
+ ".libsass{color:blue}",
+ )
+ b.AssertLogContains("! WARN Dart Sass: hugo:vars")
+}
diff --git a/tpl/safe/init.go b/tpl/safe/init.go
index 3b498e6df..ea1469e80 100644
--- a/tpl/safe/init.go
+++ b/tpl/safe/init.go
@@ -70,6 +70,13 @@ func init() {
},
)
+ ns.AddMethodMapping(func(v any) (any, error) {
+ return v, nil
+ },
+ []string{"try"},
+ [][2]string{},
+ )
+
return ns
}
diff --git a/tpl/strings/strings.go b/tpl/strings/strings.go
index 02f9a2b1e..eb5aee3cb 100644
--- a/tpl/strings/strings.go
+++ b/tpl/strings/strings.go
@@ -450,6 +450,17 @@ func (ns *Namespace) Trim(s, cutset any) (string, error) {
return strings.Trim(ss, sc), nil
}
+// TrimSpace returns the given string, removing leading and trailing whitespace
+// as defined by Unicode.
+func (ns *Namespace) TrimSpace(s any) (string, error) {
+ ss, err := cast.ToStringE(s)
+ if err != nil {
+ return "", err
+ }
+
+ return strings.TrimSpace(ss), nil
+}
+
// TrimLeft returns a slice of the string s with all leading characters
// contained in cutset removed.
func (ns *Namespace) TrimLeft(cutset, s any) (string, error) {
diff --git a/tpl/strings/strings_test.go b/tpl/strings/strings_test.go
index 4fcd3b59a..0e6039ba1 100644
--- a/tpl/strings/strings_test.go
+++ b/tpl/strings/strings_test.go
@@ -854,3 +854,30 @@ func TestDiff(t *testing.T) {
}
}
+
+func TestTrimSpace(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+
+ for _, test := range []struct {
+ s any
+ expect any
+ }{
+ {"\n\r test \n\r", "test"},
+ {template.HTML("\n\r test \n\r"), "test"},
+ {[]byte("\n\r test \n\r"), "test"},
+ // errors
+ {tstNoStringer{}, false},
+ } {
+
+ result, err := ns.TrimSpace(test.s)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+}
diff --git a/tpl/template.go b/tpl/template.go
index 5ef0eecb8..877422123 100644
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
+// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -16,161 +16,58 @@ package tpl
import (
"context"
- "io"
- "reflect"
- "regexp"
+ "slices"
"strings"
+ "sync"
"unicode"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/common/hcontext"
"github.com/gohugoio/hugo/identity"
- "github.com/gohugoio/hugo/output/layouts"
-
- "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/langs"
htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
)
-// TemplateManager manages the collection of templates.
-type TemplateManager interface {
- TemplateHandler
- TemplateFuncGetter
- AddTemplate(name, tpl string) error
- MarkReady() error
-}
-
-// TemplateVariants describes the possible variants of a template.
-// All of these may be empty.
-type TemplateVariants struct {
- Language string
- OutputFormat output.Format
-}
-
-// TemplateFinder finds templates.
-type TemplateFinder interface {
- TemplateLookup
- TemplateLookupVariant
-}
-
-// UnusedTemplatesProvider lists unused templates if the build is configured to track those.
-type UnusedTemplatesProvider interface {
- UnusedTemplates() []FileInfo
-}
-
-// TemplateHandlers holds the templates needed by Hugo.
-type TemplateHandlers struct {
- Tmpl TemplateHandler
- TxtTmpl TemplateParseFinder
-}
-
-// TemplateHandler finds and executes templates.
-type TemplateHandler interface {
- TemplateFinder
- ExecuteWithContext(ctx context.Context, t Template, wr io.Writer, data any) error
- LookupLayout(d layouts.LayoutDescriptor, f output.Format) (Template, bool, error)
- HasTemplate(name string) bool
- GetIdentity(name string) (identity.Identity, bool)
-}
-
-type TemplateLookup interface {
- Lookup(name string) (Template, bool)
-}
-
-type TemplateLookupVariant interface {
- // TODO(bep) this currently only works for shortcodes.
- // We may unify and expand this variant pattern to the
- // other templates, but we need this now for the shortcodes to
- // quickly determine if a shortcode has a template for a given
- // output format.
- // It returns the template, if it was found or not and if there are
- // alternative representations (output format, language).
- // We are currently only interested in output formats, so we should improve
- // this for speed.
- LookupVariant(name string, variants TemplateVariants) (Template, bool, bool)
- LookupVariants(name string) []Template
-}
-
// Template is the common interface between text/template and html/template.
type Template interface {
Name() string
Prepare() (*texttemplate.Template, error)
}
-// AddIdentity checks if t is an identity.Identity and returns it if so.
-// Else it wraps it in a templateIdentity using its name as the base.
-func AddIdentity(t Template) Template {
- if _, ok := t.(identity.IdentityProvider); ok {
- return t
- }
- return templateIdentityProvider{
- Template: t,
- id: identity.StringIdentity(t.Name()),
- }
+// RenderingContext represents the currently rendered site/language.
+type RenderingContext struct {
+ Site site
+ SiteOutIdx int
}
-type templateIdentityProvider struct {
- Template
- id identity.Identity
-}
+type (
+ contextKey uint8
+)
-func (t templateIdentityProvider) GetIdentity() identity.Identity {
- return t.id
-}
-
-// TemplateParser is used to parse ad-hoc templates, e.g. in the Resource chain.
-type TemplateParser interface {
- Parse(name, tpl string) (Template, error)
-}
-
-// TemplateParseFinder provides both parsing and finding.
-type TemplateParseFinder interface {
- TemplateParser
- TemplateFinder
-}
-
-// TemplateDebugger prints some debug info to stdout.
-type TemplateDebugger interface {
- Debug()
-}
-
-// TemplatesProvider as implemented by deps.Deps.
-type TemplatesProvider interface {
- Tmpl() TemplateHandler
- TextTmpl() TemplateParseFinder
-}
-
-var baseOfRe = regexp.MustCompile("template: (.*?):")
-
-func extractBaseOf(err string) string {
- m := baseOfRe.FindStringSubmatch(err)
- if len(m) == 2 {
- return m[1]
- }
- return ""
-}
-
-// TemplateFuncGetter allows to find a template func by name.
-type TemplateFuncGetter interface {
- GetFunc(name string) (reflect.Value, bool)
-}
-
-type contextKey string
+const (
+ contextKeyDependencyManagerScopedProvider contextKey = iota
+ contextKeyDependencyScope
+ contextKeyPage
+ contextKeyIsInGoldmark
+ cntextKeyCurrentTemplateInfo
+)
// Context manages values passed in the context to templates.
var Context = struct {
DependencyManagerScopedProvider hcontext.ContextDispatcher[identity.DependencyManagerScopedProvider]
GetDependencyManagerInCurrentScope func(context.Context) identity.Manager
- SetDependencyManagerInCurrentScope func(context.Context, identity.Manager) context.Context
DependencyScope hcontext.ContextDispatcher[int]
Page hcontext.ContextDispatcher[page]
IsInGoldmark hcontext.ContextDispatcher[bool]
+ CurrentTemplate hcontext.ContextDispatcher[*CurrentTemplateInfo]
}{
- DependencyManagerScopedProvider: hcontext.NewContextDispatcher[identity.DependencyManagerScopedProvider](contextKey("DependencyManagerScopedProvider")),
- DependencyScope: hcontext.NewContextDispatcher[int](contextKey("DependencyScope")),
- Page: hcontext.NewContextDispatcher[page](contextKey("Page")),
- IsInGoldmark: hcontext.NewContextDispatcher[bool](contextKey("IsInGoldmark")),
+ DependencyManagerScopedProvider: hcontext.NewContextDispatcher[identity.DependencyManagerScopedProvider](contextKeyDependencyManagerScopedProvider),
+ DependencyScope: hcontext.NewContextDispatcher[int](contextKeyDependencyScope),
+ Page: hcontext.NewContextDispatcher[page](contextKeyPage),
+ IsInGoldmark: hcontext.NewContextDispatcher[bool](contextKeyIsInGoldmark),
+ CurrentTemplate: hcontext.NewContextDispatcher[*CurrentTemplateInfo](cntextKeyCurrentTemplateInfo),
}
func init() {
@@ -187,6 +84,17 @@ type page interface {
IsNode() bool
}
+type site interface {
+ Language() *langs.Language
+}
+
+const (
+ // HugoDeferredTemplatePrefix is the prefix for placeholders for deferred templates.
+ HugoDeferredTemplatePrefix = "__hdeferred/"
+ // HugoDeferredTemplateSuffix is the suffix for placeholders for deferred templates.
+ HugoDeferredTemplateSuffix = "__d="
+)
+
const hugoNewLinePlaceholder = "___hugonl_"
var stripHTMLReplacerPre = strings.NewReplacer("\n", " ", "", hugoNewLinePlaceholder, " ", hugoNewLinePlaceholder, " ", hugoNewLinePlaceholder)
@@ -224,3 +132,58 @@ func StripHTML(s string) string {
return s
}
+
+// DeferredExecution holds the template and data for a deferred execution.
+type DeferredExecution struct {
+ Mu sync.Mutex
+ Ctx context.Context
+ TemplatePath string
+ Data any
+
+ Executed bool
+ Result string
+}
+
+type CurrentTemplateInfoOps interface {
+ CurrentTemplateInfoCommonOps
+ Base() CurrentTemplateInfoCommonOps
+}
+
+type CurrentTemplateInfoCommonOps interface {
+ // Template name.
+ Name() string
+ // Template source filename.
+ // Will be empty for internal templates.
+ Filename() string
+}
+
+// CurrentTemplateInfo as returned in templates.Current.
+type CurrentTemplateInfo struct {
+ Parent *CurrentTemplateInfo
+ Level int
+ CurrentTemplateInfoOps
+}
+
+// CurrentTemplateInfos is a slice of CurrentTemplateInfo.
+type CurrentTemplateInfos []*CurrentTemplateInfo
+
+// Reverse creates a copy of the slice and reverses it.
+func (c CurrentTemplateInfos) Reverse() CurrentTemplateInfos {
+ if len(c) == 0 {
+ return c
+ }
+ r := make(CurrentTemplateInfos, len(c))
+ copy(r, c)
+ slices.Reverse(r)
+ return r
+}
+
+// Ancestors returns the ancestors of the current template.
+func (ti *CurrentTemplateInfo) Ancestors() CurrentTemplateInfos {
+ var ancestors []*CurrentTemplateInfo
+ for ti.Parent != nil {
+ ti = ti.Parent
+ ancestors = append(ancestors, ti)
+ }
+ return ancestors
+}
diff --git a/tpl/template_test.go b/tpl/template_test.go
index 333513a3d..4c68f8132 100644
--- a/tpl/template_test.go
+++ b/tpl/template_test.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Hugo Authors. All rights reserved.
+// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -15,20 +15,8 @@ package tpl
import (
"testing"
-
- qt "github.com/frankban/quicktest"
)
-func TestExtractBaseof(t *testing.T) {
- c := qt.New(t)
-
- replaced := extractBaseOf(`failed: template: _default/baseof.html:37:11: executing "_default/baseof.html" at <.Parents>: can't evaluate field Parents in type *hugolib.PageOutput`)
-
- c.Assert(replaced, qt.Equals, "_default/baseof.html")
- c.Assert(extractBaseOf("not baseof for you"), qt.Equals, "")
- c.Assert(extractBaseOf("template: blog/baseof.html:23:11:"), qt.Equals, "blog/baseof.html")
-}
-
func TestStripHTML(t *testing.T) {
type test struct {
input, expected string
diff --git a/tpl/templates/defer_integration_test.go b/tpl/templates/defer_integration_test.go
new file mode 100644
index 000000000..f5ee09c06
--- /dev/null
+++ b/tpl/templates/defer_integration_test.go
@@ -0,0 +1,324 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package templates_test
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+
+ "github.com/gohugoio/hugo/hugolib"
+)
+
+const deferFilesCommon = `
+-- hugo.toml --
+disableLiveReload = true
+disableKinds = ["taxonomy", "term", "rss", "sitemap", "robotsTXT", "404", "section"]
+[languages]
+[languages.en]
+weight = 1
+[languages.nn]
+weight = 2
+-- i18n/en.toml --
+[hello]
+other = "Hello"
+-- i18n/nn.toml --
+[hello]
+other = "Hei"
+-- content/_index.en.md --
+---
+title: "Home"
+outputs: ["html", "amp"]
+---
+-- content/_index.nn.md --
+---
+title: "Heim"
+outputs: ["html", "amp"]
+---
+-- assets/mytext.txt --
+Hello.
+-- layouts/baseof.html --
+HTML|{{ block "main" . }}{{ end }}$
+-- layouts/index.html --
+{{ define "main" }}
+EDIT_COUNTER_OUTSIDE_0
+{{ .Store.Set "hello" "Hello" }}
+{{ $data := dict "page" . }}
+{{ with (templates.Defer (dict "data" $data) ) }}
+{{ $mytext := resources.Get "mytext.txt" }}
+REPLACE_ME|Title: {{ .page.Title }}|{{ .page.RelPermalink }}|Hello: {{ T "hello" }}|Hello Store: {{ .page.Store.Get "hello" }}|Mytext: {{ $mytext.Content }}|
+EDIT_COUNTER_DEFER_0
+{{ end }}$
+{{ end }}
+-- layouts/index.amp.html --
+AMP.
+{{ $data := dict "page" . }}
+{{ with (templates.Defer (dict "data" $data) ) }}Title AMP: {{ .page.Title }}|{{ .page.RelPermalink }}|Hello: {{ T "hello" }}{{ end }}$
+
+`
+
+func TestDeferNoBaseof(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/index.html --
+Home.
+{{ with (templates.Defer (dict "key" "foo")) }}
+ Defer
+{{ end }}
+-- content/_index.md --
+---
+title: "Home"
+---
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Home.\n\n Defer")
+}
+
+func TestDeferBaseof(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/baseof.html --
+{{ with (templates.Defer (dict "key" "foo")) }}
+Defer
+{{ end }}
+Block:{{ block "main" . }}{{ end }}$
+-- layouts/index.html --
+{{ define "main" }}
+Home.
+{{ end }}
+-- content/_index.md --
+---
+title: "Home"
+---
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Home.\n\n Defer")
+}
+
+func TestDeferMain(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/baseof.html --
+
+Block:{{ block "main" . }}{{ end }}$
+-- layouts/index.html --
+{{ define "main" }}
+Home.
+{{ with (templates.Defer (dict "key" "foo")) }}
+Defer
+{{ end }}
+{{ end }}
+-- content/_index.md --
+---
+title: "Home"
+---
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Home.\n\n Defer")
+}
+
+func TestDeferBasic(t *testing.T) {
+ t.Parallel()
+
+ b := hugolib.Test(t, deferFilesCommon)
+
+ b.AssertFileContent("public/index.html", "Title: Home|/|Hello: Hello|Hello Store: Hello|Mytext: Hello.|")
+ b.AssertFileContent("public/amp/index.html", "Title AMP: Home|/amp/|Hello: Hello")
+ b.AssertFileContent("public/nn/index.html", "Title: Heim|/nn/|Hello: Hei")
+ b.AssertFileContent("public/nn/amp/index.html", "Title AMP: Heim|/nn/amp/|Hello: Hei")
+}
+
+func TestDeferRepeatedBuildsEditOutside(t *testing.T) {
+ t.Parallel()
+
+ b := hugolib.TestRunning(t, deferFilesCommon)
+
+ for i := range 5 {
+ old := fmt.Sprintf("EDIT_COUNTER_OUTSIDE_%d", i)
+ new := fmt.Sprintf("EDIT_COUNTER_OUTSIDE_%d", i+1)
+ b.EditFileReplaceAll("layouts/index.html", old, new).Build()
+ b.AssertFileContent("public/index.html", new)
+ }
+}
+
+func TestDeferRepeatedBuildsEditDefer(t *testing.T) {
+ t.Parallel()
+
+ b := hugolib.TestRunning(t, deferFilesCommon)
+
+ for i := range 8 {
+ old := fmt.Sprintf("EDIT_COUNTER_DEFER_%d", i)
+ new := fmt.Sprintf("EDIT_COUNTER_DEFER_%d", i+1)
+ b.EditFileReplaceAll("layouts/index.html", old, new).Build()
+ b.AssertFileContent("public/index.html", new)
+ }
+}
+
+func TestDeferErrorParse(t *testing.T) {
+ t.Parallel()
+
+ b, err := hugolib.TestE(t, strings.ReplaceAll(deferFilesCommon, "Title AMP: {{ .page.Title }}", "{{ .page.Title }"))
+
+ b.Assert(err, qt.Not(qt.IsNil))
+ b.Assert(err.Error(), qt.Contains, `index.amp.html:3: unexpected "}" in operand`)
+}
+
+func TestDeferErrorRuntime(t *testing.T) {
+ t.Parallel()
+
+ b, err := hugolib.TestE(t, strings.ReplaceAll(deferFilesCommon, "Title AMP: {{ .page.Title }}", "{{ .page.Titles }}"))
+
+ b.Assert(err, qt.Not(qt.IsNil))
+ b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`/layouts/index.amp.html:3:57`))
+ b.Assert(err.Error(), qt.Contains, `execute of template failed: template: index.amp.html:3:57: executing at <.page.Titles>: can't evaluate field Titles`)
+}
+
+func TestDeferEditDeferBlock(t *testing.T) {
+ t.Parallel()
+
+ b := hugolib.TestRunning(t, deferFilesCommon)
+ b.AssertRenderCountPage(4)
+ b.EditFileReplaceAll("layouts/index.html", "REPLACE_ME", "Edited.").Build()
+ b.AssertFileContent("public/index.html", "Edited.")
+ b.AssertRenderCountPage(2)
+}
+
+//
+
+func TestDeferEditResourceUsedInDeferBlock(t *testing.T) {
+ t.Parallel()
+
+ b := hugolib.TestRunning(t, deferFilesCommon)
+ b.AssertRenderCountPage(4)
+ b.EditFiles("assets/mytext.txt", "Mytext Hello Edited.").Build()
+ b.AssertFileContent("public/index.html", "Mytext Hello Edited.")
+ b.AssertRenderCountPage(2)
+}
+
+func TestDeferMountPublic(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+[module]
+[[module.mounts]]
+source = "content"
+target = "content"
+[[module.mounts]]
+source = "layouts"
+target = "layouts"
+[[module.mounts]]
+source = 'public'
+target = 'assets/public'
+disableWatch = true
+-- layouts/index.html --
+Home.
+{{ $mydata := dict "v1" "v1value" }}
+{{ $json := resources.FromString "mydata/data.json" ($mydata | jsonify ) }}
+{{ $nop := $json.RelPermalink }}
+{{ with (templates.Defer (dict "key" "foo")) }}
+ {{ $jsonFilePublic := resources.Get "public/mydata/data.json" }}
+ {{ with $jsonFilePublic }}
+ {{ $m := $jsonFilePublic | transform.Unmarshal }}
+ v1: {{ $m.v1 }}
+ {{ end }}
+{{ end }}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "v1: v1value")
+}
+
+func TestDeferFromContentAdapterShouldFail(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- content/_content.gotmpl --
+{{ with (templates.Defer (dict "key" "foo")) }}
+ Foo.
+{{ end }}
+`
+
+ b, err := hugolib.TestE(t, files)
+
+ b.Assert(err, qt.Not(qt.IsNil))
+ b.Assert(err.Error(), qt.Contains, "error calling Defer: this method cannot be called before the site is fully initialized")
+}
+
+func TestDeferPostProcessShouldThrowAnError(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- assets/mytext.txt --
+ABCD.
+-- layouts/index.html --
+Home
+{{ with (templates.Defer (dict "key" "foo")) }}
+{{ $mytext := resources.Get "mytext.txt" | minify | resources.PostProcess }}
+{{ end }}
+
+`
+ b, err := hugolib.TestE(t, files)
+
+ b.Assert(err, qt.Not(qt.IsNil))
+ b.Assert(err.Error(), qt.Contains, "resources.PostProcess cannot be used in a deferred template")
+}
+
+// Issue #13236.
+func TestDeferMultipleInSameTemplate(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/index.html --
+Home.
+...
+{{ with (templates.Defer (dict "data" (dict "a" "b") )) }}
+ Defer 1
+{{ end }}
+...
+{{ with (templates.Defer (dict "data" (dict "a" "c") )) }}
+Defer 2
+{{ end }}
+{{ with (templates.Defer (dict "data" (dict "a" "d") )) }}
+Defer 3
+{{ end }}{{ with (templates.Defer (dict "data" (dict "a" "d") )) }}{{ end }}
+End.
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Home.", "Defer 1", "Defer 2", "Defer 3", "End.")
+}
diff --git a/tpl/templates/init.go b/tpl/templates/init.go
index ff6acdabd..7bd1f50c5 100644
--- a/tpl/templates/init.go
+++ b/tpl/templates/init.go
@@ -39,6 +39,16 @@ func init() {
},
)
+ ns.AddMethodMapping(ctx.Defer,
+ nil, // No aliases to keep the AST parsing simple.
+ [][2]string{},
+ )
+
+ ns.AddMethodMapping(ctx.DoDefer,
+ []string{"doDefer"},
+ [][2]string{},
+ )
+
return ns
}
diff --git a/tpl/templates/templates.go b/tpl/templates/templates.go
index 8e40f3443..92165e824 100644
--- a/tpl/templates/templates.go
+++ b/tpl/templates/templates.go
@@ -15,14 +15,24 @@
package templates
import (
+ "context"
+ "fmt"
+ "strconv"
+ "sync/atomic"
+
+ "github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/tpl"
+ "github.com/mitchellh/mapstructure"
)
// New returns a new instance of the templates-namespaced template functions.
func New(deps *deps.Deps) *Namespace {
- return &Namespace{
+ ns := &Namespace{
deps: deps,
}
+
+ return ns
}
// Namespace provides template functions for the "templates" namespace.
@@ -34,5 +44,66 @@ type Namespace struct {
// Note that this is the Unix-styled relative path including filename suffix,
// e.g. partials/header.html
func (ns *Namespace) Exists(name string) bool {
- return ns.deps.Tmpl().HasTemplate(name)
+ return ns.deps.GetTemplateStore().HasTemplate(name)
+}
+
+// Defer defers the execution of a template block.
+func (ns *Namespace) Defer(args ...any) (bool, error) {
+ // Prevent defer from being used in content adapters,
+ // that just doesn't work.
+ ns.deps.Site.CheckReady()
+
+ if len(args) != 0 {
+ return false, fmt.Errorf("Defer does not take any arguments")
+ }
+ return true, nil
+}
+
+var defferedIDCounter atomic.Uint64
+
+type DeferOpts struct {
+ // Optional cache key. If set, the deferred block will be executed
+ // once per unique key.
+ Key string
+
+ // Optional data context to use when executing the deferred block.
+ Data any
+}
+
+// DoDefer defers the execution of a template block.
+// For internal use only.
+func (ns *Namespace) DoDefer(ctx context.Context, id string, optsv any) string {
+ var opts DeferOpts
+ if optsv != nil {
+ if err := mapstructure.WeakDecode(optsv, &opts); err != nil {
+ panic(err)
+ }
+ }
+
+ templateName := id
+ var key string
+ if opts.Key != "" {
+ key = hashing.MD5FromStringHexEncoded(opts.Key)
+ } else {
+ key = strconv.FormatUint(defferedIDCounter.Add(1), 10)
+ }
+
+ id = fmt.Sprintf("%s_%s%s", id, key, tpl.HugoDeferredTemplateSuffix)
+
+ _, _ = ns.deps.BuildState.DeferredExecutions.Executions.GetOrCreate(id,
+ func() (*tpl.DeferredExecution, error) {
+ return &tpl.DeferredExecution{
+ TemplatePath: templateName,
+ Ctx: ctx,
+ Data: opts.Data,
+ Executed: false,
+ }, nil
+ })
+
+ return id
+}
+
+// Get information about the currently executing template.
+func (ns *Namespace) Current(ctx context.Context) *tpl.CurrentTemplateInfo {
+ return tpl.Context.CurrentTemplate.Get(ctx)
}
diff --git a/tpl/templates/templates_integration_test.go b/tpl/templates/templates_integration_test.go
index 301f783a5..baa917eee 100644
--- a/tpl/templates/templates_integration_test.go
+++ b/tpl/templates/templates_integration_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Hugo Authors. All rights reserved.
+// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,8 +14,10 @@
package templates_test
import (
+ "path/filepath"
"testing"
+ qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugolib"
)
@@ -93,3 +95,223 @@ Home: true
`)
}
+
+func TestTry(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- config.toml --
+baseURL = 'http://example.com/'
+-- layouts/index.html --
+Home.
+{{ $g := try ("hello = \"Hello Hugo\"" | transform.Unmarshal) }}
+{{ with $g.Err }}
+Err1: {{ . }}
+{{ else }}
+Value1: {{ $g.Value.hello | safeHTML }}|
+{{ end }}
+{{ $g := try ("hello != \"Hello Hugo\"" | transform.Unmarshal) }}
+{{ with $g.Err }}
+Err2: {{ . | safeHTML }}
+{{ else }}
+Value2: {{ $g.Value.hello | safeHTML }}|
+{{ end }}
+Try upper: {{ (try ("hello" | upper)).Value }}
+Try printf: {{ (try (printf "hello %s" "world")).Value }}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html",
+ "Value1: Hello Hugo|",
+ "Err2: template: index.html:",
+ "Try upper: HELLO",
+ "Try printf: hello world",
+ )
+}
+
+func TestTemplatesCurrent(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/baseof.html --
+baseof: {{ block "main" . }}{{ end }}
+-- layouts/all.html --
+{{ define "main" }}
+all.current: {{ templates.Current.Name }}
+all.current.filename: {{ templates.Current.Filename }}
+all.base: {{ with templates.Current.Base }}{{ .Name }}{{ end }}|
+all.parent: {{ with .Parent }}Name: {{ .Name }}{{ end }}|
+{{ partial "p1.html" . }}
+{{ end }}
+-- layouts/_partials/p1.html --
+p1.current: {{ with templates.Current }}Name: {{ .Name }}|{{ with .Parent }}Parent.Name: {{ .Name }}{{ end }}{{ end }}|
+p1.current.Ancestors: {{ with templates.Current }}{{ range .Ancestors }}{{ .Name }}|{{ end }}{{ end }}
+{{ partial "p2.html" . }}
+-- layouts/_partials/p2.html --
+p2.current: {{ with templates.Current }}Name: {{ .Name }}|{{ with .Parent }}Parent.Name: {{ .Name }}{{ end }}{{ end }}|
+p2.current.Ancestors: {{ with templates.Current }}{{ range .Ancestors }}{{ .Name }}|{{ end }}{{ end }}
+p3.current.Ancestors.Reverse: {{ with templates.Current }}{{ range .Ancestors.Reverse }}{{ .Name }}|{{ end }}{{ end }}
+
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html",
+ "all.current: all.html",
+ filepath.FromSlash("all.current.filename: /layouts/all.html"),
+ "all.base: baseof.html",
+ "all.parent: |",
+ "p1.current: Name: _partials/p1.html|Parent.Name: all.html|",
+ "p1.current.Ancestors: all.html|",
+ "p2.current.Ancestors: _partials/p1.html|all.html",
+ )
+}
+
+func TestBaseOfIssue13583(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- content/_index.md --
+---
+title: "Home"
+outputs: ["html", "amp"]
+---
+title: "Home"
+-- layouts/baseof.html --
+layouts/baseof.html
+{{ block "main" . }}{{ end }}
+-- layouts/baseof.amp.html --
+layouts/baseof.amp.html
+{{ block "main" . }}{{ end }}
+-- layouts/home.html --
+{{ define "main" }}
+Home.
+{{ end }}
+
+`
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.html", "layouts/baseof.html")
+ b.AssertFileContent("public/amp/index.html", "layouts/baseof.amp.html")
+}
+
+func TestAllVsAmp(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- content/_index.md --
+---
+title: "Home"
+outputs: ["html", "amp"]
+---
+title: "Home"
+-- layouts/all.html --
+All.
+
+`
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.html", "All.")
+ b.AssertFileContent("public/amp/index.html", "All.")
+}
+
+// Issue #13584.
+func TestLegacySectionSection(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- content/mysection/_index.md --
+-- layouts/section/section.html --
+layouts/section/section.html
+
+`
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/mysection/index.html", "layouts/section/section.html")
+}
+
+func TestErrorMessageParseError(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/home.html --
+Line 1.
+Line 2. {{ foo }} <- this func does not exist.
+Line 3.
+`
+
+ b, err := hugolib.TestE(t, files)
+ b.Assert(err, qt.IsNotNil)
+ b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`"/layouts/home.html:2:1": parse of template failed: template: home.html:2: function "foo" not defined`))
+}
+
+func TestErrorMessageExecuteError(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/home.html --
+Line 1.
+Line 2. {{ .Foo }} <- this method does not exist.
+Line 3.
+`
+
+ b, err := hugolib.TestE(t, files)
+ b.Assert(err, qt.IsNotNil)
+ b.Assert(err.Error(), qt.Contains, filepath.FromSlash(` "/layouts/home.html:2:11": execute of template failed`))
+}
+
+func TestPartialReturnPanicIssue13600(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/home.html --
+Partial: {{ partial "p1.html" . }}
+-- layouts/_partials/p1.html --
+P1.
+{{ return ( delimit . ", " ) | string }}
+`
+
+ b, err := hugolib.TestE(t, files)
+ b.Assert(err, qt.IsNotNil)
+ b.Assert(err.Error(), qt.Contains, "wrong number of args for string: want 1 got 0")
+}
+
+func TestPartialWithoutSuffixIssue13601(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/home.html --
+P1: {{ partial "p1" . }}
+P2: {{ partial "p2" . }}
+-- layouts/_partials/p1 --
+P1.
+-- layouts/_partials/p2 --
+P2.
+{{ return "foo bar" }}
+
+`
+
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.html", "P1: P1.\nP2: foo bar")
+}
+
+func TestTemplateExistsCaseIssue13684(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/home.html --
+P1: {{ templates.Exists "_partials/MyPartial.html" }}|P1: {{ templates.Exists "_partials/mypartial.html" }}|
+-- layouts/_partials/MyPartial.html --
+MyPartial.
+
+`
+
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.html", "P1: true|P1: true|")
+}
diff --git a/tpl/time/init.go b/tpl/time/init.go
index 5f9dd77bf..b13d0ab89 100644
--- a/tpl/time/init.go
+++ b/tpl/time/init.go
@@ -29,7 +29,7 @@ func init() {
if d.Conf.Language() == nil {
panic("Language must be set")
}
- ctx := New(langs.GetTimeFormatter(d.Conf.Language()), langs.GetLocation(d.Conf.Language()))
+ ctx := New(langs.GetTimeFormatter(d.Conf.Language()), langs.GetLocation(d.Conf.Language()), d)
ns := &internal.TemplateFuncsNamespace{
Name: name,
diff --git a/tpl/time/time.go b/tpl/time/time.go
index 57b115f35..f7c0db75e 100644
--- a/tpl/time/time.go
+++ b/tpl/time/time.go
@@ -18,16 +18,28 @@ import (
"fmt"
"time"
+ "github.com/gohugoio/hugo/cache/dynacache"
"github.com/gohugoio/hugo/common/htime"
+ "github.com/gohugoio/hugo/deps"
"github.com/spf13/cast"
)
// New returns a new instance of the time-namespaced template functions.
-func New(timeFormatter htime.TimeFormatter, location *time.Location) *Namespace {
+func New(timeFormatter htime.TimeFormatter, location *time.Location, deps *deps.Deps) *Namespace {
+ if deps.MemCache == nil {
+ panic("must provide MemCache")
+ }
+
return &Namespace{
timeFormatter: timeFormatter,
location: location,
+ deps: deps,
+ cacheIn: dynacache.GetOrCreatePartition[string, *time.Location](
+ deps.MemCache,
+ "/tmpl/time/in",
+ dynacache.OptionsPartition{Weight: 30, ClearWhen: dynacache.ClearNever},
+ ),
}
}
@@ -35,6 +47,8 @@ func New(timeFormatter htime.TimeFormatter, location *time.Location) *Namespace
type Namespace struct {
timeFormatter htime.TimeFormatter
location *time.Location
+ deps *deps.Deps
+ cacheIn *dynacache.Partition[string, *time.Location]
}
// AsTime converts the textual representation of the datetime string into
@@ -71,6 +85,21 @@ func (ns *Namespace) Now() time.Time {
return htime.Now()
}
+// In returns the time t in the IANA time zone specified by timeZoneName.
+// If timeZoneName is "" or "UTC", the time is returned in UTC.
+// If timeZoneName is "Local", the time is returned in the system's local time zone.
+// Otherwise, timeZoneName must be a valid IANA location name (e.g., "Europe/Oslo").
+func (ns *Namespace) In(timeZoneName string, t time.Time) (time.Time, error) {
+ location, err := ns.cacheIn.GetOrCreate(dynacache.CleanKey(timeZoneName), func(string) (*time.Location, error) {
+ return time.LoadLocation(timeZoneName)
+ })
+ if err != nil {
+ return time.Time{}, err
+ }
+
+ return t.In(location), nil
+}
+
// ParseDuration parses the duration string s.
// A duration string is a possibly signed sequence of
// decimal numbers, each with optional fraction and a unit suffix,
diff --git a/tpl/time/time_test.go b/tpl/time/time_test.go
index b13e62929..622f1b12d 100644
--- a/tpl/time/time_test.go
+++ b/tpl/time/time_test.go
@@ -11,24 +11,31 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package time
+package time_test
import (
"strings"
"testing"
- "time"
+ gtime "time"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/htime"
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/tpl/time"
+
translators "github.com/gohugoio/localescompressed"
)
func TestTimeLocation(t *testing.T) {
t.Parallel()
- loc, _ := time.LoadLocation("America/Antigua")
- ns := New(htime.NewTimeFormatter(translators.GetTranslator("en")), loc)
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{T: t},
+ ).Build()
+
+ loc, _ := gtime.LoadLocation("America/Antigua")
+ ns := time.New(htime.NewTimeFormatter(translators.GetTranslator("en")), loc, b.H.Deps)
for i, test := range []struct {
name string
@@ -72,7 +79,7 @@ func TestTimeLocation(t *testing.T) {
// See https://github.com/gohugoio/hugo/issues/8843#issuecomment-891551447
// Drop the location string (last element) when comparing,
// as that may change depending on the local locale.
- timeStr := result.(time.Time).String()
+ timeStr := result.(gtime.Time).String()
timeStr = timeStr[:strings.LastIndex(timeStr, " ")]
if !strings.HasPrefix(test.expect.(string), timeStr) {
t.Errorf("[%d] AsTime got %v but expected %v", i, timeStr, test.expect)
@@ -85,9 +92,14 @@ func TestTimeLocation(t *testing.T) {
func TestFormat(t *testing.T) {
c := qt.New(t)
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{T: t},
+ ).Build()
+
c.Run("UTC", func(c *qt.C) {
c.Parallel()
- ns := New(htime.NewTimeFormatter(translators.GetTranslator("en")), time.UTC)
+
+ ns := time.New(htime.NewTimeFormatter(translators.GetTranslator("en")), gtime.UTC, b.H.Deps)
for i, test := range []struct {
layout string
@@ -95,15 +107,15 @@ func TestFormat(t *testing.T) {
expect any
}{
{"Monday, Jan 2, 2006", "2015-01-21", "Wednesday, Jan 21, 2015"},
- {"Monday, Jan 2, 2006", time.Date(2015, time.January, 21, 0, 0, 0, 0, time.UTC), "Wednesday, Jan 21, 2015"},
+ {"Monday, Jan 2, 2006", gtime.Date(2015, gtime.January, 21, 0, 0, 0, 0, gtime.UTC), "Wednesday, Jan 21, 2015"},
{"This isn't a date layout string", "2015-01-21", "This isn't a date layout string"},
// The following test case gives either "Tuesday, Jan 20, 2015" or "Monday, Jan 19, 2015" depending on the local time zone
- {"Monday, Jan 2, 2006", 1421733600, time.Unix(1421733600, 0).Format("Monday, Jan 2, 2006")},
+ {"Monday, Jan 2, 2006", 1421733600, gtime.Unix(1421733600, 0).Format("Monday, Jan 2, 2006")},
{"Monday, Jan 2, 2006", 1421733600.123, false},
- {time.RFC3339, time.Date(2016, time.March, 3, 4, 5, 0, 0, time.UTC), "2016-03-03T04:05:00Z"},
- {time.RFC1123, time.Date(2016, time.March, 3, 4, 5, 0, 0, time.UTC), "Thu, 03 Mar 2016 04:05:00 UTC"},
- {time.RFC3339, "Thu, 03 Mar 2016 04:05:00 UTC", "2016-03-03T04:05:00Z"},
- {time.RFC1123, "2016-03-03T04:05:00Z", "Thu, 03 Mar 2016 04:05:00 UTC"},
+ {gtime.RFC3339, gtime.Date(2016, gtime.March, 3, 4, 5, 0, 0, gtime.UTC), "2016-03-03T04:05:00Z"},
+ {gtime.RFC1123, gtime.Date(2016, gtime.March, 3, 4, 5, 0, 0, gtime.UTC), "Thu, 03 Mar 2016 04:05:00 UTC"},
+ {gtime.RFC3339, "Thu, 03 Mar 2016 04:05:00 UTC", "2016-03-03T04:05:00Z"},
+ {gtime.RFC1123, "2016-03-03T04:05:00Z", "Thu, 03 Mar 2016 04:05:00 UTC"},
// Custom layouts, as introduced in Hugo 0.87.
{":date_medium", "2015-01-21", "Jan 21, 2015"},
} {
@@ -128,9 +140,9 @@ func TestFormat(t *testing.T) {
c.Run("TZ America/Los_Angeles", func(c *qt.C) {
c.Parallel()
- loc, err := time.LoadLocation("America/Los_Angeles")
+ loc, err := gtime.LoadLocation("America/Los_Angeles")
c.Assert(err, qt.IsNil)
- ns := New(htime.NewTimeFormatter(translators.GetTranslator("en")), loc)
+ ns := time.New(htime.NewTimeFormatter(translators.GetTranslator("en")), loc, b.H.Deps)
d, err := ns.Format(":time_full", "2020-03-09T11:00:00")
@@ -142,28 +154,32 @@ func TestFormat(t *testing.T) {
func TestDuration(t *testing.T) {
t.Parallel()
- ns := New(htime.NewTimeFormatter(translators.GetTranslator("en")), time.UTC)
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{T: t},
+ ).Build()
+
+ ns := time.New(htime.NewTimeFormatter(translators.GetTranslator("en")), gtime.UTC, b.H.Deps)
for i, test := range []struct {
unit any
num any
expect any
}{
- {"nanosecond", 10, 10 * time.Nanosecond},
- {"ns", 10, 10 * time.Nanosecond},
- {"microsecond", 20, 20 * time.Microsecond},
- {"us", 20, 20 * time.Microsecond},
- {"µs", 20, 20 * time.Microsecond},
- {"millisecond", 20, 20 * time.Millisecond},
- {"ms", 20, 20 * time.Millisecond},
- {"second", 30, 30 * time.Second},
- {"s", 30, 30 * time.Second},
- {"minute", 20, 20 * time.Minute},
- {"m", 20, 20 * time.Minute},
- {"hour", 20, 20 * time.Hour},
- {"h", 20, 20 * time.Hour},
+ {"nanosecond", 10, 10 * gtime.Nanosecond},
+ {"ns", 10, 10 * gtime.Nanosecond},
+ {"microsecond", 20, 20 * gtime.Microsecond},
+ {"us", 20, 20 * gtime.Microsecond},
+ {"µs", 20, 20 * gtime.Microsecond},
+ {"millisecond", 20, 20 * gtime.Millisecond},
+ {"ms", 20, 20 * gtime.Millisecond},
+ {"second", 30, 30 * gtime.Second},
+ {"s", 30, 30 * gtime.Second},
+ {"minute", 20, 20 * gtime.Minute},
+ {"m", 20, 20 * gtime.Minute},
+ {"hour", 20, 20 * gtime.Hour},
+ {"h", 20, 20 * gtime.Hour},
{"hours", 20, false},
- {"hour", "30", 30 * time.Hour},
+ {"hour", "30", 30 * gtime.Hour},
} {
result, err := ns.Duration(test.unit, test.num)
if b, ok := test.expect.(bool); ok && !b {
@@ -181,3 +197,74 @@ func TestDuration(t *testing.T) {
}
}
}
+
+func TestIn(t *testing.T) {
+ t.Parallel()
+
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{T: t},
+ ).Build()
+
+ ns := time.New(htime.NewTimeFormatter(translators.GetTranslator("en")), gtime.UTC, b.H.Deps)
+
+ in := gtime.Date(2025, gtime.March, 31, 15, 0, 0, 0, gtime.UTC)
+
+ tests := []struct {
+ name string
+ tzn string // time zone name
+ want string
+ wantErr bool
+ }{
+ {name: "A", tzn: "America/Denver", want: "2025-03-31T09:00:00-06:00", wantErr: false},
+ {name: "B", tzn: "Australia/Adelaide", want: "2025-04-01T01:30:00+10:30", wantErr: false},
+ {name: "C", tzn: "Europe/Oslo", want: "2025-03-31T17:00:00+02:00", wantErr: false},
+ {name: "D", tzn: "UTC", want: "2025-03-31T15:00:00+00:00", wantErr: false},
+ {name: "E", tzn: "", want: "2025-03-31T15:00:00+00:00", wantErr: false},
+ {name: "F", tzn: "InvalidTimeZoneName", want: "0001-01-01T00:00:00+00:00", wantErr: true},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := ns.In(tt.tzn, in)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("time.In() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ got := result.Format("2006-01-02T15:04:05-07:00")
+ if got != tt.want {
+ t.Errorf("time.In() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+// For benchmark tests below.
+var timeZoneNames []string = []string{"America/New_York", "Europe/Oslo", "Australia/Sydney", "UTC", "Local"}
+
+func BenchmarkInWithCaching(b *testing.B) {
+ bb := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{T: b},
+ ).Build()
+
+ ns := time.New(htime.NewTimeFormatter(translators.GetTranslator("en")), gtime.UTC, bb.H.Deps)
+
+ for i := 0; i < b.N; i++ {
+ timeZoneName := timeZoneNames[i%len(timeZoneNames)]
+ _, err := ns.In(timeZoneName, gtime.Now())
+ if err != nil {
+ b.Fatalf("Error during benchmark: %v", err)
+ }
+ }
+}
+
+func BenchmarkInWithoutCaching(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ timeZoneName := timeZoneNames[i%len(timeZoneNames)]
+ location, err := gtime.LoadLocation(timeZoneName)
+ if err != nil {
+ b.Fatalf("Error during benchmark: %v", err)
+ }
+
+ _ = gtime.Now().In(location)
+ }
+}
diff --git a/tpl/tplimpl/category_string.go b/tpl/tplimpl/category_string.go
new file mode 100644
index 000000000..2f7730113
--- /dev/null
+++ b/tpl/tplimpl/category_string.go
@@ -0,0 +1,30 @@
+// Code generated by "stringer -type Category"; DO NOT EDIT.
+
+package tplimpl
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[CategoryLayout-1]
+ _ = x[CategoryBaseof-2]
+ _ = x[CategoryMarkup-3]
+ _ = x[CategoryShortcode-4]
+ _ = x[CategoryPartial-5]
+ _ = x[CategoryServer-6]
+ _ = x[CategoryHugo-7]
+}
+
+const _Category_name = "CategoryLayoutCategoryBaseofCategoryMarkupCategoryShortcodeCategoryPartialCategoryServerCategoryHugo"
+
+var _Category_index = [...]uint8{0, 14, 28, 42, 59, 74, 88, 100}
+
+func (i Category) String() string {
+ i -= 1
+ if i < 0 || i >= Category(len(_Category_index)-1) {
+ return "Category(" + strconv.FormatInt(int64(i+1), 10) + ")"
+ }
+ return _Category_name[_Category_index[i]:_Category_index[i+1]]
+}
diff --git a/tpl/tplimpl/embedded/templates/_default/_markup/render-image.html b/tpl/tplimpl/embedded/templates/_default/_markup/render-image.html
deleted file mode 100644
index 875763910..000000000
--- a/tpl/tplimpl/embedded/templates/_default/_markup/render-image.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{{- $u := urls.Parse .Destination -}}
-{{- $src := $u.String -}}
-{{- if not $u.IsAbs -}}
- {{- with or (.PageInner.Resources.Get $u.Path) (resources.Get $u.Path) -}}
- {{- $src = .RelPermalink -}}
- {{- end -}}
-{{- end -}}
-{{- $attributes := merge .Attributes (dict "alt" .Text "src" $src "title" (.Title | transform.HTMLEscape)) -}}
-
-{{- /**/ -}}
diff --git a/tpl/tplimpl/embedded/templates/_default/_markup/render-link.html b/tpl/tplimpl/embedded/templates/_default/_markup/render-link.html
deleted file mode 100644
index 30e4d2660..000000000
--- a/tpl/tplimpl/embedded/templates/_default/_markup/render-link.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{{- $u := urls.Parse .Destination -}}
-{{- $href := $u.String -}}
-{{- if strings.HasPrefix $u.String "#" }}
- {{- $href = printf "%s#%s" .PageInner.RelPermalink $u.Fragment }}
-{{- else if not $u.IsAbs -}}
- {{- with or
- ($.PageInner.GetPage $u.Path)
- ($.PageInner.Resources.Get $u.Path)
- (resources.Get $u.Path)
- -}}
- {{- $href = .RelPermalink -}}
- {{- with $u.RawQuery -}}
- {{- $href = printf "%s?%s" $href . -}}
- {{- end -}}
- {{- with $u.Fragment -}}
- {{- $href = printf "%s#%s" $href . -}}
- {{- end -}}
- {{- end -}}
-{{- end -}}
-{{- $attributes := dict "href" $href "title" (.Title | transform.HTMLEscape) -}}
-{{ .Text | safeHTML }}
-{{- /**/ -}}
diff --git a/tpl/tplimpl/embedded/templates/_default/robots.txt b/tpl/tplimpl/embedded/templates/_default/robots.txt
deleted file mode 100644
index 4f9540ba3..000000000
--- a/tpl/tplimpl/embedded/templates/_default/robots.txt
+++ /dev/null
@@ -1 +0,0 @@
-User-agent: *
\ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/_hugo/build/js/batch-esm-runner.gotmpl b/tpl/tplimpl/embedded/templates/_hugo/build/js/batch-esm-runner.gotmpl
new file mode 100644
index 000000000..e1b3b9bc3
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_hugo/build/js/batch-esm-runner.gotmpl
@@ -0,0 +1,16 @@
+{{ range $i, $e := .Scripts -}}
+ {{ printf "import { %s as Script%d } from %q;" .Export $i .Import }}
+{{ end -}}
+{{ range $i, $e := .Runners }}
+ {{ printf "import { %s as Run%d } from %q;" .Export $i .Import }}
+{{ end }}
+{{/* */}}
+let scripts = [];
+{{ range $i, $e := .Scripts -}}
+ scripts.push({{ .RunnerJSON $i }});
+{{ end -}}
+{{/* */}}
+{{ range $i, $e := .Runners }}
+ {{ $id := printf "Run%d" $i }}
+ {{ $id }}(scripts);
+{{ end }}
diff --git a/tpl/tplimpl/embedded/templates/_default/_markup/render-codeblock-goat.html b/tpl/tplimpl/embedded/templates/_markup/render-codeblock-goat.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/_default/_markup/render-codeblock-goat.html
rename to tpl/tplimpl/embedded/templates/_markup/render-codeblock-goat.html
diff --git a/tpl/tplimpl/embedded/templates/_markup/render-image.html b/tpl/tplimpl/embedded/templates/_markup/render-image.html
new file mode 100644
index 000000000..d5040f6df
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_markup/render-image.html
@@ -0,0 +1,23 @@
+{{- $u := urls.Parse .Destination -}}
+{{- $src := $u.String -}}
+{{- if not $u.IsAbs -}}
+ {{- $path := strings.TrimPrefix "./" $u.Path -}}
+ {{- with or (.PageInner.Resources.Get $path) (resources.Get $path) -}}
+ {{- $src = .RelPermalink -}}
+ {{- with $u.RawQuery -}}
+ {{- $src = printf "%s?%s" $src . -}}
+ {{- end -}}
+ {{- with $u.Fragment -}}
+ {{- $src = printf "%s#%s" $src . -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+
+{{- /**/ -}}
diff --git a/tpl/tplimpl/embedded/templates/_markup/render-link.html b/tpl/tplimpl/embedded/templates/_markup/render-link.html
new file mode 100644
index 000000000..95e15aba4
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_markup/render-link.html
@@ -0,0 +1,22 @@
+{{- $u := urls.Parse .Destination -}}
+{{- $href := $u.String -}}
+{{- if strings.HasPrefix $u.String "#" -}}
+ {{- $href = printf "%s#%s" .PageInner.RelPermalink $u.Fragment -}}
+{{- else if and $href (not $u.IsAbs) -}}
+ {{- $path := strings.TrimPrefix "./" $u.Path -}}
+ {{- with or
+ ($.PageInner.GetPage $path)
+ ($.PageInner.Resources.Get $path)
+ (resources.Get $path)
+ -}}
+ {{- $href = .RelPermalink -}}
+ {{- with $u.RawQuery -}}
+ {{- $href = printf "%s?%s" $href . -}}
+ {{- end -}}
+ {{- with $u.Fragment -}}
+ {{- $href = printf "%s#%s" $href . -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+{{ .Text }}
+{{- /**/ -}}
diff --git a/tpl/tplimpl/embedded/templates/_markup/render-table.html b/tpl/tplimpl/embedded/templates/_markup/render-table.html
new file mode 100644
index 000000000..c43a72832
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_markup/render-table.html
@@ -0,0 +1,37 @@
+
+
+ {{- range .THead }}
+
+ {{- range . }}
+
+ {{- .Text -}}
+
+ {{- end }}
+
+ {{- end }}
+
+
+ {{- range .TBody }}
+
+ {{- range . }}
+
+ {{- .Text -}}
+
+ {{- end }}
+
+ {{- end }}
+
+
diff --git a/tpl/tplimpl/embedded/templates/partials/_funcs/get-page-images.html b/tpl/tplimpl/embedded/templates/_partials/_funcs/get-page-images.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/partials/_funcs/get-page-images.html
rename to tpl/tplimpl/embedded/templates/_partials/_funcs/get-page-images.html
diff --git a/tpl/tplimpl/embedded/templates/disqus.html b/tpl/tplimpl/embedded/templates/_partials/disqus.html
similarity index 92%
rename from tpl/tplimpl/embedded/templates/disqus.html
rename to tpl/tplimpl/embedded/templates/_partials/disqus.html
index 19fcb7fda..fb7cbd0fe 100644
--- a/tpl/tplimpl/embedded/templates/disqus.html
+++ b/tpl/tplimpl/embedded/templates/_partials/disqus.html
@@ -5,7 +5,7 @@
window.disqus_config = function () {
{{with .Params.disqus_identifier }}this.page.identifier = '{{ . }}';{{end}}
{{with .Params.disqus_title }}this.page.title = '{{ . }}';{{end}}
- {{with .Params.disqus_url }}this.page.url = '{{ . | html }}';{{end}}
+ {{with .Params.disqus_url }}this.page.url = '{{ . | transform.HTMLEscape | safeURL }}';{{end}}
};
(function() {
if (["localhost", "127.0.0.1"].indexOf(window.location.hostname) != -1) {
diff --git a/tpl/tplimpl/embedded/templates/google_analytics.html b/tpl/tplimpl/embedded/templates/_partials/google_analytics.html
similarity index 59%
rename from tpl/tplimpl/embedded/templates/google_analytics.html
rename to tpl/tplimpl/embedded/templates/_partials/google_analytics.html
index b8930d4bd..49856630f 100644
--- a/tpl/tplimpl/embedded/templates/google_analytics.html
+++ b/tpl/tplimpl/embedded/templates/_partials/google_analytics.html
@@ -1,8 +1,8 @@
{{ if not site.Config.Privacy.GoogleAnalytics.Disable }}
- {{ with site.Config.Services.GoogleAnalytics.ID }}
- {{ if strings.HasPrefix (lower .) "ua-" }}
- {{ warnf "Google Analytics 4 (GA4) replaced Google Universal Analytics (UA) effective 1 July 2023. See https://support.google.com/analytics/answer/11583528. Create a GA4 property and data stream, then replace the Google Analytics ID in your site configuration with the new value." }}
- {{ else }}
+ {{- with site.Config.Services.GoogleAnalytics.ID }}
+ {{- if strings.HasPrefix (lower .) "ua-" }}
+ {{- warnf "Google Analytics 4 (GA4) replaced Google Universal Analytics (UA) effective 1 July 2023. See https://support.google.com/analytics/answer/11583528. Create a GA4 property and data stream, then replace the Google Analytics ID in your site configuration with the new value." }}
+ {{- else }}
- {{ end }}
- {{ end }}
-{{ end }}
+ {{- end }}
+ {{- end }}
+{{- end -}}
diff --git a/tpl/tplimpl/embedded/templates/opengraph.html b/tpl/tplimpl/embedded/templates/_partials/opengraph.html
similarity index 90%
rename from tpl/tplimpl/embedded/templates/opengraph.html
rename to tpl/tplimpl/embedded/templates/_partials/opengraph.html
index a9b348e9e..9f40aedf0 100644
--- a/tpl/tplimpl/embedded/templates/opengraph.html
+++ b/tpl/tplimpl/embedded/templates/_partials/opengraph.html
@@ -8,12 +8,12 @@
{{- end }}
-{{- with or .Description .Summary site.Params.description | plainify | htmlUnescape | chomp }}
-
+{{- with or .Description .Summary site.Params.description | plainify | htmlUnescape }}
+
{{- end }}
-{{- with or .Params.locale site.Language.LanguageCode site.Language.Lang }}
-
+{{- with or .Params.locale site.Language.LanguageCode }}
+
{{- end }}
{{- if .IsPage }}
diff --git a/tpl/tplimpl/embedded/templates/pagination.html b/tpl/tplimpl/embedded/templates/_partials/pagination.html
similarity index 96%
rename from tpl/tplimpl/embedded/templates/pagination.html
rename to tpl/tplimpl/embedded/templates/_partials/pagination.html
index 717797ab2..995ac5680 100644
--- a/tpl/tplimpl/embedded/templates/pagination.html
+++ b/tpl/tplimpl/embedded/templates/_partials/pagination.html
@@ -20,7 +20,7 @@
{{- if in $validFormats $format }}
{{- if gt $page.Paginator.TotalPages 1 }}
{{- end }}
{{- else }}
@@ -29,7 +29,7 @@
{{/* Format: default
{{/* --------------------------------------------------------------------- */}}
-{{- define "partials/inline/pagination/default" }}
+{{- define "_partials/inline/pagination/default.html" }}
{{- with .Paginator }}
{{- $currentPageNumber := .PageNumber }}
@@ -100,7 +100,7 @@
{{/* Format: terse
{{/* --------------------------------------------------------------------- */}}
-{{- define "partials/inline/pagination/terse" }}
+{{- define "_partials/inline/pagination/terse.html" }}
{{- with .Paginator }}
{{- $currentPageNumber := .PageNumber }}
diff --git a/tpl/tplimpl/embedded/templates/schema.html b/tpl/tplimpl/embedded/templates/_partials/schema.html
similarity index 87%
rename from tpl/tplimpl/embedded/templates/schema.html
rename to tpl/tplimpl/embedded/templates/_partials/schema.html
index c4e89abd6..36c01178b 100644
--- a/tpl/tplimpl/embedded/templates/schema.html
+++ b/tpl/tplimpl/embedded/templates/_partials/schema.html
@@ -1,9 +1,9 @@
-{{- with or .Title site.Title }}
+{{- with or .Title site.Title | plainify }}
{{- end }}
-{{- with or .Description .Summary site.Params.Description }}
-
+{{- with or .Description .Summary site.Params.description | plainify | htmlUnescape }}
+
{{- end }}
{{- $ISO8601 := "2006-01-02T15:04:05-07:00" }}
diff --git a/tpl/tplimpl/embedded/templates/_partials/twitter_cards.html b/tpl/tplimpl/embedded/templates/_partials/twitter_cards.html
new file mode 100644
index 000000000..8af0e986c
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_partials/twitter_cards.html
@@ -0,0 +1,28 @@
+{{- $images := partial "_funcs/get-page-images" . }}
+{{- with index $images 0 }}
+
+
+{{- else }}
+
+{{- end }}
+
+{{- with or .Title site.Title site.Params.title | plainify }}
+
+{{- end }}
+
+{{- with or .Description .Summary site.Params.description | plainify | htmlUnescape }}
+
+{{- end }}
+
+{{- $twitterSite := "" }}
+{{- with site.Params.social }}
+ {{- if reflect.IsMap . }}
+ {{- with .twitter }}
+ {{- $content := . }}
+ {{- if not (strings.HasPrefix . "@") }}
+ {{- $content = printf "@%v" . }}
+ {{- end }}
+
+ {{- end }}
+ {{- end }}
+{{- end }}
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/1__h_simple_assets.html b/tpl/tplimpl/embedded/templates/_shortcodes/1__h_simple_assets.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/1__h_simple_assets.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/1__h_simple_assets.html
diff --git a/tpl/tplimpl/embedded/templates/_shortcodes/comment.html b/tpl/tplimpl/embedded/templates/_shortcodes/comment.html
new file mode 100644
index 000000000..c35b5cad6
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_shortcodes/comment.html
@@ -0,0 +1,2 @@
+{{- warnf "The %q shortcode was deprecated in v0.143.0 and will be removed in a future release. Please use HTML comments instead. See %s" .Name .Position -}}
+{{- $noop := .Inner -}}
diff --git a/tpl/tplimpl/embedded/templates/_shortcodes/details.html b/tpl/tplimpl/embedded/templates/_shortcodes/details.html
new file mode 100644
index 000000000..82c4f68f7
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_shortcodes/details.html
@@ -0,0 +1,75 @@
+{{- /*
+Renders an HTML details element.
+
+@param {string} [class] The value of the element's class attribute.
+@param {string} [name] The value of the element's name attribute.
+@param {string} [summary] The content of the child summary element.
+@param {string} [title] The value of the element's title attribute.
+@param {bool} [open=false] Whether to initially display the content of the details element.
+
+@reference https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details
+
+@examples
+
+ {{< details >}}
+ A basic collapsible section.
+ {{< /details >}}
+
+ {{< details summary="Custom Summary Text" >}}
+ Showing custom `summary` text.
+ {{< /details >}}
+
+ {{< details summary="Open Details" open=true >}}
+ Contents displayed initially by using `open`.
+ {{< /details >}}
+
+ {{< details summary="Styled Content" class="my-custom-class" >}}
+ Content can be styled with CSS by specifying a `class`.
+
+ Target details element:
+
+ ```css
+ details.my-custom-class { }
+ ```
+
+ Target summary element:
+
+ ```css
+ details.my-custom-class > summary > * { }
+ ```
+
+ Target inner content:
+
+ ```css
+ details.my-custom-class > :not(summary) { }
+ ```
+ {{< /details >}}
+
+ {{< details summary="Grouped Details" name="my-details" >}}
+ Specifying a `name` allows elements to be connected, with only one able to be open at a time.
+ {{< /details >}}
+
+*/}}
+
+{{- /* Get arguments. */}}
+{{- $class := or (.Get "class") "" }}
+{{- $name := or (.Get "name") "" }}
+{{- $summary := or (.Get "summary") "Details" }}
+{{- $title := or (.Get "title") "" }}
+{{- $open := false }}
+{{- if in (slice "false" false 0) (.Get "open") }}
+ {{- $open = false }}
+{{- else if in (slice "true" true 1) (.Get "open") }}
+ {{- $open = true }}
+{{- end }}
+
+{{- /* Render. */}}
+
+ {{ $summary | .Page.RenderString }}
+ {{ .Inner | .Page.RenderString (dict "display" "block") -}}
+
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/figure.html b/tpl/tplimpl/embedded/templates/_shortcodes/figure.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/figure.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/figure.html
diff --git a/tpl/tplimpl/embedded/templates/_shortcodes/gist.html b/tpl/tplimpl/embedded/templates/_shortcodes/gist.html
new file mode 100644
index 000000000..316d2d54e
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_shortcodes/gist.html
@@ -0,0 +1,2 @@
+{{- warnf "The %q shortcode was deprecated in v0.143.0 and will be removed in a future release. See https://gohugo.io/shortcodes/gist for instructions to create a replacement." .Name -}}
+
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/highlight.html b/tpl/tplimpl/embedded/templates/_shortcodes/highlight.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/highlight.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/highlight.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/instagram.html b/tpl/tplimpl/embedded/templates/_shortcodes/instagram.html
similarity index 98%
rename from tpl/tplimpl/embedded/templates/shortcodes/instagram.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/instagram.html
index 5d940d67b..804038e7d 100644
--- a/tpl/tplimpl/embedded/templates/shortcodes/instagram.html
+++ b/tpl/tplimpl/embedded/templates/_shortcodes/instagram.html
@@ -3,7 +3,7 @@
{{- with .Get 0 -}}
{{- template "render-instagram" (dict "id" . "pc" $pc) -}}
{{- else -}}
- {{- errorf "The %q shortocde requires a single positional parameter, the ID of the Instagram post. See %s" .Name .Position -}}
+ {{- errorf "The %q shortcode requires a single positional parameter, the ID of the Instagram post. See %s" .Name .Position -}}
{{- end -}}
{{- end -}}
@@ -239,6 +239,6 @@
{{- if not .pc.Simple -}}
-
+
{{- end -}}
{{- end -}}
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/instagram_simple.html b/tpl/tplimpl/embedded/templates/_shortcodes/instagram_simple.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/instagram_simple.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/instagram_simple.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/param.html b/tpl/tplimpl/embedded/templates/_shortcodes/param.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/param.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/param.html
diff --git a/tpl/tplimpl/embedded/templates/_shortcodes/qr.html b/tpl/tplimpl/embedded/templates/_shortcodes/qr.html
new file mode 100644
index 000000000..05e8dbde9
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_shortcodes/qr.html
@@ -0,0 +1,79 @@
+{{- /*
+Encodes the given text into a QR code using the specified options and renders the resulting image.
+
+@param {string} text The text to encode, falling back to the text between the opening and closing shortcode tags.
+@param {string} [level=medium] The error correction level to use when encoding the text, one of low, medium, quartile, or high.
+@param {int} [scale=4] The number of image pixels per QR code module. Must be greater than or equal to 2.
+@param {string} [targetDir] The subdirectory within the publishDir where Hugo will place the generated image.
+@param {string} [alt] The alt attribute of the img element.
+@param {string} [class] The class attribute of the img element.
+@param {string} [id] The id attribute of the img element.
+@param {string} [title] The title attribute of the img element.
+@param {string} [loading] The loading attribute of the img element, one of lazy or eager.
+
+@returns {template.HTML}
+
+@examples
+
+ {{< qr text="https://gohugo.io" />}}
+
+ {{< qr >}}
+ https://gohugo.io"
+ {{< /qr >}}
+
+ {{< qr
+ text="https://gohugo.io"
+ level="high"
+ scale=4
+ targetDir="codes"
+ alt="QR code linking to https://gohugo.io"
+ class="my-class"
+ id="my-id"
+ title="My Title"
+ />}}
+
+*/}}
+
+{{- /* Constants. */}}
+{{- $validLevels := slice "low" "medium" "quartile" "high" }}
+{{- $minimumScale := 2 }}
+
+{{- /* Get arguments. */}}
+{{- $text := or (.Get "text") (strings.TrimSpace .Inner) "" }}
+{{- $level := or (.Get "level") "medium" }}
+{{- $scale := or (.Get "scale") 4 }}
+{{- $targetDir := or (.Get "targetDir") "" }}
+{{- $alt := or (.Get "alt") "" }}
+{{- $class := or (.Get "class") "" }}
+{{- $id := or (.Get "id") "" }}
+{{- $title := or (.Get "title") "" }}
+{{- $loading := or (.Get "loading") "" }}
+
+{{- /* Validate arguments. */}}
+{{- $errors := false}}
+{{- if not $text }}
+ {{- errorf "The %q shortcode requires a %q argument. See %s" .Name "text" .Position }}
+ {{- $errors = true }}
+{{- end }}
+{{- if not (in $validLevels $level) }}
+ {{- errorf "The %q argument passed to the %q shortcode must be one of %s. See %s" "level" .Name (delimit $validLevels ", " ", or ") .Position }}
+ {{- $errors = true }}
+{{- end }}
+{{- if or (lt $scale $minimumScale) (ne $scale (int $scale)) }}
+ {{- errorf "The %q argument passed to the %q shortcode must be an integer greater than or equal to %d. See %s" "scale" .Name $minimumScale .Position }}
+ {{- $errors = true }}
+{{- end }}
+
+{{- /* Render image. */}}
+{{- if not $errors }}
+ {{- $opts := dict "level" $level "scale" $scale "targetDir" $targetDir }}
+ {{- with images.QR $text $opts -}}
+
+ {{- end }}
+{{- end -}}
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/ref.html b/tpl/tplimpl/embedded/templates/_shortcodes/ref.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/ref.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/ref.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/relref.html b/tpl/tplimpl/embedded/templates/_shortcodes/relref.html
similarity index 100%
rename from tpl/tplimpl/embedded/templates/shortcodes/relref.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/relref.html
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/twitter.html b/tpl/tplimpl/embedded/templates/_shortcodes/twitter.html
similarity index 52%
rename from tpl/tplimpl/embedded/templates/shortcodes/twitter.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/twitter.html
index ba5a851ee..c6200ffd2 100644
--- a/tpl/tplimpl/embedded/templates/shortcodes/twitter.html
+++ b/tpl/tplimpl/embedded/templates/_shortcodes/twitter.html
@@ -1,12 +1,13 @@
-{{- $pc := .Page.Site.Config.Privacy.Twitter -}}
+{{- warnf "The \"twitter\", \"tweet\", and \"twitter_simple\" shortcodes were deprecated in v0.142.0 and will be removed in a future release. Please use the \"x\" shortcode instead." }}
+{{- $pc := site.Config.Privacy.Twitter -}}
{{- if not $pc.Disable -}}
{{- if $pc.Simple -}}
- {{- template "_internal/shortcodes/twitter_simple.html" . -}}
+ {{- template "_shortcodes/twitter_simple.html" . -}}
{{- else -}}
{{- $id := or (.Get "id") "" -}}
{{- $user := or (.Get "user") "" -}}
{{- if and $id $user -}}
- {{- template "render-tweet" (dict "id" $id "user" $user "dnt" $pc.EnableDNT "name" .Name "position" .Position) -}}
+ {{- template "render-tweet" (dict "id" $id "user" $user "dnt" $pc.EnableDNT "ctx" .) -}}
{{- else -}}
{{- errorf "The %q shortcode requires two named parameters: user and id. See %s" .Name .Position -}}
{{- end -}}
@@ -17,13 +18,13 @@
{{- $url := printf "https://twitter.com/%v/status/%v" .user .id -}}
{{- $query := querify "url" $url "dnt" .dnt -}}
{{- $request := printf "https://publish.twitter.com/oembed?%s" $query -}}
- {{- with resources.GetRemote $request -}}
+ {{- with try (resources.GetRemote $request) -}}
{{- with .Err -}}
- {{- errorf "%s" . -}}
- {{- else -}}
+ {{- warnidf "shortcode-twitter-getremote" "The %q shortcode was unable to retrieve the remote data: %s. See %s" $.ctx.Name . $.ctx.Position -}}
+ {{- else with .Value -}}
{{- (. | transform.Unmarshal).html | safeHTML -}}
+ {{- else -}}
+ {{- warnidf "shortcode-twitter-getremote" "The %q shortcode was unable to retrieve the remote data. See %s" $.ctx.Name $.ctx.Position -}}
{{- end -}}
- {{- else -}}
- {{- warnidf "shortcode-twitter-getremote" "The %q shortcode was unable to retrieve the remote data. See %s" .name .position -}}
{{- end -}}
{{- end -}}
diff --git a/tpl/tplimpl/embedded/templates/_shortcodes/twitter_simple.html b/tpl/tplimpl/embedded/templates/_shortcodes/twitter_simple.html
new file mode 100644
index 000000000..34150393d
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_shortcodes/twitter_simple.html
@@ -0,0 +1,61 @@
+{{- warnf "The \"twitter\", \"tweet\", and \"twitter_simple\" shortcodes were deprecated in v0.142.0 and will be removed in a future release. Please use the \"x\" shortcode instead." }}
+{{- if not site.Config.Privacy.Twitter.Disable -}}
+ {{- $id := or (.Get "id") "" -}}
+ {{- $user := or (.Get "user") "" -}}
+ {{- if and $id $user -}}
+ {{- template "render-simple-tweet" (dict "id" $id "user" $user "ctx" .) -}}
+ {{- else -}}
+ {{- errorf "The %q shortcode requires two named parameters: user and id. See %s" .Name .Position -}}
+ {{- end -}}
+{{- end -}}
+
+{{- define "render-simple-tweet" -}}
+ {{- $dnt := site.Config.Privacy.Twitter.EnableDNT }}
+ {{- $url := printf "https://twitter.com/%v/status/%v" .user .id -}}
+ {{- $query := querify "url" $url "dnt" $dnt "omit_script" true -}}
+ {{- $request := printf "https://publish.twitter.com/oembed?%s" $query -}}
+ {{- with try (resources.GetRemote $request) -}}
+ {{- with .Err -}}
+ {{- warnidf "shortcode-twitter-simple-getremote" "The %q shortcode was unable to retrieve the remote data: %s. See %s" $.ctx.Name . $.ctx.Position -}}
+ {{- else with .Value -}}
+ {{- if not site.Config.Services.Twitter.DisableInlineCSS }}
+ {{- template "__h_simple_twitter_css" (dict "ctx" $.ctx) }}
+ {{- end }}
+ {{- (. | transform.Unmarshal).html | safeHTML -}}
+ {{- else -}}
+ {{- warnidf "shortcode-twitter-simple-getremote" "The %q shortcode was unable to retrieve the remote data. See %s" $.ctx.Name $.ctx.Position -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+
+{{- define "__h_simple_twitter_css" -}}
+ {{- if not (.ctx.Page.Store.Get "__h_simple_twitter_css") -}}
+ {{/* Only include once */}}
+ {{- .ctx.Page.Store.Set "__h_simple_twitter_css" true -}}
+
+ {{- end -}}
+{{- end -}}
diff --git a/tpl/tplimpl/embedded/templates/_shortcodes/vimeo.html b/tpl/tplimpl/embedded/templates/_shortcodes/vimeo.html
new file mode 100644
index 000000000..fb8ea0d97
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_shortcodes/vimeo.html
@@ -0,0 +1,67 @@
+{{- /*
+Renders an embedded Vimeo video.
+
+Accepts named or positional arguments. If positional, order is id, class,
+title, then loading.
+
+@param {bool} [allowFullScreen=true] Whether the iframe element can activate full screen mode.
+@param {string} [class] The class attribute of the wrapping div element. When specified, removes the style attributes from the iframe element and its wrapping div element.
+@param {string} [id] The video id. Optional if the id is the first and only positional argument.
+@param {string} [loading=eager] The loading attribute of the iframe element.
+@param {string} [title=Vimeo video] The title attribute of the iframe element.
+
+@returns {template.HTML}
+
+@example {{< vimeo 55073825 >}}
+@example {{< vimeo id=55073825 class="foo bar" loading=lazy title="My Video" >}}
+*/}}
+{{- $pc := site.Config.Privacy.Vimeo }}
+{{- if not $pc.Disable }}
+ {{- if $pc.Simple }}
+ {{- template "_shortcodes/vimeo_simple.html" . }}
+ {{- else }}
+ {{- $dnt := cond $pc.EnableDNT 1 0 }}
+
+ {{- $allowFullScreen := true }}
+ {{- $class := or (.Get "class") }}
+ {{- $id := or (.Get "id") (.Get 0) }}
+ {{- $loading := or (.Get "loading") }}
+ {{- $title := or (.Get "title") }}
+
+ {{- if in (slice "true" true 1) (.Get "allowFullScreen") }}
+ {{- $allowFullScreen = true }}
+ {{- else if in (slice "false" false 0) (.Get "allowFullScreen") }}
+ {{- $allowFullScreen = false }}
+ {{- end }}
+
+ {{- $iframeAllowList := "" }}
+ {{- if $allowFullScreen }}
+ {{- $iframeAllowList = "fullscreen" }}
+ {{- end }}
+
+ {{- $divStyle := "position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;" }}
+ {{- $iframeStyle := "position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" }}
+
+ {{- with $id }}
+ {{- $src := printf "https://player.vimeo.com/video/%v?dnt=%v" . $dnt }}
+
+
+
+
+ {{- else }}
+ {{- errorf "The %q shortcode requires a video id, either as the first positional argument or an argument named id. See %s" .Name .Position }}
+ {{- end }}
+ {{- end }}
+{{- end }}
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html b/tpl/tplimpl/embedded/templates/_shortcodes/vimeo_simple.html
similarity index 63%
rename from tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/vimeo_simple.html
index e262d3c3c..a00f3e7f6 100644
--- a/tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html
+++ b/tpl/tplimpl/embedded/templates/_shortcodes/vimeo_simple.html
@@ -1,52 +1,52 @@
-{{- $pc := .Page.Site.Config.Privacy.Vimeo -}}
+{{- $pc := site.Config.Privacy.Vimeo -}}
{{- if not $pc.Disable -}}
- {{- $ctx := dict "page" .Page "pc" $pc "name" .Name "position" .Position }}
+ {{- $ctx := dict "ctx" . }}
{{- if .IsNamedParams -}}
{{- with .Get "id" -}}
{{- $ctx = merge $ctx (dict "id" . "class" ($.Get "class")) -}}
{{- template "render-vimeo" $ctx -}}
{{- else -}}
- {{- errorf "The %q shortocde requires a single named parameter, the ID of the Vimeo video. See %s" .Name .Position -}}
+ {{- errorf "The %q shortcode requires a single named parameter, the ID of the Vimeo video. See %s" .Name .Position -}}
{{- end -}}
{{- else -}}
{{- with .Get 0 -}}
{{- $ctx = merge $ctx (dict "id" . "class" ($.Get 1)) -}}
{{- template "render-vimeo" $ctx -}}
{{- else -}}
- {{- errorf "The %q shortocde requires a single positional parameter, the ID of the Vimeo video. See %s" .Name .Position -}}
+ {{- errorf "The %q shortcode requires a single positional parameter, the ID of the Vimeo video. See %s" .Name .Position -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- define "render-vimeo" -}}
- {{- $dnt := cond .pc.EnableDNT 1 0 -}}
+ {{- $dnt := cond site.Config.Privacy.Vimeo.EnableDNT 1 0 -}}
{{- $url := urls.JoinPath "https://vimeo.com" .id -}}
{{- $query := querify "url" $url "dnt" $dnt -}}
{{- $request := printf "https://vimeo.com/api/oembed.json?%s" $query -}}
- {{- with resources.GetRemote $request -}}
+ {{- with try (resources.GetRemote $request) -}}
{{- with .Err -}}
- {{- errorf "%s" . -}}
- {{- else -}}
+ {{- warnidf "shortcode-vimeo-simple" "The %q shortcode was unable to retrieve the remote data: %s. See %s" $.ctx.Name . $.ctx.Position -}}
+ {{- else with .Value -}}
{{- with . | transform.Unmarshal -}}
{{- $class := printf "%s %s" "s_video_simple" "__h_video" -}}
{{- with $.class -}}
{{- $class = printf "%s %s" "s_video_simple" . -}}
{{- else -}}
- {{ template "__h_simple_css" $.page }}
+ {{ template "__h_simple_css" $.ctx.Page }}
{{- end -}}
{{- $thumbnail := .thumbnail_url -}}
{{- $original := $thumbnail | replaceRE "(_.*\\.)" "." -}}
{{- end -}}
+ {{- else -}}
+ {{- warnidf "shortcode-vimeo-simple" "The %q shortcode was unable to retrieve the remote data. See %s" $.ctx.Name $.ctx.Position -}}
{{- end -}}
- {{- else -}}
- {{- warnidf "shortcode-vimeo-simple" "The %q shortcode was unable to retrieve the remote data. See %s" .name .position -}}
{{- end -}}
{{- end -}}
diff --git a/tpl/tplimpl/embedded/templates/_shortcodes/x.html b/tpl/tplimpl/embedded/templates/_shortcodes/x.html
new file mode 100644
index 000000000..38bf0f7b6
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_shortcodes/x.html
@@ -0,0 +1,29 @@
+{{- $pc := site.Config.Privacy.X -}}
+{{- if not $pc.Disable -}}
+ {{- if $pc.Simple -}}
+ {{- template "_shortcodes/x_simple.html" . -}}
+ {{- else -}}
+ {{- $id := or (.Get "id") "" -}}
+ {{- $user := or (.Get "user") "" -}}
+ {{- if and $id $user -}}
+ {{- template "render-x" (dict "id" $id "user" $user "dnt" $pc.EnableDNT "ctx" .) -}}
+ {{- else -}}
+ {{- errorf "The %q shortcode requires two named parameters: user and id. See %s" .Name .Position -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+
+{{- define "render-x" -}}
+ {{- $url := printf "https://x.com/%v/status/%v" .user .id -}}
+ {{- $query := querify "url" $url "dnt" .dnt -}}
+ {{- $request := printf "https://publish.x.com/oembed?%s" $query -}}
+ {{- with try (resources.GetRemote $request) -}}
+ {{- with .Err -}}
+ {{- warnidf "shortcode-x-getremote" "The %q shortcode was unable to retrieve the remote data: %s. See %s" $.ctx.Name . $.ctx.Position -}}
+ {{- else with .Value -}}
+ {{- (. | transform.Unmarshal).html | safeHTML -}}
+ {{- else -}}
+ {{- warnidf "shortcode-x-getremote" "The %q shortcode was unable to retrieve the remote data. See %s" $.ctx.Name $.ctx.Position -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
diff --git a/tpl/tplimpl/embedded/templates/_shortcodes/x_simple.html b/tpl/tplimpl/embedded/templates/_shortcodes/x_simple.html
new file mode 100644
index 000000000..1e22835f6
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_shortcodes/x_simple.html
@@ -0,0 +1,60 @@
+{{- if not site.Config.Privacy.X.Disable -}}
+ {{- $id := or (.Get "id") "" -}}
+ {{- $user := or (.Get "user") "" -}}
+ {{- if and $id $user -}}
+ {{- template "render-simple-x" (dict "id" $id "user" $user "ctx" .) -}}
+ {{- else -}}
+ {{- errorf "The %q shortcode requires two named parameters: user and id. See %s" .Name .Position -}}
+ {{- end -}}
+{{- end -}}
+
+{{- define "render-simple-x" -}}
+ {{- $dnt := site.Config.Privacy.X.EnableDNT }}
+ {{- $url := printf "https://x.com/%v/status/%v" .user .id -}}
+ {{- $query := querify "url" $url "dnt" $dnt "omit_script" true -}}
+ {{- $request := printf "https://publish.x.com/oembed?%s" $query -}}
+ {{- with try (resources.GetRemote $request) -}}
+ {{- with .Err -}}
+ {{- warnidf "shortcode-x-simple-getremote" "The %q shortcode was unable to retrieve the remote data: %s. See %s" $.ctx.Name . $.ctx.Position -}}
+ {{- else with .Value -}}
+ {{- if not site.Config.Services.X.DisableInlineCSS }}
+ {{- template "__h_simple_x_css" (dict "ctx" $.ctx) }}
+ {{- end }}
+ {{- (. | transform.Unmarshal).html | safeHTML -}}
+ {{- else -}}
+ {{- warnidf "shortcode-x-simple-getremote" "The %q shortcode was unable to retrieve the remote data. See %s" $.ctx.Name $.ctx.Position -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+
+{{- define "__h_simple_x_css" -}}
+ {{- if not (.ctx.Page.Store.Get "__h_simple_x_css") -}}
+ {{/* Only include once */}}
+ {{- .ctx.Page.Store.Set "__h_simple_x_css" true -}}
+
+ {{- end -}}
+{{- end -}}
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/youtube.html b/tpl/tplimpl/embedded/templates/_shortcodes/youtube.html
similarity index 63%
rename from tpl/tplimpl/embedded/templates/shortcodes/youtube.html
rename to tpl/tplimpl/embedded/templates/_shortcodes/youtube.html
index 93fa18197..18b086944 100644
--- a/tpl/tplimpl/embedded/templates/shortcodes/youtube.html
+++ b/tpl/tplimpl/embedded/templates/_shortcodes/youtube.html
@@ -6,7 +6,7 @@ Renders an embedded YouTube video.
@param {string} [class] The class attribute of the wrapping div element. When specified, removes the style attributes from the iframe element and its wrapping div element.
@param {bool} [controls=true] Whether to display the video controls.
@param {int} [end] The time, measured in seconds from the start of the video, when the player should stop playing the video.
-@param {string} [id] The video id. Optional if the id is provided as first positional argument.
+@param {string} [id] The video id. Optional if the id is the first and only positional argument.
@param {string} [loading=eager] The loading attribute of the iframe element.
@param {bool} [loop=false] Whether to indefinitely repeat the video. Ignores the start and end arguments after the first play.
@param {bool} [mute=false] Whether to mute the video. Always true when autoplay is true.
@@ -26,8 +26,8 @@ Renders an embedded YouTube video.
{{- if not $pc.Disable }}
{{- with $id := or (.Get "id") (.Get 0) }}
- {{/* Set defaults. */}}
- {{- $allowFullScreen := "allowfullscreen" }}
+ {{- /* Set defaults. */}}
+ {{- $allowFullScreen := true }}
{{- $autoplay := 0 }}
{{- $class := "" }}
{{- $controls := 1 }}
@@ -37,32 +37,33 @@ Renders an embedded YouTube video.
{{- $mute := 0 }}
{{- $start := 0 }}
{{- $title := "YouTube video" }}
+ {{- $iframeAllowList := "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" }}
{{- /* Get arguments. */}}
- {{- if in (slice "false" false 0) ($.Get "allowFullScreen") }}
- {{- $allowFullScreen = "" }}
- {{- else if in (slice "true" true 1) ($.Get "allowFullScreen") }}
- {{- $allowFullScreen = "allowfullscreen" }}
+ {{- if in (slice "true" true 1) ($.Get "allowFullScreen") }}
+ {{- $allowFullScreen = true }}
+ {{- else if in (slice "false" false 0) ($.Get "allowFullScreen") }}
+ {{- $allowFullScreen = false }}
{{- end }}
- {{- if in (slice "false" false 0) ($.Get "autoplay") }}
- {{- $autoplay = 0 }}
- {{- else if in (slice "true" true 1) ($.Get "autoplay") }}
+ {{- if in (slice "true" true 1) ($.Get "autoplay") }}
{{- $autoplay = 1 }}
+ {{- else if in (slice "false" false 0) ($.Get "autoplay") }}
+ {{- $autoplay = 0 }}
{{- end }}
- {{- if in (slice "false" false 0) ($.Get "controls") }}
- {{- $controls = 0 }}
- {{- else if in (slice "true" true 1) ($.Get "controls") }}
+ {{- if in (slice "true" true 1) ($.Get "controls") }}
{{- $controls = 1 }}
+ {{- else if in (slice "false" false 0) ($.Get "controls") }}
+ {{- $controls = 0 }}
{{- end }}
- {{- if in (slice "false" false 0) ($.Get "loop") }}
- {{- $loop = 0 }}
- {{- else if in (slice "true" true 1) ($.Get "loop") }}
+ {{- if in (slice "true" true 1) ($.Get "loop") }}
{{- $loop = 1 }}
+ {{- else if in (slice "false" false 0) ($.Get "loop") }}
+ {{- $loop = 0 }}
{{- end }}
- {{- if in (slice "false" false 0) ($.Get "mute") }}
- {{- $mute = 0 }}
- {{- else if or (in (slice "true" true 1) ($.Get "mute")) $autoplay }}
+ {{- if or (in (slice "true" true 1) ($.Get "mute")) $autoplay }}
{{- $mute = 1 }}
+ {{- else if in (slice "false" false 0) ($.Get "mute") }}
+ {{- $mute = 0 }}
{{- end }}
{{- $class := or ($.Get "class") $class }}
{{- $end := or ($.Get "end") $end }}
@@ -70,23 +71,13 @@ Renders an embedded YouTube video.
{{- $start := or ($.Get "start") $start }}
{{- $title := or ($.Get "title") $title }}
- {{- /* Determine host. */}}
- {{- $host := cond $pc.PrivacyEnhanced "www.youtube-nocookie.com" "www.youtube.com" }}
-
- {{- /* Set styles. */}}
- {{- $divStyle := "position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;" }}
- {{- $iframeStyle := "position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" }}
- {{- if $class }}
- {{- $iframeStyle = "" }}
- {{- end }}
-
- {{- /* Set class or style of wrapping div element. */}}
- {{- $divClassOrStyle := printf "style=%q" $divStyle }}
- {{- with $class }}
- {{- $divClassOrStyle = printf "class=%q" $class }}
+ {{- /* Adjust iframeAllowList. */}}
+ {{- if $allowFullScreen }}
+ {{- $iframeAllowList = printf "%s; fullscreen" $iframeAllowList }}
{{- end }}
{{- /* Define src attribute. */}}
+ {{- $host := cond $pc.PrivacyEnhanced "www.youtube-nocookie.com" "www.youtube.com" }}
{{- $src := printf "https://%s/embed/%s" $host $id }}
{{- $params := dict
"autoplay" $autoplay
@@ -99,34 +90,35 @@ Renders an embedded YouTube video.
{{- if $loop }}
{{- $params = merge $params (dict "playlist" $id) }}
{{- end }}
- {{- $s := slice }}
- {{- range $k, $v := $params }}
- {{- $s = $s | append $k }}
- {{- $s = $s | append $v }}
- {{- end }}
- {{- with querify $s }}
+ {{- with querify $params }}
{{- $src = printf "%s?%s" $src . }}
{{- end }}
- {{- /* Set iframe attributes. */}}
- {{- $iframeAttributes := dict
- "allow" "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
- "allowfullscreen" $allowFullScreen
- "loading" $loading
- "referrerpolicy" "strict-origin-when-cross-origin"
- "src" $src
- "style" $iframeStyle
- "title" $title
- }}
+ {{- /* Set div attributes. */}}
+ {{- $divStyle := "position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;" }}
+ {{- if $class }}
+ {{- $divStyle = "" }}
+ {{- end }}
- {{- /* Render. */}}
-
+ {{- /* Set iframe attributes. */}}
+ {{- $iframeStyle := "position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" }}
+ {{- if $class }}
+ {{- $iframeStyle = "" }}
+ {{- end }}
+ {{- $referrerpolicy := "strict-origin-when-cross-origin" }}
+
+ {{- /* Render. */ -}}
+
{{- else }}
diff --git a/tpl/tplimpl/embedded/templates/robots.txt b/tpl/tplimpl/embedded/templates/robots.txt
new file mode 100644
index 000000000..7d329b1db
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/robots.txt
@@ -0,0 +1 @@
+User-agent: *
diff --git a/tpl/tplimpl/embedded/templates/_default/rss.xml b/tpl/tplimpl/embedded/templates/rss.xml
similarity index 81%
rename from tpl/tplimpl/embedded/templates/_default/rss.xml
rename to tpl/tplimpl/embedded/templates/rss.xml
index 2e505f1bc..2df2cae6e 100644
--- a/tpl/tplimpl/embedded/templates/_default/rss.xml
+++ b/tpl/tplimpl/embedded/templates/rss.xml
@@ -1,4 +1,3 @@
-{{- /* Deprecate site.Author.email in favor of site.Params.author.email */}}
{{- $authorEmail := "" }}
{{- with site.Params.author }}
{{- if reflect.IsMap . }}
@@ -6,14 +5,8 @@
{{- $authorEmail = . }}
{{- end }}
{{- end }}
-{{- else }}
- {{- with site.Author.email }}
- {{- $authorEmail = . }}
- {{- warnf "The author key in site configuration is deprecated. Use params.author.email instead." }}
- {{- end }}
{{- end }}
-{{- /* Deprecate site.Author.name in favor of site.Params.author.name */}}
{{- $authorName := "" }}
{{- with site.Params.author }}
{{- if reflect.IsMap . }}
@@ -23,11 +16,6 @@
{{- else }}
{{- $authorName = . }}
{{- end }}
-{{- else }}
- {{- with site.Author.name }}
- {{- $authorName = . }}
- {{- warnf "The author key in site configuration is deprecated. Use params.author.name instead." }}
- {{- end }}
{{- end }}
{{- $pctx := . }}
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/gist.html b/tpl/tplimpl/embedded/templates/shortcodes/gist.html
deleted file mode 100644
index 5f754f651..000000000
--- a/tpl/tplimpl/embedded/templates/shortcodes/gist.html
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/twitter_simple.html b/tpl/tplimpl/embedded/templates/shortcodes/twitter_simple.html
deleted file mode 100644
index 1f3b3c523..000000000
--- a/tpl/tplimpl/embedded/templates/shortcodes/twitter_simple.html
+++ /dev/null
@@ -1,49 +0,0 @@
-{{- $pc := .Page.Site.Config.Privacy.Twitter -}}
-{{- $sc := .Page.Site.Config.Services.Twitter -}}
-{{- if not $pc.Disable -}}
- {{- $id := or (.Get "id") "" -}}
- {{- $user := or (.Get "user") "" -}}
- {{- if and $id $user -}}
- {{- template "render-simple-tweet" (dict "id" $id "user" $user "dnt" $pc.EnableDNT "name" .Name "position" .Position) -}}
- {{- else -}}
- {{- errorf "The %q shortcode requires two named parameters: user and id. See %s" .Name .Position -}}
- {{- end -}}
-{{- end -}}
-
-{{- define "render-simple-tweet" -}}
- {{- $url := printf "https://twitter.com/%v/status/%v" .user .id -}}
- {{- $query := querify "url" $url "dnt" .dnt "omit_script" true -}}
- {{- $request := printf "https://publish.twitter.com/oembed?%s" $query -}}
- {{- with resources.GetRemote $request -}}
- {{- with .Err -}}
- {{- errorf "%s" . -}}
- {{- else -}}
- {{- (. | transform.Unmarshal).html | safeHTML -}}
- {{- end -}}
- {{- else -}}
- {{- warnidf "shortcode-twitter-simple-getremote" "The %q shortcode was unable to retrieve the remote data. See %s" .name .position -}}
- {{- end -}}
-{{- end -}}
-
-{{- define "__h_simple_twitter_css" -}}
- {{- if not (.Page.Scratch.Get "__h_simple_twitter_css") -}}
- {{/* Only include once */}}
- {{- .Page.Scratch.Set "__h_simple_twitter_css" true -}}
-
- {{- end -}}
-{{- end -}}
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/vimeo.html b/tpl/tplimpl/embedded/templates/shortcodes/vimeo.html
deleted file mode 100644
index 8ddad9b43..000000000
--- a/tpl/tplimpl/embedded/templates/shortcodes/vimeo.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{{- $pc := .Page.Site.Config.Privacy.Vimeo -}}
-{{- if not $pc.Disable -}}
-{{- if $pc.Simple -}}
-{{ template "_internal/shortcodes/vimeo_simple.html" . }}
-{{- else -}}
-{{ if .IsNamedParams }}
-
-
{{ else }}
-
-
-
-{{ end }}
-{{- end -}}
-{{- end -}}
\ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/_default/sitemap.xml b/tpl/tplimpl/embedded/templates/sitemap.xml
similarity index 100%
rename from tpl/tplimpl/embedded/templates/_default/sitemap.xml
rename to tpl/tplimpl/embedded/templates/sitemap.xml
diff --git a/tpl/tplimpl/embedded/templates/_default/sitemapindex.xml b/tpl/tplimpl/embedded/templates/sitemapindex.xml
similarity index 100%
rename from tpl/tplimpl/embedded/templates/_default/sitemapindex.xml
rename to tpl/tplimpl/embedded/templates/sitemapindex.xml
diff --git a/tpl/tplimpl/embedded/templates/twitter_cards.html b/tpl/tplimpl/embedded/templates/twitter_cards.html
deleted file mode 100644
index 14c92274b..000000000
--- a/tpl/tplimpl/embedded/templates/twitter_cards.html
+++ /dev/null
@@ -1,22 +0,0 @@
-{{- $images := partial "_funcs/get-page-images" . -}}
-{{- with index $images 0 -}}
-
-
-{{- else -}}
-
-{{- end -}}
-
-
-
-{{- $twitterSite := "" }}
-{{- with site.Params.social }}
- {{- if reflect.IsMap . }}
- {{- with .twitter }}
- {{- $content := . }}
- {{- if not (strings.HasPrefix . "@") }}
- {{- $content = printf "@%v" . }}
- {{- end }}
-
- {{- end }}
- {{- end }}
-{{- end }}
diff --git a/tpl/tplimpl/legacy.go b/tpl/tplimpl/legacy.go
new file mode 100644
index 000000000..ee9a6e5d1
--- /dev/null
+++ b/tpl/tplimpl/legacy.go
@@ -0,0 +1,130 @@
+// Copyright 2025 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tplimpl
+
+import (
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/resources/kinds"
+)
+
+type layoutLegacyMapping struct {
+ sourcePath string
+ target layoutLegacyMappingTarget
+}
+
+type layoutLegacyMappingTarget struct {
+ targetPath string
+ targetDesc TemplateDescriptor
+ targetCategory Category
+}
+
+var (
+ ltermPlural = layoutLegacyMappingTarget{
+ targetPath: "/PLURAL",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindTerm},
+ targetCategory: CategoryLayout,
+ }
+ ltermBase = layoutLegacyMappingTarget{
+ targetPath: "",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindTerm},
+ targetCategory: CategoryLayout,
+ }
+
+ ltaxPlural = layoutLegacyMappingTarget{
+ targetPath: "/PLURAL",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindTaxonomy},
+ targetCategory: CategoryLayout,
+ }
+ ltaxBase = layoutLegacyMappingTarget{
+ targetPath: "",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindTaxonomy},
+ targetCategory: CategoryLayout,
+ }
+
+ lsectBase = layoutLegacyMappingTarget{
+ targetPath: "",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindSection},
+ targetCategory: CategoryLayout,
+ }
+ lsectTheSection = layoutLegacyMappingTarget{
+ targetPath: "/THESECTION",
+ targetDesc: TemplateDescriptor{Kind: kinds.KindSection},
+ targetCategory: CategoryLayout,
+ }
+)
+
+type legacyTargetPathIdentifiers struct {
+ targetPath string
+ targetCategory Category
+ kind string
+ lang string
+ outputFormat string
+ ext string
+}
+
+type legacyOrdinalMapping struct {
+ ordinal int
+ mapping layoutLegacyMappingTarget
+}
+
+type legacyOrdinalMappingFi struct {
+ m legacyOrdinalMapping
+ fi hugofs.FileMetaInfo
+}
+
+var legacyTermMappings = []layoutLegacyMapping{
+ {sourcePath: "/PLURAL/term", target: ltermPlural},
+ {sourcePath: "/PLURAL/SINGULAR", target: ltermPlural},
+ {sourcePath: "/term/term", target: ltermBase},
+ {sourcePath: "/term/SINGULAR", target: ltermPlural},
+ {sourcePath: "/term/taxonomy", target: ltermPlural},
+ {sourcePath: "/term/list", target: ltermBase},
+ {sourcePath: "/taxonomy/term", target: ltermBase},
+ {sourcePath: "/taxonomy/SINGULAR", target: ltermPlural},
+ {sourcePath: "/SINGULAR/term", target: ltermPlural},
+ {sourcePath: "/SINGULAR/SINGULAR", target: ltermPlural},
+ {sourcePath: "/_default/SINGULAR", target: ltermPlural},
+ {sourcePath: "/_default/taxonomy", target: ltermBase},
+}
+
+var legacyTaxonomyMappings = []layoutLegacyMapping{
+ {sourcePath: "/PLURAL/SINGULAR.terms", target: ltaxPlural},
+ {sourcePath: "/PLURAL/terms", target: ltaxPlural},
+ {sourcePath: "/PLURAL/taxonomy", target: ltaxPlural},
+ {sourcePath: "/PLURAL/list", target: ltaxPlural},
+ {sourcePath: "/SINGULAR/SINGULAR.terms", target: ltaxPlural},
+ {sourcePath: "/SINGULAR/terms", target: ltaxPlural},
+ {sourcePath: "/SINGULAR/taxonomy", target: ltaxPlural},
+ {sourcePath: "/SINGULAR/list", target: ltaxPlural},
+ {sourcePath: "/taxonomy/SINGULAR.terms", target: ltaxPlural},
+ {sourcePath: "/taxonomy/terms", target: ltaxBase},
+ {sourcePath: "/taxonomy/taxonomy", target: ltaxBase},
+ {sourcePath: "/taxonomy/list", target: ltaxBase},
+ {sourcePath: "/_default/SINGULAR.terms", target: ltaxBase},
+ {sourcePath: "/_default/terms", target: ltaxBase},
+ {sourcePath: "/_default/taxonomy", target: ltaxBase},
+}
+
+var legacySectionMappings = []layoutLegacyMapping{
+ // E.g. /mysection/mysection.html
+ {sourcePath: "/THESECTION/THESECTION", target: lsectTheSection},
+ // E.g. /section/mysection.html
+ {sourcePath: "/SECTIONKIND/THESECTION", target: lsectTheSection},
+ // E.g. /section/section.html
+ {sourcePath: "/SECTIONKIND/SECTIONKIND", target: lsectBase},
+ // E.g. /section/list.html
+ {sourcePath: "/SECTIONKIND/list", target: lsectBase},
+ // E.g. /_default/mysection.html
+ {sourcePath: "/_default/THESECTION", target: lsectTheSection},
+}
diff --git a/tpl/tplimpl/legacy_integration_test.go b/tpl/tplimpl/legacy_integration_test.go
new file mode 100644
index 000000000..a96e35fca
--- /dev/null
+++ b/tpl/tplimpl/legacy_integration_test.go
@@ -0,0 +1,38 @@
+// Copyright 2025 The Hugo Authors. All rights reserved.
+//
+// Portions Copyright The Go Authors.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tplimpl_test
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/hugolib"
+)
+
+func TestLegacyPartialIssue13599(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/partials/mypartial.html --
+Mypartial.
+-- layouts/_default/index.html --
+mypartial: {{ template "partials/mypartial.html" . }}
+
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Mypartial.")
+}
diff --git a/tpl/tplimpl/render_hook_integration_test.go b/tpl/tplimpl/render_hook_integration_test.go
index 07c24108d..45c6cced1 100644
--- a/tpl/tplimpl/render_hook_integration_test.go
+++ b/tpl/tplimpl/render_hook_integration_test.go
@@ -1,4 +1,4 @@
-// Copyright 2024 The Hugo Authors. All rights reserved.
+// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -51,7 +51,8 @@ title: s1/p1
title: s1/p2
---
[500](a.txt) // global resource
-[600](b.txt) // page resource
+[510](b.txt) // page resource
+[520](./b.txt) // page resource
-- content/s1/p2/b.txt --
irrelevant
-- content/s1/p3.md --
@@ -90,6 +91,9 @@ title: s1/p3
[430](p2/)
[440](/s1/p2/)
[450](../s1/p2/)
+
+// empty
+[]()
`
b := hugolib.Test(t, files)
@@ -121,39 +125,70 @@ title: s1/p3
`
430 `,
`
440 `,
`
450 `,
+
+ `
`,
)
b.AssertFileContent("public/s1/p2/index.html",
`
500 `,
- `
600 `,
+ `
510 `,
+ `
520 `,
)
}
// Issue 12203
-func TestEmbeddedImageRenderHookMarkdownAttributes(t *testing.T) {
+// Issue 12468
+// Issue 12514
+func TestEmbeddedImageRenderHook(t *testing.T) {
t.Parallel()
files := `
-- config.toml --
-disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+baseURL = 'https://example.org/dir/'
+disableKinds = ['home','rss','section','sitemap','taxonomy','term']
+[markup.goldmark.extensions.typographer]
+disable = true
[markup.goldmark.parser]
wrapStandAloneImageWithinParagraph = false
[markup.goldmark.parser.attribute]
block = false
[markup.goldmark.renderHooks.image]
enableDefault = true
--- content/_index.md --
-
+-- content/p1/index.md --
+![]()
+
+
+
+
+
+
{.foo #bar}
--- layouts/index.html --
+
+
+{id="\">"}
+-- content/p1/pixel.png --
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
+-- layouts/_default/single.html --
{{ .Content }}
`
b := hugolib.Test(t, files)
- b.AssertFileContent("public/index.html", `
`)
+ b.AssertFileContent("public/p1/index.html",
+ `
`,
+ `
`,
+ `
`,
+ `
`,
+ `
`,
+ )
files = strings.Replace(files, "block = false", "block = true", -1)
b = hugolib.Test(t, files)
- b.AssertFileContent("public/index.html", `
`)
+ b.AssertFileContent("public/p1/index.html",
+ `
`,
+ `
`,
+ `
`,
+ `
`,
+ `
`,
+ )
}
diff --git a/tpl/tplimpl/shortcodes.go b/tpl/tplimpl/shortcodes.go
deleted file mode 100644
index a1e7b463e..000000000
--- a/tpl/tplimpl/shortcodes.go
+++ /dev/null
@@ -1,153 +0,0 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package tplimpl
-
-import (
- "strings"
-
- "github.com/gohugoio/hugo/tpl"
-)
-
-// Currently lang, outFormat, suffix
-const numTemplateVariants = 3
-
-type shortcodeVariant struct {
- // The possible variants: lang, outFormat, suffix
- // gtag
- // gtag.html
- // gtag.no.html
- // gtag.no.amp.html
- // A slice of length numTemplateVariants.
- variants []string
-
- ts *templateState
-}
-
-type shortcodeTemplates struct {
- variants []shortcodeVariant
-}
-
-func (s *shortcodeTemplates) indexOf(variants []string) int {
-L:
- for i, v1 := range s.variants {
- for i, v2 := range v1.variants {
- if v2 != variants[i] {
- continue L
- }
- }
- return i
- }
- return -1
-}
-
-func (s *shortcodeTemplates) fromVariants(variants tpl.TemplateVariants) (shortcodeVariant, bool) {
- return s.fromVariantsSlice([]string{
- variants.Language,
- strings.ToLower(variants.OutputFormat.Name),
- variants.OutputFormat.MediaType.FirstSuffix.Suffix,
- })
-}
-
-func (s *shortcodeTemplates) fromVariantsSlice(variants []string) (shortcodeVariant, bool) {
- var (
- bestMatch shortcodeVariant
- bestMatchWeight int
- )
-
- for _, variant := range s.variants {
- w := s.compareVariants(variants, variant.variants)
- if bestMatchWeight == 0 || w > bestMatchWeight {
- bestMatch = variant
- bestMatchWeight = w
- }
- }
-
- return bestMatch, true
-}
-
-// calculate a weight for two string slices of same length.
-// higher value means "better match".
-func (s *shortcodeTemplates) compareVariants(a, b []string) int {
- weight := 0
- k := len(a)
- for i, av := range a {
- bv := b[i]
- if av == bv {
- // Add more weight to the left side (language...).
- weight = weight + k - i
- } else {
- weight--
- }
- }
- return weight
-}
-
-func templateVariants(name string) []string {
- _, variants := templateNameAndVariants(name)
- return variants
-}
-
-func templateNameAndVariants(name string) (string, []string) {
- variants := make([]string, numTemplateVariants)
-
- parts := strings.Split(name, ".")
-
- if len(parts) <= 1 {
- // No variants.
- return name, variants
- }
-
- name = parts[0]
- parts = parts[1:]
- lp := len(parts)
- start := len(variants) - lp
-
- for i, j := start, 0; i < len(variants); i, j = i+1, j+1 {
- variants[i] = parts[j]
- }
-
- if lp > 1 && lp < len(variants) {
- for i := lp - 1; i > 0; i-- {
- variants[i-1] = variants[i]
- }
- }
-
- if lp == 1 {
- // Suffix only. Duplicate it into the output format field to
- // make HTML win over AMP.
- variants[len(variants)-2] = variants[len(variants)-1]
- }
-
- return name, variants
-}
-
-func resolveTemplateType(name string) templateType {
- if isShortcode(name) {
- return templateShortcode
- }
-
- if strings.Contains(name, "partials/") {
- return templatePartial
- }
-
- return templateUndefined
-}
-
-func isShortcode(name string) bool {
- return strings.Contains(name, shortcodesPathPrefix)
-}
-
-func isInternal(name string) bool {
- return strings.HasPrefix(name, internalPathPrefix)
-}
diff --git a/tpl/tplimpl/shortcodes_integration_test.go b/tpl/tplimpl/shortcodes_integration_test.go
new file mode 100644
index 000000000..86f6007ca
--- /dev/null
+++ b/tpl/tplimpl/shortcodes_integration_test.go
@@ -0,0 +1,796 @@
+// Copyright 2025 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tplimpl_test
+
+import (
+ "strings"
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/htesting/hqt"
+ "github.com/gohugoio/hugo/hugolib"
+)
+
+func TestCommentShortcode(t *testing.T) {
+ // This cannot be parallel as it depends on output from the global logger.
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- layouts/index.html --
+{{ .Content }}
+-- content/_index.md --
+---
+title: home
+---
+a{{< comment >}}b{{< /comment >}}c
+`
+
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+ b.AssertFileContent("public/index.html", "
ac
")
+ b.AssertLogContains(`WARN The "comment" shortcode was deprecated in v0.143.0 and will be removed in a future release. Please use HTML comments instead.`)
+}
+
+func TestDetailsShortcode(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- layouts/index.html --
+{{ .Content }}
+-- content/_index.md --
+---
+title: home
+---
+{{< details >}}
+A: An _emphasized_ word.
+{{< /details >}}
+
+{{< details
+ class="my-class"
+ name="my-name"
+ open=true
+ summary="A **bold** word"
+ title="my-title"
+>}}
+B: An _emphasized_ word.
+{{< /details >}}
+
+{{< details open=false >}}
+C: An _emphasized_ word.
+{{< /details >}}
+
+{{< details open="false" >}}
+D: An _emphasized_ word.
+{{< /details >}}
+
+{{< details open=0 >}}
+E: An _emphasized_ word.
+{{< /details >}}
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html",
+ "
\n Details \n A: An emphasized word.
\n ",
+ "
\n A bold word \n B: An emphasized word.
\n ",
+ "
\n Details \n C: An emphasized word.
\n ",
+ "
\n Details \n D: An emphasized word.
\n ",
+ "
\n Details \n D: An emphasized word.
\n ",
+ )
+}
+
+func TestFigureShortcode(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- content/_index.md --
+---
+title: home
+---
+{{< figure
+ src="a.jpg"
+ alt="alternate text"
+ width=600
+ height=400
+ loading="lazy"
+ class="my-class"
+ link="https://example.org"
+ target="_blank"
+ rel="noopener"
+ title="my-title"
+ caption="a **bold** word"
+ attr="an _emphasized_ word"
+ attrlink="https://example.org/foo"
+>}}
+-- layouts/index.html --
+Hash: {{ .Content | hash.XxHash }}
+Content: {{ .Content }}
+`
+
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.html", "35b077dcb9887a84")
+}
+
+func TestGistShortcode(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- layouts/index.html --
+{{ .Content }}
+-- content/_index.md --
+---
+title: home
+---
+{{< gist jmooring 23932424365401ffa5e9d9810102a477 >}}
+`
+
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+ b.AssertFileContent("public/index.html", ``)
+ b.AssertLogContains(`WARN The "gist" shortcode was deprecated in v0.143.0 and will be removed in a future release. See https://gohugo.io/shortcodes/gist for instructions to create a replacement.`)
+}
+
+func TestHighlightShortcode(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['home','rss','section','sitemap','taxonomy','term']
+-- layouts/_default/single.html --
+Hash: {{ .Content | hash.XxHash }}
+Content: {{ .Content }}
+-- content/p1.md --
+---
+title: p1
+---
+{{< highlight go >}}
+func main() {
+ for i := 0; i < 3; i++ {
+ fmt.Println("Value of i:", i)
+ }
+}
+{{< /highlight >}}
+-- content/p2.md --
+---
+title: p2
+---
+{{< highlight go "noClasses=false" >}}
+func main() {
+ for i := 0; i < 3; i++ {
+ fmt.Println("Value of i:", i)
+ }
+}
+{{< /highlight >}}
+-- content/p3.md --
+---
+title: p3
+---
+{{< highlight go "lineNos=inline" >}}
+func main() {
+ for i := 0; i < 3; i++ {
+ fmt.Println("Value of i:", i)
+ }
+}
+{{< /highlight >}}
+-- content/p4.md --
+---
+title: p4
+---
+{{< highlight go "lineNos=table" >}}
+func main() {
+ for i := 0; i < 3; i++ {
+ fmt.Println("Value of i:", i)
+ }
+}
+{{< /highlight >}}
+-- content/p5.md --
+---
+title: p5
+---
+{{< highlight go "anchorLineNos=true, hl_Lines=2-4, lineAnchors=foo, lineNoStart=42, lineNos=true, lineNumbersInTable=false, style=emacs, wrapperClass=my-class" >}}
+func main() {
+ for i := 0; i < 3; i++ {
+ fmt.Println("Value of i:", i)
+ }
+}
+{{< /highlight >}}
+-- content/p6.md --
+---
+title: p6
+---
+{{< highlight go "anchorlinenos=true, hl_lines=2-4, lineanchors=foo, linenostart=42, linenos=true, linenumbersintable=false, style=emacs, wrapperclass=my-class" >}}
+func main() {
+ for i := 0; i < 3; i++ {
+ fmt.Println("Value of i:", i)
+ }
+}
+{{< /highlight >}}
+-- content/p7.md --
+---
+title: p7
+---
+An inline {{< highlight go "hl_inline=true" >}}fmt.Println("Value of i:", i)Hello world!{{< /highlight >}} statement.
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html", "576ee13be18ddba2")
+ b.AssertFileContent("public/p2/index.html", "8774e8b4bf60aa9e")
+ b.AssertFileContent("public/p3/index.html", "7634b47df1859f58")
+ b.AssertFileContent("public/p4/index.html", "385a15e400df4e39")
+ b.AssertFileContent("public/p5/index.html", "b3a73f3eddc6e0c1")
+ b.AssertFileContent("public/p6/index.html", "b3a73f3eddc6e0c1")
+ b.AssertFileContent("public/p7/index.html", "f12eeaa4d6d9c7ac")
+}
+
+func TestInstagramShortcode(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+privacy.instagram.simple = false
+-- content/_index.md --
+---
+title: home
+---
+{{< instagram CxOWiQNP2MO >}}
+-- layouts/index.html --
+Hash: {{ .Content | hash.XxHash }}
+Content: {{ .Content }}
+`
+
+ // Regular mode
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.html", "6e93404b93277876")
+
+ // Simple mode
+ files = strings.ReplaceAll(files, "privacy.instagram.simple = false", "privacy.instagram.simple = true")
+ b = hugolib.Test(t, files)
+ b.AssertFileContent("public/index.html", "2c1dce3881be0513")
+}
+
+func TestParamShortcode(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+[params]
+b = 2
+-- layouts/index.html --
+{{ .Content }}
+-- content/_index.md --
+---
+title: home
+params:
+ a: 1
+---
+A: {{% param "a" %}}
+B: {{% param "b" %}}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileExists("public/index.html", true)
+ b.AssertFileContent("public/index.html",
+ "A: 1",
+ "B: 2",
+ )
+}
+
+func TestQRShortcode(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- layouts/index.html --
+{{ .Content }}
+-- content/_index.md --
+---
+title: home
+---
+{{< qr
+ text="https://gohugo.io"
+ level="high"
+ scale=4
+ targetDir="codes"
+ alt="QR code linking to https://gohugo.io"
+ class="my-class"
+ id="my-id"
+ title="My Title"
+/>}}
+
+{{< qr >}}
+https://gohugo.io"
+{{< /qr >}}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html",
+ `
`,
+ `
`,
+ )
+}
+
+func TestRefShortcode(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+baseURL = 'https://example.org/'
+disableKinds = ['rss','section','sitemap','taxonomy','term']
+defaultContentLanguageInSubdir = true
+[markup.goldmark.extensions]
+linkify = false
+[languages.en]
+weight = 1
+[languages.de]
+weight = 2
+[outputs]
+page = ['html','json']
+-- layouts/_default/home.html --
+{{ .Content }}
+-- layouts/_default/single.html.html --
+{{ .Title }}
+-- layouts/_default/single.json.json --
+{{ .Title }}
+-- content/_index.en.md --
+---
+title: home
+---
+A: {{% ref "/s1/p1.md" %}}
+
+B: {{% ref path="/s1/p1.md" %}}
+
+C: {{% ref path="/s1/p1.md" lang="en" %}}
+
+D: {{% ref path="/s1/p1.md" lang="de" %}}
+
+E: {{% ref path="/s1/p1.md" lang="en" outputFormat="html" %}}
+
+F: {{% ref path="/s1/p1.md" lang="de" outputFormat="html" %}}
+
+G: {{% ref path="/s1/p1.md" lang="en" outputFormat="json" %}}
+
+H: {{% ref path="/s1/p1.md" lang="de" outputFormat="json" %}}
+-- content/s1/p1.en.md --
+---
+title: p1 (en)
+---
+-- content/s1/p1.de.md --
+---
+title: p1 (de)
+---
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/en/index.html",
+ `p>A: https://example.org/en/s1/p1/`,
+ `p>B: https://example.org/en/s1/p1/`,
+ `p>C: https://example.org/en/s1/p1/`,
+ `p>D: https://example.org/de/s1/p1/`,
+ `p>E: https://example.org/en/s1/p1/`,
+ `p>F: https://example.org/de/s1/p1/`,
+ `p>G: https://example.org/en/s1/p1/index.json`,
+ `p>H: https://example.org/de/s1/p1/index.json`,
+ )
+}
+
+func TestRelRefShortcode(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+baseURL = 'https://example.org/'
+disableKinds = ['rss','section','sitemap','taxonomy','term']
+defaultContentLanguageInSubdir = true
+[markup.goldmark.extensions]
+linkify = false
+[languages.en]
+weight = 1
+[languages.de]
+weight = 2
+[outputs]
+page = ['html','json']
+-- layouts/_default/home.html --
+{{ .Content }}
+-- layouts/_default/single.html.html --
+{{ .Title }}
+-- layouts/_default/single.json.json --
+{{ .Title }}
+-- content/_index.en.md --
+---
+title: home
+---
+A: {{% relref "/s1/p1.md" %}}
+
+B: {{% relref path="/s1/p1.md" %}}
+
+C: {{% relref path="/s1/p1.md" lang="en" %}}
+
+D: {{% relref path="/s1/p1.md" lang="de" %}}
+
+E: {{% relref path="/s1/p1.md" lang="en" outputFormat="html" %}}
+
+F: {{% relref path="/s1/p1.md" lang="de" outputFormat="html" %}}
+
+G: {{% relref path="/s1/p1.md" lang="en" outputFormat="json" %}}
+
+H: {{% relref path="/s1/p1.md" lang="de" outputFormat="json" %}}
+-- content/s1/p1.en.md --
+---
+title: p1 (en)
+---
+-- content/s1/p1.de.md --
+---
+title: p1 (de)
+---
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/en/index.html",
+ `p>A: /en/s1/p1/`,
+ `p>B: /en/s1/p1/`,
+ `p>C: /en/s1/p1/`,
+ `p>D: /de/s1/p1/`,
+ `p>E: /en/s1/p1/`,
+ `p>F: /de/s1/p1/`,
+ `p>G: /en/s1/p1/index.json`,
+ `p>H: /de/s1/p1/index.json`,
+ )
+}
+
+func TestVimeoShortcode(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['home','rss','section','sitemap','taxonomy','term']
+privacy.vimeo.simple = false
+-- content/p1.md --
+---
+title: p1
+---
+{{< vimeo 55073825 >}}
+-- content/p2.md --
+---
+title: p2
+---
+{{< vimeo id=55073825 allowFullScreen=true >}}
+-- content/p3.md --
+---
+title: p3
+---
+{{< vimeo id=55073825 allowFullScreen=false >}}
+-- layouts/_default/single.html --
+Hash: {{ .Content | hash.XxHash }}
+Content: {{ .Content }}
+`
+
+ // Regular mode
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/p1/index.html", "82566e6b8d04b53e")
+ b.AssertFileContent("public/p2/index.html", "82566e6b8d04b53e")
+ b.AssertFileContent("public/p3/index.html", "2b5f9cc3167d1336")
+
+ // Simple mode
+ files = strings.ReplaceAll(files, "privacy.vimeo.simple = false", "privacy.vimeo.simple = true")
+ b = hugolib.Test(t, files)
+ b.AssertFileContent("public/p1/index.html", "04d861fc957ee638")
+
+ // Simple mode with non-existent id
+ files = strings.ReplaceAll(files, "{{< vimeo 55073825 >}}", "{{< vimeo __id_does_not_exist__ >}}")
+ b = hugolib.Test(t, files, hugolib.TestOptWarn())
+ b.AssertLogContains(`WARN The "vimeo" shortcode was unable to retrieve the remote data.`)
+}
+
+// Issue 13214
+// We deprecated the twitter, tweet (alias of twitter), and twitter_simple
+// shortcodes in v0.141.0, replacing them with x and x_simple.
+func TestXShortcodes(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['home','rss','section','sitemap','taxonomy','term']
+#CONFIG
+-- content/p1.md --
+---
+title: p1
+---
+{{< x user="SanDiegoZoo" id="1453110110599868418" >}}
+-- content/p2.md --
+---
+title: p2
+---
+{{< twitter user="SanDiegoZoo" id="1453110110599868418" >}}
+-- content/p3.md --
+---
+title: p3
+---
+{{< tweet user="SanDiegoZoo" id="1453110110599868418" >}}
+-- content/p4.md --
+---
+title: p4
+---
+{{< x_simple user="SanDiegoZoo" id="1453110110599868418" >}}
+-- content/p5.md --
+---
+title: p5
+---
+{{< twitter_simple user="SanDiegoZoo" id="1453110110599868418" >}}
+-- layouts/_default/single.html --
+{{ .Content | strings.TrimSpace | safeHTML }}
+--
+`
+
+ b := hugolib.Test(t, files)
+
+ // Test x, twitter, and tweet shortcodes
+ want := `
+ `
+ b.AssertFileContent("public/p1/index.html", want)
+
+ htmlFiles := []string{
+ b.FileContent("public/p1/index.html"),
+ b.FileContent("public/p2/index.html"),
+ b.FileContent("public/p3/index.html"),
+ }
+
+ b.Assert(htmlFiles, hqt.IsAllElementsEqual)
+
+ // Test x_simple and twitter_simple shortcodes
+ wantSimple := "\n--"
+ b.AssertFileContent("public/p4/index.html", wantSimple)
+
+ htmlFiles = []string{
+ b.FileContent("public/p4/index.html"),
+ b.FileContent("public/p5/index.html"),
+ }
+ b.Assert(htmlFiles, hqt.IsAllElementsEqual)
+
+ filesOriginal := files
+
+ // Test privacy.twitter.simple
+ files = strings.ReplaceAll(filesOriginal, "#CONFIG", "privacy.twitter.simple=true")
+ b = hugolib.Test(t, files)
+ htmlFiles = []string{
+ b.FileContent("public/p2/index.html"),
+ b.FileContent("public/p3/index.html"),
+ b.FileContent("public/p5/index.html"),
+ }
+ b.Assert(htmlFiles, hqt.IsAllElementsEqual)
+
+ // Test privacy.x.simple
+ files = strings.ReplaceAll(filesOriginal, "#CONFIG", "privacy.x.simple=true")
+ b = hugolib.Test(t, files)
+ htmlFiles = []string{
+ b.FileContent("public/p1/index.html"),
+ b.FileContent("public/p4/index.html"),
+ b.FileContent("public/p4/index.html"),
+ }
+ b.Assert(htmlFiles, hqt.IsAllElementsEqual)
+
+ htmlFiles = []string{
+ b.FileContent("public/p2/index.html"),
+ b.FileContent("public/p3/index.html"),
+ }
+ b.Assert(htmlFiles, hqt.IsAllElementsEqual)
+
+ // Test privacy.twitter.disable
+ files = strings.ReplaceAll(filesOriginal, "#CONFIG", "privacy.twitter.disable = true")
+ b = hugolib.Test(t, files)
+ b.AssertFileContent("public/p1/index.html", "")
+ htmlFiles = []string{
+ b.FileContent("public/p1/index.html"),
+ b.FileContent("public/p2/index.html"),
+ b.FileContent("public/p3/index.html"),
+ b.FileContent("public/p4/index.html"),
+ b.FileContent("public/p4/index.html"),
+ }
+ b.Assert(htmlFiles, hqt.IsAllElementsEqual)
+
+ // Test privacy.x.disable
+ files = strings.ReplaceAll(filesOriginal, "#CONFIG", "privacy.x.disable = true")
+ b = hugolib.Test(t, files)
+ b.AssertFileContent("public/p1/index.html", "")
+ htmlFiles = []string{
+ b.FileContent("public/p1/index.html"),
+ b.FileContent("public/p4/index.html"),
+ }
+ b.Assert(htmlFiles, hqt.IsAllElementsEqual)
+
+ htmlFiles = []string{
+ b.FileContent("public/p2/index.html"),
+ b.FileContent("public/p3/index.html"),
+ }
+ b.Assert(htmlFiles, hqt.IsAllElementsEqual)
+
+ // Test warnings
+ files = `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- content/_index.md --
+---
+title: home
+---
+{{< x user="__user_does_not_exist__" id="__id_does_not_exist__" >}}
+{{< x_simple user="__user_does_not_exist__" id="__id_does_not_exist__" >}}
+{{< twitter user="__user_does_not_exist__" id="__id_does_not_exist__" >}}
+{{< twitter_simple user="__user_does_not_exist__" id="__id_does_not_exist__" >}}
+-- layouts/index.html --
+{{ .Content }}
+`
+
+ b = hugolib.Test(t, files, hugolib.TestOptWarn())
+ b.AssertLogContains(
+ `WARN The "x" shortcode was unable to retrieve the remote data.`,
+ `WARN The "x_simple" shortcode was unable to retrieve the remote data.`,
+ `WARN The "twitter", "tweet", and "twitter_simple" shortcodes were deprecated in v0.142.0 and will be removed in a future release.`,
+ `WARN The "twitter" shortcode was unable to retrieve the remote data.`,
+ `WARN The "twitter_simple" shortcode was unable to retrieve the remote data.`,
+ )
+}
+
+func TestYouTubeShortcode(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['home','rss','section','sitemap','taxonomy','term']
+privacy.youtube.privacyEnhanced = false
+-- layouts/_default/single.html --
+Hash: {{ .Content | hash.XxHash }}
+Content: {{ .Content }}
+-- content/p1.md --
+---
+title: p1
+---
+{{< youtube 0RKpf3rK57I >}}
+-- content/p2.md --
+---
+title: p2
+---
+{{< youtube
+ id="0RKpf3rK57I"
+ allowFullScreen=false
+ autoplay=true
+ class="my-class"
+ controls="false"
+ end=42
+ loading="lazy"
+ loop=true
+ mute=true
+ start=6
+ title="my title"
+>}}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html", "4b54bf9bd03946ec")
+ b.AssertFileContent("public/p2/index.html", "289c655e727e596c")
+
+ files = strings.ReplaceAll(files, "privacy.youtube.privacyEnhanced = false", "privacy.youtube.privacyEnhanced = true")
+
+ b = hugolib.Test(t, files)
+ b.AssertFileContent("public/p1/index.html", "78eb19b5c6f3768f")
+ b.AssertFileContent("public/p2/index.html", "a6db910a9cf54bc1")
+}
+
+func TestShortcodePlainTextVsHTMLTemplateIssue13698(t *testing.T) {
+ t.Parallel()
+
+ filesTemplate := `
+-- hugo.toml --
+markup.goldmark.renderer.unsafe = true
+-- layouts/all.html --
+Content: {{ .Content }}|
+-- layouts/_shortcodes/mymarkdown.md --
+
Foo bar
+-- content/p1.md --
+---
+title: p1
+---
+## A shortcode
+
+SHORTCODE
+
+`
+
+ files := strings.ReplaceAll(filesTemplate, "SHORTCODE", "{{% mymarkdown %}}")
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/p1/index.html", "
Foo bar
")
+
+ files = strings.ReplaceAll(filesTemplate, "SHORTCODE", "{{< mymarkdown >}}")
+
+ var err error
+ b, err = hugolib.TestE(t, files)
+ b.Assert(err, qt.IsNotNil)
+ b.Assert(err.Error(), qt.Contains, `no compatible template found for shortcode "mymarkdown" in [/_shortcodes/mymarkdown.md]; note that to use plain text template shortcodes in HTML you need to use the shortcode {{% delimiter`)
+}
+
+func TestShortcodeOnlyLanguageInBaseIssue13699And13740(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+baseURL = 'https://example.org/'
+disableLanguages = ['de']
+[languages]
+[languages.en]
+weight = 1
+[languages.de]
+weight = 2
+-- layouts/_shortcodes/de.html --
+de.html
+-- layouts/all.html --
+{{ .Content }}
+-- content/_index.md --
+---
+title: home
+---
+{{< de >}}
+
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "de.html")
+}
+
+func TestShortcodeLanguage13767(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+defaultContentLanguage = 'pl'
+defaultContentLanguageInSubdir = true
+[languages.pl]
+weight = 1
+[languages.en]
+weight = 2
+-- content/_index.md --
+---
+title: dom
+---
+{{< myshortcode >}}
+-- content/_index.en.md --
+---
+title: home
+---
+{{< myshortcode >}}
+-- layouts/_shortcodes/myshortcode.html --
+myshortcode.html
+-- layouts/_shortcodes/myshortcode.en.html --
+myshortcode.en.html
+-- layouts/all.html --
+{{ .Content }}
+
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/pl/index.html", "myshortcode.html")
+ b.AssertFileContent("public/en/index.html", "myshortcode.en.html")
+}
diff --git a/tpl/tplimpl/shortcodes_test.go b/tpl/tplimpl/shortcodes_test.go
deleted file mode 100644
index f97c7f278..000000000
--- a/tpl/tplimpl/shortcodes_test.go
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package tplimpl
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
-)
-
-func TestShortcodesTemplate(t *testing.T) {
- t.Run("isShortcode", func(t *testing.T) {
- c := qt.New(t)
- c.Assert(isShortcode("shortcodes/figures.html"), qt.Equals, true)
- c.Assert(isShortcode("_internal/shortcodes/figures.html"), qt.Equals, true)
- c.Assert(isShortcode("shortcodes\\figures.html"), qt.Equals, false)
- c.Assert(isShortcode("myshortcodes"), qt.Equals, false)
- })
-
- t.Run("variantsFromName", func(t *testing.T) {
- c := qt.New(t)
- c.Assert(templateVariants("figure.html"), qt.DeepEquals, []string{"", "html", "html"})
- c.Assert(templateVariants("figure.no.html"), qt.DeepEquals, []string{"no", "no", "html"})
- c.Assert(templateVariants("figure.no.amp.html"), qt.DeepEquals, []string{"no", "amp", "html"})
- c.Assert(templateVariants("figure.amp.html"), qt.DeepEquals, []string{"amp", "amp", "html"})
-
- name, variants := templateNameAndVariants("figure.html")
- c.Assert(name, qt.Equals, "figure")
- c.Assert(variants, qt.DeepEquals, []string{"", "html", "html"})
- })
-
- t.Run("compareVariants", func(t *testing.T) {
- c := qt.New(t)
- var s *shortcodeTemplates
-
- tests := []struct {
- name string
- name1 string
- name2 string
- expected int
- }{
- {"Same suffix", "figure.html", "figure.html", 6},
- {"Same suffix and output format", "figure.html.html", "figure.html.html", 6},
- {"Same suffix, output format and language", "figure.no.html.html", "figure.no.html.html", 6},
- {"No suffix", "figure", "figure", 6},
- {"Different output format", "figure.amp.html", "figure.html.html", -1},
- {"One with output format, one without", "figure.amp.html", "figure.html", -1},
- }
-
- for _, test := range tests {
- w := s.compareVariants(templateVariants(test.name1), templateVariants(test.name2))
- c.Assert(w, qt.Equals, test.expected)
- }
- })
-
- t.Run("indexOf", func(t *testing.T) {
- c := qt.New(t)
-
- s := &shortcodeTemplates{
- variants: []shortcodeVariant{
- {variants: []string{"a", "b", "c"}},
- {variants: []string{"a", "b", "d"}},
- },
- }
-
- c.Assert(s.indexOf([]string{"a", "b", "c"}), qt.Equals, 0)
- c.Assert(s.indexOf([]string{"a", "b", "d"}), qt.Equals, 1)
- c.Assert(s.indexOf([]string{"a", "b", "x"}), qt.Equals, -1)
- })
-
- t.Run("Name", func(t *testing.T) {
- c := qt.New(t)
-
- c.Assert(templateBaseName(templateShortcode, "shortcodes/foo.html"), qt.Equals, "foo.html")
- c.Assert(templateBaseName(templateShortcode, "_internal/shortcodes/foo.html"), qt.Equals, "foo.html")
- c.Assert(templateBaseName(templateShortcode, "shortcodes/test/foo.html"), qt.Equals, "test/foo.html")
-
- c.Assert(true, qt.Equals, true)
- })
-}
diff --git a/tpl/tplimpl/subcategory_string.go b/tpl/tplimpl/subcategory_string.go
new file mode 100644
index 000000000..a36bdee7c
--- /dev/null
+++ b/tpl/tplimpl/subcategory_string.go
@@ -0,0 +1,25 @@
+// Code generated by "stringer -type SubCategory"; DO NOT EDIT.
+
+package tplimpl
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[SubCategoryMain-0]
+ _ = x[SubCategoryEmbedded-1]
+ _ = x[SubCategoryInline-2]
+}
+
+const _SubCategory_name = "SubCategoryMainSubCategoryEmbeddedSubCategoryInline"
+
+var _SubCategory_index = [...]uint8{0, 15, 34, 51}
+
+func (i SubCategory) String() string {
+ if i < 0 || i >= SubCategory(len(_SubCategory_index)-1) {
+ return "SubCategory(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _SubCategory_name[_SubCategory_index[i]:_SubCategory_index[i+1]]
+}
diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go
deleted file mode 100644
index 63dc29662..000000000
--- a/tpl/tplimpl/template.go
+++ /dev/null
@@ -1,1174 +0,0 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package tplimpl
-
-import (
- "bytes"
- "context"
- "embed"
- "fmt"
- "io"
- "io/fs"
- "path/filepath"
- "reflect"
- "regexp"
- "sort"
- "strings"
- "sync"
- "time"
- "unicode"
- "unicode/utf8"
-
- "github.com/gohugoio/hugo/common/types"
- "github.com/gohugoio/hugo/output/layouts"
-
- "github.com/gohugoio/hugo/helpers"
-
- "github.com/gohugoio/hugo/output"
-
- "github.com/gohugoio/hugo/deps"
- "github.com/spf13/afero"
-
- "github.com/gohugoio/hugo/common/herrors"
- "github.com/gohugoio/hugo/hugofs"
-
- htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
- texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
-
- "github.com/gohugoio/hugo/identity"
- "github.com/gohugoio/hugo/tpl"
-)
-
-const (
- textTmplNamePrefix = "_text/"
-
- shortcodesPathPrefix = "shortcodes/"
- internalPathPrefix = "_internal/"
- embeddedPathPrefix = "_embedded/"
- baseFileBase = "baseof"
-)
-
-// The identifiers may be truncated in the log, e.g.
-// "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image"
-// We need this to identify position in templates with base templates applied.
-var identifiersRe = regexp.MustCompile(`at \<(.*?)(\.{3})?\>:`)
-
-var embeddedTemplatesAliases = map[string][]string{
- "shortcodes/twitter.html": {"shortcodes/tweet.html"},
-}
-
-var (
- _ tpl.TemplateManager = (*templateExec)(nil)
- _ tpl.TemplateHandler = (*templateExec)(nil)
- _ tpl.TemplateFuncGetter = (*templateExec)(nil)
- _ tpl.TemplateFinder = (*templateExec)(nil)
- _ tpl.UnusedTemplatesProvider = (*templateExec)(nil)
-
- _ tpl.Template = (*templateState)(nil)
- _ tpl.Info = (*templateState)(nil)
-)
-
-var baseTemplateDefineRe = regexp.MustCompile(`^{{-?\s*define`)
-
-// needsBaseTemplate returns true if the first non-comment template block is a
-// define block.
-// If a base template does not exist, we will handle that when it's used.
-func needsBaseTemplate(templ string) bool {
- idx := -1
- inComment := false
- for i := 0; i < len(templ); {
- if !inComment && strings.HasPrefix(templ[i:], "{{/*") {
- inComment = true
- i += 4
- } else if !inComment && strings.HasPrefix(templ[i:], "{{- /*") {
- inComment = true
- i += 6
- } else if inComment && strings.HasPrefix(templ[i:], "*/}}") {
- inComment = false
- i += 4
- } else if inComment && strings.HasPrefix(templ[i:], "*/ -}}") {
- inComment = false
- i += 6
- } else {
- r, size := utf8.DecodeRuneInString(templ[i:])
- if !inComment {
- if strings.HasPrefix(templ[i:], "{{") {
- idx = i
- break
- } else if !unicode.IsSpace(r) {
- break
- }
- }
- i += size
- }
- }
-
- if idx == -1 {
- return false
- }
-
- return baseTemplateDefineRe.MatchString(templ[idx:])
-}
-
-func newStandaloneTextTemplate(funcs map[string]any) tpl.TemplateParseFinder {
- return &textTemplateWrapperWithLock{
- RWMutex: &sync.RWMutex{},
- Template: texttemplate.New("").Funcs(funcs),
- }
-}
-
-func newTemplateHandlers(d *deps.Deps) (*tpl.TemplateHandlers, error) {
- exec, funcs := newTemplateExecuter(d)
- funcMap := make(map[string]any)
- for k, v := range funcs {
- funcMap[k] = v.Interface()
- }
-
- var templateUsageTracker map[string]templateInfo
- if d.Conf.PrintUnusedTemplates() {
- templateUsageTracker = make(map[string]templateInfo)
- }
-
- h := &templateHandler{
- nameBaseTemplateName: make(map[string]string),
- transformNotFound: make(map[string]*templateState),
-
- shortcodes: make(map[string]*shortcodeTemplates),
- templateInfo: make(map[string]tpl.Info),
- baseof: make(map[string]templateInfo),
- needsBaseof: make(map[string]templateInfo),
-
- main: newTemplateNamespace(funcMap),
-
- Deps: d,
- layoutHandler: layouts.NewLayoutHandler(),
- layoutsFs: d.BaseFs.Layouts.Fs,
- layoutTemplateCache: make(map[layoutCacheKey]layoutCacheEntry),
-
- templateUsageTracker: templateUsageTracker,
- }
-
- if err := h.loadEmbedded(); err != nil {
- return nil, err
- }
-
- if err := h.loadTemplates(); err != nil {
- return nil, err
- }
-
- e := &templateExec{
- d: d,
- executor: exec,
- funcs: funcs,
- templateHandler: h,
- }
-
- if err := e.postTransform(); err != nil {
- return nil, err
- }
-
- return &tpl.TemplateHandlers{
- Tmpl: e,
- TxtTmpl: newStandaloneTextTemplate(funcMap),
- }, nil
-}
-
-func newTemplateNamespace(funcs map[string]any) *templateNamespace {
- return &templateNamespace{
- prototypeHTML: htmltemplate.New("").Funcs(funcs),
- prototypeText: texttemplate.New("").Funcs(funcs),
- templateStateMap: &templateStateMap{
- templates: make(map[string]*templateState),
- },
- }
-}
-
-func newTemplateState(templ tpl.Template, info templateInfo, id identity.Identity) *templateState {
- if id == nil {
- id = info
- }
- return &templateState{
- info: info,
- typ: info.resolveType(),
- Template: templ,
- parseInfo: tpl.DefaultParseInfo,
- id: id,
- }
-}
-
-type layoutCacheKey struct {
- d layouts.LayoutDescriptor
- f string
-}
-
-type templateExec struct {
- d *deps.Deps
- executor texttemplate.Executer
- funcs map[string]reflect.Value
-
- *templateHandler
-}
-
-func (t templateExec) Clone(d *deps.Deps) *templateExec {
- exec, funcs := newTemplateExecuter(d)
- t.executor = exec
- t.funcs = funcs
- t.d = d
- return &t
-}
-
-func (t *templateExec) Execute(templ tpl.Template, wr io.Writer, data any) error {
- return t.ExecuteWithContext(context.Background(), templ, wr, data)
-}
-
-func (t *templateExec) ExecuteWithContext(ctx context.Context, templ tpl.Template, wr io.Writer, data any) error {
- if rlocker, ok := templ.(types.RLocker); ok {
- rlocker.RLock()
- defer rlocker.RUnlock()
- }
- if t.Metrics != nil {
- defer t.Metrics.MeasureSince(templ.Name(), time.Now())
- }
-
- if t.templateUsageTracker != nil {
- if ts, ok := templ.(*templateState); ok {
-
- t.templateUsageTrackerMu.Lock()
- if _, found := t.templateUsageTracker[ts.Name()]; !found {
- t.templateUsageTracker[ts.Name()] = ts.info
- }
-
- if !ts.baseInfo.IsZero() {
- if _, found := t.templateUsageTracker[ts.baseInfo.name]; !found {
- t.templateUsageTracker[ts.baseInfo.name] = ts.baseInfo
- }
- }
- t.templateUsageTrackerMu.Unlock()
- }
- }
-
- execErr := t.executor.ExecuteWithContext(ctx, templ, wr, data)
- if execErr != nil {
- execErr = t.addFileContext(templ, execErr)
- }
- return execErr
-}
-
-func (t *templateExec) UnusedTemplates() []tpl.FileInfo {
- if t.templateUsageTracker == nil {
- return nil
- }
- var unused []tpl.FileInfo
-
- for _, ti := range t.needsBaseof {
- if _, found := t.templateUsageTracker[ti.name]; !found {
- unused = append(unused, ti)
- }
- }
-
- for _, ti := range t.baseof {
- if _, found := t.templateUsageTracker[ti.name]; !found {
- unused = append(unused, ti)
- }
- }
-
- for _, ts := range t.main.templates {
- ti := ts.info
- if strings.HasPrefix(ti.name, "_internal/") || ti.meta == nil {
- continue
- }
-
- if _, found := t.templateUsageTracker[ti.name]; !found {
- unused = append(unused, ti)
- }
- }
-
- sort.Slice(unused, func(i, j int) bool {
- return unused[i].Name() < unused[j].Name()
- })
-
- return unused
-}
-
-func (t *templateExec) GetFunc(name string) (reflect.Value, bool) {
- v, found := t.funcs[name]
- return v, found
-}
-
-func (t *templateExec) MarkReady() error {
- var err error
- t.readyInit.Do(func() {
- // We only need the clones if base templates are in use.
- if len(t.needsBaseof) > 0 {
- err = t.main.createPrototypes()
- }
- })
-
- return err
-}
-
-type templateHandler struct {
- main *templateNamespace
- needsBaseof map[string]templateInfo
- baseof map[string]templateInfo
-
- readyInit sync.Once
-
- // This is the filesystem to load the templates from. All the templates are
- // stored in the root of this filesystem.
- layoutsFs afero.Fs
-
- layoutHandler *layouts.LayoutHandler
-
- layoutTemplateCache map[layoutCacheKey]layoutCacheEntry
- layoutTemplateCacheMu sync.RWMutex
-
- *deps.Deps
-
- // Used to get proper filenames in errors
- nameBaseTemplateName map[string]string
-
- // Holds name and source of template definitions not found during the first
- // AST transformation pass.
- transformNotFound map[string]*templateState
-
- // shortcodes maps shortcode name to template variants
- // (language, output format etc.) of that shortcode.
- shortcodes map[string]*shortcodeTemplates
-
- // templateInfo maps template name to some additional information about that template.
- // Note that for shortcodes that same information is embedded in the
- // shortcodeTemplates type.
- templateInfo map[string]tpl.Info
-
- // May be nil.
- templateUsageTracker map[string]templateInfo
- templateUsageTrackerMu sync.Mutex
-}
-
-type layoutCacheEntry struct {
- found bool
- templ tpl.Template
- err error
-}
-
-// AddTemplate parses and adds a template to the collection.
-// Templates with name prefixed with "_text" will be handled as plain
-// text templates.
-func (t *templateHandler) AddTemplate(name, tpl string) error {
- templ, err := t.addTemplateTo(t.newTemplateInfo(name, tpl), t.main)
- if err == nil {
- t.applyTemplateTransformers(t.main, templ)
- }
- return err
-}
-
-func (t *templateHandler) Lookup(name string) (tpl.Template, bool) {
- templ, found := t.main.Lookup(name)
- if found {
- return templ, true
- }
-
- return nil, false
-}
-
-func (t *templateHandler) LookupLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) {
- key := layoutCacheKey{d, f.Name}
- t.layoutTemplateCacheMu.RLock()
- if cacheVal, found := t.layoutTemplateCache[key]; found {
- t.layoutTemplateCacheMu.RUnlock()
- return cacheVal.templ, cacheVal.found, cacheVal.err
- }
- t.layoutTemplateCacheMu.RUnlock()
-
- t.layoutTemplateCacheMu.Lock()
- defer t.layoutTemplateCacheMu.Unlock()
-
- templ, found, err := t.findLayout(d, f)
- cacheVal := layoutCacheEntry{found: found, templ: templ, err: err}
- t.layoutTemplateCache[key] = cacheVal
- return cacheVal.templ, cacheVal.found, cacheVal.err
-}
-
-// This currently only applies to shortcodes and what we get here is the
-// shortcode name.
-func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
- name = templateBaseName(templateShortcode, name)
- s, found := t.shortcodes[name]
- if !found {
- return nil, false, false
- }
-
- sv, found := s.fromVariants(variants)
- if !found {
- return nil, false, false
- }
-
- more := len(s.variants) > 1
-
- return sv.ts, true, more
-}
-
-// LookupVariants returns all variants of name, nil if none found.
-func (t *templateHandler) LookupVariants(name string) []tpl.Template {
- name = templateBaseName(templateShortcode, name)
- s, found := t.shortcodes[name]
- if !found {
- return nil
- }
-
- variants := make([]tpl.Template, len(s.variants))
- for i := 0; i < len(variants); i++ {
- variants[i] = s.variants[i].ts
- }
-
- return variants
-}
-
-func (t *templateHandler) HasTemplate(name string) bool {
- if _, found := t.baseof[name]; found {
- return true
- }
-
- if _, found := t.needsBaseof[name]; found {
- return true
- }
-
- _, found := t.Lookup(name)
- return found
-}
-
-func (t *templateHandler) GetIdentity(name string) (identity.Identity, bool) {
- if _, found := t.needsBaseof[name]; found {
- return identity.StringIdentity(name), true
- }
-
- if _, found := t.baseof[name]; found {
- return identity.StringIdentity(name), true
- }
-
- tt, found := t.Lookup(name)
- if !found {
- return nil, false
- }
- return tt.(identity.IdentityProvider).GetIdentity(), found
-}
-
-func (t *templateHandler) findLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) {
- d.OutputFormatName = f.Name
- d.Suffix = f.MediaType.FirstSuffix.Suffix
- layouts, _ := t.layoutHandler.For(d)
- for _, name := range layouts {
- templ, found := t.main.Lookup(name)
- if found {
- return templ, true, nil
- }
-
- overlay, found := t.needsBaseof[name]
-
- if !found {
- continue
- }
-
- d.Baseof = true
- baseLayouts, _ := t.layoutHandler.For(d)
- var base templateInfo
- found = false
- for _, l := range baseLayouts {
- base, found = t.baseof[l]
- if found {
- break
- }
- }
-
- templ, err := t.applyBaseTemplate(overlay, base)
- if err != nil {
- return nil, false, err
- }
-
- ts := newTemplateState(templ, overlay, identity.Or(base, overlay))
-
- if found {
- ts.baseInfo = base
- }
-
- t.applyTemplateTransformers(t.main, ts)
-
- if err := t.extractPartials(ts.Template); err != nil {
- return nil, false, err
- }
-
- return ts, true, nil
-
- }
-
- return nil, false, nil
-}
-
-func (t *templateHandler) newTemplateInfo(name, tpl string) templateInfo {
- var isText bool
- var isEmbedded bool
-
- if strings.HasPrefix(name, embeddedPathPrefix) {
- isEmbedded = true
- name = strings.TrimPrefix(name, embeddedPathPrefix)
- }
-
- name, isText = t.nameIsText(name)
- return templateInfo{
- name: name,
- isText: isText,
- isEmbedded: isEmbedded,
- template: tpl,
- }
-}
-
-func (t *templateHandler) addFileContext(templ tpl.Template, inerr error) error {
- if strings.HasPrefix(templ.Name(), "_internal") {
- return inerr
- }
-
- ts, ok := templ.(*templateState)
- if !ok {
- return inerr
- }
-
- identifiers := t.extractIdentifiers(inerr.Error())
-
- checkFilename := func(info templateInfo, inErr error) (error, bool) {
- if info.meta == nil {
- return inErr, false
- }
-
- lineMatcher := func(m herrors.LineMatcher) int {
- if m.Position.LineNumber != m.LineNumber {
- return -1
- }
-
- for _, id := range identifiers {
- if strings.Contains(m.Line, id) {
- // We found the line, but return a 0 to signal to
- // use the column from the error message.
- return 0
- }
- }
- return -1
- }
-
- f, err := info.meta.Open()
- if err != nil {
- return inErr, false
- }
- defer f.Close()
-
- fe := herrors.NewFileErrorFromName(inErr, info.meta.Filename)
- fe.UpdateContent(f, lineMatcher)
-
- if !fe.ErrorContext().Position.IsValid() {
- return inErr, false
- }
- return fe, true
- }
-
- inerr = fmt.Errorf("execute of template failed: %w", inerr)
-
- if err, ok := checkFilename(ts.info, inerr); ok {
- return err
- }
-
- err, _ := checkFilename(ts.baseInfo, inerr)
-
- return err
-}
-
-func (t *templateHandler) extractIdentifiers(line string) []string {
- m := identifiersRe.FindAllStringSubmatch(line, -1)
- identifiers := make([]string, len(m))
- for i := 0; i < len(m); i++ {
- identifiers[i] = m[i][1]
- }
- return identifiers
-}
-
-func (t *templateHandler) addShortcodeVariant(ts *templateState) {
- name := ts.Name()
- base := templateBaseName(templateShortcode, name)
-
- shortcodename, variants := templateNameAndVariants(base)
-
- templs, found := t.shortcodes[shortcodename]
- if !found {
- templs = &shortcodeTemplates{}
- t.shortcodes[shortcodename] = templs
- }
-
- sv := shortcodeVariant{variants: variants, ts: ts}
-
- i := templs.indexOf(variants)
-
- if i != -1 {
- // Only replace if it's an override of an internal template.
- if !isInternal(name) {
- templs.variants[i] = sv
- }
- } else {
- templs.variants = append(templs.variants, sv)
- }
-}
-
-func (t *templateHandler) addTemplateFile(name string, fim hugofs.FileMetaInfo) error {
- getTemplate := func(fim hugofs.FileMetaInfo) (templateInfo, error) {
- meta := fim.Meta()
- f, err := meta.Open()
- if err != nil {
- return templateInfo{meta: meta}, err
- }
- defer f.Close()
- b, err := io.ReadAll(f)
- if err != nil {
- return templateInfo{meta: meta}, err
- }
-
- s := removeLeadingBOM(string(b))
-
- var isText bool
- name, isText = t.nameIsText(name)
-
- return templateInfo{
- name: name,
- isText: isText,
- template: s,
- meta: meta,
- }, nil
- }
-
- tinfo, err := getTemplate(fim)
- if err != nil {
- return err
- }
-
- if isBaseTemplatePath(name) {
- // Store it for later.
- t.baseof[name] = tinfo
- return nil
- }
-
- needsBaseof := !t.noBaseNeeded(name) && needsBaseTemplate(tinfo.template)
- if needsBaseof {
- t.needsBaseof[name] = tinfo
- return nil
- }
-
- templ, err := t.addTemplateTo(tinfo, t.main)
- if err != nil {
- return tinfo.errWithFileContext("parse failed", err)
- }
- t.applyTemplateTransformers(t.main, templ)
-
- return nil
-}
-
-func (t *templateHandler) addTemplateTo(info templateInfo, to *templateNamespace) (*templateState, error) {
- return to.parse(info)
-}
-
-func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Template, error) {
- if overlay.isText {
- var (
- templ = t.main.prototypeTextClone.New(overlay.name)
- err error
- )
-
- if !base.IsZero() {
- templ, err = templ.Parse(base.template)
- if err != nil {
- return nil, base.errWithFileContext("parse failed", err)
- }
- }
-
- templ, err = texttemplate.Must(templ.Clone()).Parse(overlay.template)
- if err != nil {
- return nil, overlay.errWithFileContext("parse failed", err)
- }
-
- // The extra lookup is a workaround, see
- // * https://github.com/golang/go/issues/16101
- // * https://github.com/gohugoio/hugo/issues/2549
- // templ = templ.Lookup(templ.Name())
-
- return templ, nil
- }
-
- var (
- templ = t.main.prototypeHTMLClone.New(overlay.name)
- err error
- )
-
- if !base.IsZero() {
- templ, err = templ.Parse(base.template)
- if err != nil {
- return nil, base.errWithFileContext("parse failed", err)
- }
- }
-
- templ, err = htmltemplate.Must(templ.Clone()).Parse(overlay.template)
- if err != nil {
- return nil, overlay.errWithFileContext("parse failed", err)
- }
-
- // The extra lookup is a workaround, see
- // * https://github.com/golang/go/issues/16101
- // * https://github.com/gohugoio/hugo/issues/2549
- templ = templ.Lookup(templ.Name())
-
- return templ, err
-}
-
-func (t *templateHandler) applyTemplateTransformers(ns *templateNamespace, ts *templateState) (*templateContext, error) {
- c, err := applyTemplateTransformers(ts, ns.newTemplateLookup(ts))
- if err != nil {
- return nil, err
- }
-
- for k := range c.templateNotFound {
- t.transformNotFound[k] = ts
- }
-
- return c, err
-}
-
-//go:embed all:embedded/templates/*
-//go:embed embedded/templates/_default/*
-//go:embed embedded/templates/_server/*
-var embeddedTemplatesFs embed.FS
-
-func (t *templateHandler) loadEmbedded() error {
- return fs.WalkDir(embeddedTemplatesFs, ".", func(path string, d fs.DirEntry, err error) error {
- if err != nil {
- return err
- }
- if d == nil || d.IsDir() {
- return nil
- }
-
- templb, err := embeddedTemplatesFs.ReadFile(path)
- if err != nil {
- return err
- }
-
- // Get the newlines on Windows in line with how we had it back when we used Go Generate
- // to write the templates to Go files.
- templ := string(bytes.ReplaceAll(templb, []byte("\r\n"), []byte("\n")))
- name := strings.TrimPrefix(filepath.ToSlash(path), "embedded/templates/")
- templateName := name
-
- // For the render hooks and the server templates it does not make sense to preserve the
- // double _internal double book-keeping,
- // just add it if its now provided by the user.
- if !strings.Contains(path, "_default/_markup") && !strings.HasPrefix(name, "_server/") && !strings.HasPrefix(name, "partials/_funcs/") {
- templateName = internalPathPrefix + name
- }
-
- if _, found := t.Lookup(templateName); !found {
- if err := t.AddTemplate(embeddedPathPrefix+templateName, templ); err != nil {
- return err
- }
- }
-
- if aliases, found := embeddedTemplatesAliases[name]; found {
- // TODO(bep) avoid reparsing these aliases
- for _, alias := range aliases {
- alias = internalPathPrefix + alias
- if err := t.AddTemplate(embeddedPathPrefix+alias, templ); err != nil {
- return err
- }
- }
- }
-
- return nil
- })
-}
-
-func (t *templateHandler) loadTemplates() error {
- walker := func(path string, fi hugofs.FileMetaInfo) error {
- if fi.IsDir() {
- return nil
- }
-
- if isDotFile(path) || isBackupFile(path) {
- return nil
- }
-
- name := strings.TrimPrefix(filepath.ToSlash(path), "/")
- filename := filepath.Base(path)
- outputFormats := t.Conf.GetConfigSection("outputFormats").(output.Formats)
- outputFormat, found := outputFormats.FromFilename(filename)
-
- if found && outputFormat.IsPlainText {
- name = textTmplNamePrefix + name
- }
-
- if err := t.addTemplateFile(name, fi); err != nil {
- return err
- }
-
- return nil
- }
-
- if err := helpers.Walk(t.Layouts.Fs, "", walker); err != nil {
- if !herrors.IsNotExist(err) {
- return err
- }
- return nil
- }
-
- return nil
-}
-
-func (t *templateHandler) nameIsText(name string) (string, bool) {
- isText := strings.HasPrefix(name, textTmplNamePrefix)
- if isText {
- name = strings.TrimPrefix(name, textTmplNamePrefix)
- }
- return name, isText
-}
-
-func (t *templateHandler) noBaseNeeded(name string) bool {
- if strings.HasPrefix(name, "shortcodes/") || strings.HasPrefix(name, "partials/") {
- return true
- }
- return strings.Contains(name, "_markup/")
-}
-
-func (t *templateHandler) extractPartials(templ tpl.Template) error {
- templs := templates(templ)
- for _, templ := range templs {
- if templ.Name() == "" || !strings.HasPrefix(templ.Name(), "partials/") {
- continue
- }
-
- ts := newTemplateState(templ, templateInfo{name: templ.Name()}, nil)
- ts.typ = templatePartial
-
- t.main.mu.RLock()
- _, found := t.main.templates[templ.Name()]
- t.main.mu.RUnlock()
-
- if !found {
- t.main.mu.Lock()
- // This is a template defined inline.
- _, err := applyTemplateTransformers(ts, t.main.newTemplateLookup(ts))
- if err != nil {
- t.main.mu.Unlock()
- return err
- }
- t.main.templates[templ.Name()] = ts
- t.main.mu.Unlock()
-
- }
- }
-
- return nil
-}
-
-func (t *templateHandler) postTransform() error {
- defineCheckedHTML := false
- defineCheckedText := false
-
- for _, v := range t.main.templates {
- if v.typ == templateShortcode {
- t.addShortcodeVariant(v)
- }
-
- if defineCheckedHTML && defineCheckedText {
- continue
- }
-
- isText := isText(v.Template)
- if isText {
- if defineCheckedText {
- continue
- }
- defineCheckedText = true
- } else {
- if defineCheckedHTML {
- continue
- }
- defineCheckedHTML = true
- }
-
- if err := t.extractPartials(v.Template); err != nil {
- return err
- }
- }
-
- for name, source := range t.transformNotFound {
- lookup := t.main.newTemplateLookup(source)
- templ := lookup(name)
- if templ != nil {
- _, err := applyTemplateTransformers(templ, lookup)
- if err != nil {
- return err
- }
- }
- }
-
- for _, v := range t.shortcodes {
- sort.Slice(v.variants, func(i, j int) bool {
- v1, v2 := v.variants[i], v.variants[j]
- name1, name2 := v1.ts.Name(), v2.ts.Name()
- isHTMl1, isHTML2 := strings.HasSuffix(name1, "html"), strings.HasSuffix(name2, "html")
-
- // There will be a weighted selection later, but make
- // sure these are sorted to get a stable selection for
- // output formats missing specific templates.
- // Prefer HTML.
- if isHTMl1 || isHTML2 && !(isHTMl1 && isHTML2) {
- return isHTMl1
- }
-
- return name1 < name2
- })
- }
-
- return nil
-}
-
-type templateNamespace struct {
- prototypeText *texttemplate.Template
- prototypeHTML *htmltemplate.Template
- prototypeTextClone *texttemplate.Template
- prototypeHTMLClone *htmltemplate.Template
-
- *templateStateMap
-}
-
-func (t templateNamespace) Clone() *templateNamespace {
- t.mu.Lock()
- defer t.mu.Unlock()
-
- t.templateStateMap = &templateStateMap{
- templates: make(map[string]*templateState),
- }
-
- t.prototypeText = texttemplate.Must(t.prototypeText.Clone())
- t.prototypeHTML = htmltemplate.Must(t.prototypeHTML.Clone())
-
- return &t
-}
-
-func (t *templateNamespace) Lookup(name string) (tpl.Template, bool) {
- t.mu.RLock()
- defer t.mu.RUnlock()
-
- templ, found := t.templates[name]
- if !found {
- return nil, false
- }
-
- return templ, found
-}
-
-func (t *templateNamespace) createPrototypes() error {
- t.prototypeTextClone = texttemplate.Must(t.prototypeText.Clone())
- t.prototypeHTMLClone = htmltemplate.Must(t.prototypeHTML.Clone())
-
- return nil
-}
-
-func (t *templateNamespace) newTemplateLookup(in *templateState) func(name string) *templateState {
- return func(name string) *templateState {
- if templ, found := t.templates[name]; found {
- if templ.isText() != in.isText() {
- return nil
- }
- return templ
- }
- if templ, found := findTemplateIn(name, in); found {
- return newTemplateState(templ, templateInfo{name: templ.Name()}, nil)
- }
- return nil
- }
-}
-
-func (t *templateNamespace) parse(info templateInfo) (*templateState, error) {
- t.mu.Lock()
- defer t.mu.Unlock()
-
- if info.isText {
- prototype := t.prototypeText
-
- templ, err := prototype.New(info.name).Parse(info.template)
- if err != nil {
- return nil, err
- }
-
- ts := newTemplateState(templ, info, nil)
-
- t.templates[info.name] = ts
-
- return ts, nil
- }
-
- prototype := t.prototypeHTML
-
- templ, err := prototype.New(info.name).Parse(info.template)
- if err != nil {
- return nil, err
- }
-
- ts := newTemplateState(templ, info, nil)
-
- t.templates[info.name] = ts
-
- return ts, nil
-}
-
-var _ tpl.IsInternalTemplateProvider = (*templateState)(nil)
-
-type templateState struct {
- tpl.Template
-
- typ templateType
- parseInfo tpl.ParseInfo
- id identity.Identity
-
- info templateInfo
- baseInfo templateInfo // Set when a base template is used.
-}
-
-func (t *templateState) IsInternalTemplate() bool {
- return t.info.isEmbedded
-}
-
-func (t *templateState) GetIdentity() identity.Identity {
- return t.id
-}
-
-func (t *templateState) ParseInfo() tpl.ParseInfo {
- return t.parseInfo
-}
-
-func (t *templateState) isText() bool {
- return isText(t.Template)
-}
-
-func (t *templateState) String() string {
- return t.Name()
-}
-
-func isText(templ tpl.Template) bool {
- _, isText := templ.(*texttemplate.Template)
- return isText
-}
-
-type templateStateMap struct {
- mu sync.RWMutex
- templates map[string]*templateState
-}
-
-type textTemplateWrapperWithLock struct {
- *sync.RWMutex
- *texttemplate.Template
-}
-
-func (t *textTemplateWrapperWithLock) Lookup(name string) (tpl.Template, bool) {
- t.RLock()
- templ := t.Template.Lookup(name)
- t.RUnlock()
- if templ == nil {
- return nil, false
- }
- return &textTemplateWrapperWithLock{
- RWMutex: t.RWMutex,
- Template: templ,
- }, true
-}
-
-func (t *textTemplateWrapperWithLock) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
- panic("not supported")
-}
-
-func (t *textTemplateWrapperWithLock) LookupVariants(name string) []tpl.Template {
- panic("not supported")
-}
-
-func (t *textTemplateWrapperWithLock) Parse(name, tpl string) (tpl.Template, error) {
- t.Lock()
- defer t.Unlock()
- return t.Template.New(name).Parse(tpl)
-}
-
-func isBackupFile(path string) bool {
- return path[len(path)-1] == '~'
-}
-
-func isBaseTemplatePath(path string) bool {
- return strings.Contains(filepath.Base(path), baseFileBase)
-}
-
-func isDotFile(path string) bool {
- return filepath.Base(path)[0] == '.'
-}
-
-func removeLeadingBOM(s string) string {
- const bom = '\ufeff'
-
- for i, r := range s {
- if i == 0 && r != bom {
- return s
- }
- if i > 0 {
- return s[i:]
- }
- }
-
- return s
-}
-
-// resolves _internal/shortcodes/param.html => param.html etc.
-func templateBaseName(typ templateType, name string) string {
- name = strings.TrimPrefix(name, internalPathPrefix)
- switch typ {
- case templateShortcode:
- return strings.TrimPrefix(name, shortcodesPathPrefix)
- default:
- panic("not implemented")
- }
-}
-
-func unwrap(templ tpl.Template) tpl.Template {
- if ts, ok := templ.(*templateState); ok {
- return ts.Template
- }
- return templ
-}
-
-func templates(in tpl.Template) []tpl.Template {
- var templs []tpl.Template
- in = unwrap(in)
- if textt, ok := in.(*texttemplate.Template); ok {
- for _, t := range textt.Templates() {
- templs = append(templs, t)
- }
- }
-
- if htmlt, ok := in.(*htmltemplate.Template); ok {
- for _, t := range htmlt.Templates() {
- templs = append(templs, t)
- }
- }
-
- return templs
-}
diff --git a/tpl/tplimpl/templateProvider.go b/tpl/tplimpl/templateProvider.go
deleted file mode 100644
index 435868964..000000000
--- a/tpl/tplimpl/templateProvider.go
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright 2017-present The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package tplimpl
-
-import (
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/tpl"
-)
-
-// TemplateProvider manages templates.
-type TemplateProvider struct{}
-
-// DefaultTemplateProvider is a globally available TemplateProvider.
-var DefaultTemplateProvider *TemplateProvider
-
-// Update updates the Hugo Template System in the provided Deps
-// with all the additional features, templates & functions.
-func (*TemplateProvider) NewResource(dst *deps.Deps) error {
- handlers, err := newTemplateHandlers(dst)
- if err != nil {
- return err
- }
- dst.SetTempl(handlers)
- return nil
-}
-
-// Clone clones.
-func (*TemplateProvider) CloneResource(dst, src *deps.Deps) error {
- t := src.Tmpl().(*templateExec)
- c := t.Clone(dst)
- funcMap := make(map[string]any)
- for k, v := range c.funcs {
- funcMap[k] = v.Interface()
- }
- dst.SetTempl(&tpl.TemplateHandlers{
- Tmpl: c,
- TxtTmpl: newStandaloneTextTemplate(funcMap),
- })
- return nil
-}
diff --git a/tpl/tplimpl/template_ast_transformers_test.go b/tpl/tplimpl/template_ast_transformers_test.go
deleted file mode 100644
index bd889b832..000000000
--- a/tpl/tplimpl/template_ast_transformers_test.go
+++ /dev/null
@@ -1,161 +0,0 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package tplimpl
-
-import (
- "testing"
-
- template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/tpl"
-)
-
-// Issue #2927
-func TestTransformRecursiveTemplate(t *testing.T) {
- c := qt.New(t)
-
- recursive := `
-{{ define "menu-nodes" }}
-{{ template "menu-node" }}
-{{ end }}
-{{ define "menu-node" }}
-{{ template "menu-node" }}
-{{ end }}
-{{ template "menu-nodes" }}
-`
-
- templ, err := template.New("foo").Parse(recursive)
- c.Assert(err, qt.IsNil)
- ts := newTestTemplate(templ)
-
- ctx := newTemplateContext(
- ts,
- newTestTemplateLookup(ts),
- )
- ctx.applyTransformations(templ.Tree.Root)
-}
-
-func newTestTemplate(templ tpl.Template) *templateState {
- return newTemplateState(
- templ,
- templateInfo{
- name: templ.Name(),
- },
- nil,
- )
-}
-
-func newTestTemplateLookup(in *templateState) func(name string) *templateState {
- m := make(map[string]*templateState)
- return func(name string) *templateState {
- if in.Name() == name {
- return in
- }
-
- if ts, found := m[name]; found {
- return ts
- }
-
- if templ, found := findTemplateIn(name, in); found {
- ts := newTestTemplate(templ)
- m[name] = ts
- return ts
- }
-
- return nil
- }
-}
-
-func TestCollectInfo(t *testing.T) {
- configStr := `{ "version": 42 }`
-
- tests := []struct {
- name string
- tplString string
- expected tpl.ParseInfo
- }{
- {"Basic Inner", `{{ .Inner }}`, tpl.ParseInfo{IsInner: true, Config: tpl.DefaultParseConfig}},
- {"Basic config map", "{{ $_hugo_config := `" + configStr + "` }}", tpl.ParseInfo{Config: tpl.ParseConfig{Version: 42}}},
- }
-
- echo := func(in any) any {
- return in
- }
-
- funcs := template.FuncMap{
- "highlight": echo,
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- c := qt.New(t)
-
- templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
- c.Assert(err, qt.IsNil)
- ts := newTestTemplate(templ)
- ts.typ = templateShortcode
- ctx := newTemplateContext(
- ts,
- newTestTemplateLookup(ts),
- )
- ctx.applyTransformations(templ.Tree.Root)
- c.Assert(ctx.t.parseInfo, qt.DeepEquals, test.expected)
- })
- }
-}
-
-func TestPartialReturn(t *testing.T) {
- tests := []struct {
- name string
- tplString string
- expected bool
- }{
- {"Basic", `
-{{ $a := "Hugo Rocks!" }}
-{{ return $a }}
-`, true},
- {"Expression", `
-{{ return add 32 }}
-`, true},
- }
-
- echo := func(in any) any {
- return in
- }
-
- funcs := template.FuncMap{
- "return": echo,
- "add": echo,
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- c := qt.New(t)
-
- templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
- c.Assert(err, qt.IsNil)
- ts := newTestTemplate(templ)
- ctx := newTemplateContext(
- ts,
- newTestTemplateLookup(ts),
- )
-
- _, err = ctx.applyTransformations(templ.Tree.Root)
-
- // Just check that it doesn't fail in this test. We have functional tests
- // in hugoblib.
- c.Assert(err, qt.IsNil)
- })
- }
-}
diff --git a/tpl/tplimpl/template_errors.go b/tpl/tplimpl/template_errors.go
deleted file mode 100644
index a9d259220..000000000
--- a/tpl/tplimpl/template_errors.go
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright 2018 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package tplimpl
-
-import (
- "fmt"
-
- "github.com/gohugoio/hugo/common/herrors"
- "github.com/gohugoio/hugo/hugofs"
- "github.com/gohugoio/hugo/identity"
-)
-
-var _ identity.Identity = (*templateInfo)(nil)
-
-type templateInfo struct {
- name string
- template string
- isText bool // HTML or plain text template.
- isEmbedded bool
-
- meta *hugofs.FileMeta
-}
-
-func (t templateInfo) IdentifierBase() string {
- return t.name
-}
-
-func (t templateInfo) Name() string {
- return t.name
-}
-
-func (t templateInfo) Filename() string {
- return t.meta.Filename
-}
-
-func (t templateInfo) IsZero() bool {
- return t.name == ""
-}
-
-func (t templateInfo) resolveType() templateType {
- return resolveTemplateType(t.name)
-}
-
-func (info templateInfo) errWithFileContext(what string, err error) error {
- err = fmt.Errorf(what+": %w", err)
- fe := herrors.NewFileErrorFromName(err, info.meta.Filename)
- f, err := info.meta.Open()
- if err != nil {
- return err
- }
- defer f.Close()
- return fe.UpdateContent(f, nil)
-}
diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go
index e5ee4d54f..18d4d4a27 100644
--- a/tpl/tplimpl/template_funcs.go
+++ b/tpl/tplimpl/template_funcs.go
@@ -1,4 +1,4 @@
-// Copyright 2017-present The Hugo Authors. All rights reserved.
+// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Portions Copyright The Go Authors.
@@ -25,44 +25,7 @@ import (
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/tpl"
- template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
-
- "github.com/gohugoio/hugo/deps"
-
- "github.com/gohugoio/hugo/tpl/internal"
-
- // Init the namespaces
- _ "github.com/gohugoio/hugo/tpl/cast"
- _ "github.com/gohugoio/hugo/tpl/collections"
- _ "github.com/gohugoio/hugo/tpl/compare"
- _ "github.com/gohugoio/hugo/tpl/crypto"
- _ "github.com/gohugoio/hugo/tpl/css"
- _ "github.com/gohugoio/hugo/tpl/data"
- _ "github.com/gohugoio/hugo/tpl/debug"
- _ "github.com/gohugoio/hugo/tpl/diagrams"
- _ "github.com/gohugoio/hugo/tpl/encoding"
- _ "github.com/gohugoio/hugo/tpl/fmt"
- _ "github.com/gohugoio/hugo/tpl/hugo"
- _ "github.com/gohugoio/hugo/tpl/images"
- _ "github.com/gohugoio/hugo/tpl/inflect"
- _ "github.com/gohugoio/hugo/tpl/js"
- _ "github.com/gohugoio/hugo/tpl/lang"
- _ "github.com/gohugoio/hugo/tpl/math"
- _ "github.com/gohugoio/hugo/tpl/openapi/openapi3"
- _ "github.com/gohugoio/hugo/tpl/os"
- _ "github.com/gohugoio/hugo/tpl/page"
- _ "github.com/gohugoio/hugo/tpl/partials"
- _ "github.com/gohugoio/hugo/tpl/path"
- _ "github.com/gohugoio/hugo/tpl/reflect"
- _ "github.com/gohugoio/hugo/tpl/resources"
- _ "github.com/gohugoio/hugo/tpl/safe"
- _ "github.com/gohugoio/hugo/tpl/site"
- _ "github.com/gohugoio/hugo/tpl/strings"
- _ "github.com/gohugoio/hugo/tpl/templates"
- _ "github.com/gohugoio/hugo/tpl/time"
- _ "github.com/gohugoio/hugo/tpl/transform"
- _ "github.com/gohugoio/hugo/tpl/urls"
)
var (
@@ -210,70 +173,3 @@ func (t *templateExecHelper) trackDependencies(ctx context.Context, tmpl texttem
return ctx
}
-
-func newTemplateExecuter(d *deps.Deps) (texttemplate.Executer, map[string]reflect.Value) {
- funcs := createFuncMap(d)
- funcsv := make(map[string]reflect.Value)
-
- for k, v := range funcs {
- vv := reflect.ValueOf(v)
- funcsv[k] = vv
- }
-
- // Duplicate Go's internal funcs here for faster lookups.
- for k, v := range template.GoFuncs {
- if _, exists := funcsv[k]; !exists {
- vv, ok := v.(reflect.Value)
- if !ok {
- vv = reflect.ValueOf(v)
- }
- funcsv[k] = vv
- }
- }
-
- for k, v := range texttemplate.GoFuncs {
- if _, exists := funcsv[k]; !exists {
- funcsv[k] = v
- }
- }
-
- exeHelper := &templateExecHelper{
- watching: d.Conf.Watching(),
- funcs: funcsv,
- site: reflect.ValueOf(d.Site),
- siteParams: reflect.ValueOf(d.Site.Params()),
- }
-
- return texttemplate.NewExecuter(
- exeHelper,
- ), funcsv
-}
-
-func createFuncMap(d *deps.Deps) map[string]any {
- funcMap := template.FuncMap{}
-
- // Merge the namespace funcs
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns := nsf(d)
- if _, exists := funcMap[ns.Name]; exists {
- panic(ns.Name + " is a duplicate template func")
- }
- funcMap[ns.Name] = ns.Context
- for _, mm := range ns.MethodMappings {
- for _, alias := range mm.Aliases {
- if _, exists := funcMap[alias]; exists {
- panic(alias + " is a duplicate template func")
- }
- funcMap[alias] = mm.Method
- }
- }
- }
-
- if d.OverloadedTemplateFuncs != nil {
- for k, v := range d.OverloadedTemplateFuncs {
- funcMap[k] = v
- }
- }
-
- return funcMap
-}
diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go
index f1ab3d659..639db909a 100644
--- a/tpl/tplimpl/template_funcs_test.go
+++ b/tpl/tplimpl/template_funcs_test.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
+// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@ package tplimpl_test
import (
"fmt"
+ "runtime"
"strings"
"testing"
@@ -24,6 +25,9 @@ import (
)
func TestTemplateFuncsExamples(t *testing.T) {
+ if runtime.GOARCH == "s390x" {
+ t.Skip("Skip on s390x, see https://github.com/gohugoio/hugo/issues/13204")
+ }
t.Parallel()
files := `
diff --git a/tpl/template_info.go b/tpl/tplimpl/template_info.go
similarity index 73%
rename from tpl/template_info.go
rename to tpl/tplimpl/template_info.go
index fd126d80f..0bc807d91 100644
--- a/tpl/template_info.go
+++ b/tpl/tplimpl/template_info.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
+// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -11,24 +11,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package tpl
+package tplimpl
// Increments on breaking changes.
const TemplateVersion = 2
-type Info interface {
- ParseInfo() ParseInfo
-}
-
-type FileInfo interface {
- Name() string
- Filename() string
-}
-
-type IsInternalTemplateProvider interface {
- IsInternalTemplate() bool
-}
-
+// ParseInfo holds information about a parsed ntemplate.
type ParseInfo struct {
// Set for shortcode templates with any {{ .Inner }}
IsInner bool
@@ -44,14 +32,15 @@ func (info ParseInfo) IsZero() bool {
return info.Config.Version == 0
}
+// ParseConfig holds configuration extracted from the template.
type ParseConfig struct {
Version int
}
-var DefaultParseConfig = ParseConfig{
+var defaultParseConfig = ParseConfig{
Version: TemplateVersion,
}
-var DefaultParseInfo = ParseInfo{
- Config: DefaultParseConfig,
+var defaultParseInfo = ParseInfo{
+ Config: defaultParseConfig,
}
diff --git a/tpl/tplimpl/template_test.go b/tpl/tplimpl/template_test.go
deleted file mode 100644
index 5e372d986..000000000
--- a/tpl/tplimpl/template_test.go
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package tplimpl
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
-)
-
-func TestNeedsBaseTemplate(t *testing.T) {
- c := qt.New(t)
-
- c.Assert(needsBaseTemplate(`{{ define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(`{{define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(`{{- define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(`{{-define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(`
-
- {{-define "main" }}
-
- `), qt.Equals, true)
- c.Assert(needsBaseTemplate(` {{ define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(`
- {{ define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(` A {{ define "main" }}`), qt.Equals, false)
- c.Assert(needsBaseTemplate(` {{ printf "foo" }}`), qt.Equals, false)
- c.Assert(needsBaseTemplate(`{{/* comment */}} {{ define "main" }}`), qt.Equals, true)
- c.Assert(needsBaseTemplate(` {{/* comment */}} A {{ define "main" }}`), qt.Equals, false)
-}
diff --git a/tpl/tplimpl/templatedescriptor.go b/tpl/tplimpl/templatedescriptor.go
new file mode 100644
index 000000000..fd86f15fa
--- /dev/null
+++ b/tpl/tplimpl/templatedescriptor.go
@@ -0,0 +1,238 @@
+// Copyright 2025 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tplimpl
+
+import (
+ "github.com/gohugoio/hugo/resources/kinds"
+)
+
+const baseNameBaseof = "baseof"
+
+// This is used both as a key and in lookups.
+type TemplateDescriptor struct {
+ // Group 1.
+ Kind string // page, home, section, taxonomy, term (and only those)
+ LayoutFromTemplate string // list, single, all,mycustomlayout
+ LayoutFromUser string // custom layout set in front matter, e.g. list, single, all, mycustomlayout
+
+ // Group 2.
+ OutputFormat string // rss, csv ...
+ MediaType string // text/html, text/plain, ...
+ Lang string // en, nn, fr, ...
+
+ Variant1 string // contextual variant, e.g. "link" in render hooks."
+ Variant2 string // contextual variant, e.g. "id" in render.
+
+ // Misc.
+ LayoutFromUserMustMatch bool // If set, we only look for the exact layout.
+ IsPlainText bool // Whether this is a plain text template.
+ AlwaysAllowPlainText bool // Whether to e.g. allow plain text templates to be rendered in HTML.
+}
+
+func (d *TemplateDescriptor) normalizeFromFile() {
+ if d.LayoutFromTemplate == d.OutputFormat {
+ d.LayoutFromTemplate = ""
+ }
+
+ if d.Kind == kinds.KindTemporary {
+ d.Kind = ""
+ }
+
+ if d.LayoutFromTemplate == d.Kind {
+ d.LayoutFromTemplate = ""
+ }
+}
+
+type descriptorHandler struct {
+ opts StoreOptions
+}
+
+// Note that this in this setup is usually a descriptor constructed from a page,
+// so we want to find the best match for that page.
+func (s descriptorHandler) compareDescriptors(category Category, isEmbedded bool, this, other TemplateDescriptor) weight {
+ if this.LayoutFromUserMustMatch && this.LayoutFromUser != other.LayoutFromTemplate {
+ return weightNoMatch
+ }
+
+ w := this.doCompare(category, s.opts.DefaultContentLanguage, other)
+
+ if w.w1 <= 0 {
+ if category == CategoryMarkup && (this.Variant1 == other.Variant1) && (this.Variant2 == other.Variant2 || this.Variant2 != "" && other.Variant2 == "") {
+ // See issue 13242.
+ if this.OutputFormat != other.OutputFormat && this.OutputFormat == s.opts.DefaultOutputFormat {
+ return w
+ }
+
+ w.w1 = 1
+ }
+
+ if category == CategoryShortcode {
+ if (this.IsPlainText == other.IsPlainText || !other.IsPlainText) || this.AlwaysAllowPlainText {
+ w.w1 = 1
+ }
+ }
+ }
+
+ return w
+}
+
+//lint:ignore ST1006 this vs other makes it easier to reason about.
+func (this TemplateDescriptor) doCompare(category Category, defaultContentLanguage string, other TemplateDescriptor) weight {
+ w := weightNoMatch
+
+ if !this.AlwaysAllowPlainText {
+ // HTML in plain text is OK, but not the other way around.
+ if other.IsPlainText && !this.IsPlainText {
+ return w
+ }
+ }
+
+ if other.Kind != "" && other.Kind != this.Kind {
+ return w
+ }
+
+ if other.LayoutFromTemplate != "" && other.LayoutFromTemplate != layoutAll {
+ if this.LayoutFromUser == "" || this.LayoutFromUser != other.LayoutFromTemplate {
+ if other.LayoutFromTemplate != this.LayoutFromTemplate {
+ return w
+ }
+ }
+ }
+
+ if other.Lang != "" && other.Lang != this.Lang {
+ return w
+ }
+
+ if other.OutputFormat != "" && other.OutputFormat != this.OutputFormat {
+ if this.MediaType != other.MediaType {
+ return w
+ }
+
+ // We want e.g. home page in amp output format (media type text/html) to
+ // find a template even if one isn't specified for that output format,
+ // when one exist for the html output format (same media type).
+ skip := category != CategoryBaseof && (this.Kind == "" || (this.Kind != other.Kind && (this.LayoutFromTemplate != other.LayoutFromTemplate && other.LayoutFromTemplate != layoutAll)))
+ if this.LayoutFromUser != "" {
+ skip = skip && (this.LayoutFromUser != other.LayoutFromTemplate)
+ }
+ if skip {
+ return w
+ }
+
+ // Continue.
+ }
+
+ if other.MediaType != this.MediaType {
+ return w
+ }
+
+ // One example of variant1 and 2 is for render codeblocks:
+ // variant1=codeblock, variant2=go (language).
+ if other.Variant1 != "" {
+ if other.Variant1 != this.Variant1 {
+ return w
+ }
+
+ if other.Variant2 != "" && other.Variant2 != this.Variant2 {
+ return w
+ }
+ }
+
+ const (
+ weightKind = 5 // page, home, section, taxonomy, term (and only those)
+ weightcustomLayout = 6 // custom layout (mylayout, set in e.g. front matter)
+ weightLayoutStandard = 4 // standard layouts (single,list)
+ weightLayoutAll = 2 // the "all" layout
+ weightOutputFormat = 4 // a configured output format (e.g. rss, html, json)
+ weightMediaType = 1 // a configured media type (e.g. text/html, text/plain)
+ weightLang = 1 // a configured language (e.g. en, nn, fr, ...)
+ weightVariant1 = 6 // currently used for render hooks, e.g. "link", "image"
+ weightVariant2 = 4 // currently used for render hooks, e.g. the language "go" in code blocks.
+
+ // We will use the values for group 2 and 3
+ // if the distance up to the template is shorter than
+ // the one we're comparing with.
+ // E.g for a page in /posts/mypage.md with the
+ // two templates /layouts/posts/single.html and /layouts/page.html,
+ // the first one is the best match even if the second one
+ // has a higher w1 value.
+ weight2Group1 = 1 // kind, standardl layout (single,list,all)
+ weight2Group2 = 2 // custom layout (mylayout)
+
+ weight3 = 1 // for media type, lang, output format.
+ )
+
+ // Now we now know that the other descriptor is a subset of this.
+ // Now calculate the weights.
+ w.w1++
+
+ if other.Kind != "" && other.Kind == this.Kind {
+ w.w1 += weightKind
+ w.w2 = weight2Group1
+ }
+
+ if other.LayoutFromTemplate != "" && (other.LayoutFromTemplate == this.LayoutFromTemplate) {
+ w.w1 += weightLayoutStandard
+ w.w2 = weight2Group1
+ } else if other.LayoutFromTemplate == layoutAll {
+ w.w1 += weightLayoutAll
+ w.w2 = weight2Group1
+ }
+
+ // LayoutCustom is only set in this (usually from Page.Layout).
+ if this.LayoutFromUser != "" && this.LayoutFromUser == other.LayoutFromTemplate {
+ w.w1 += weightcustomLayout
+ w.w2 = weight2Group2
+ }
+
+ if (other.Lang != "" && other.Lang == this.Lang) || (other.Lang == "" && this.Lang == defaultContentLanguage) {
+ w.w1 += weightLang
+ w.w3 += weight3
+ }
+
+ if other.OutputFormat != "" && other.OutputFormat == this.OutputFormat {
+ w.w1 += weightOutputFormat
+ w.w3 += weight3
+ }
+
+ if other.MediaType != "" && other.MediaType == this.MediaType {
+ w.w1 += weightMediaType
+ w.w3 += weight3
+ }
+
+ if other.Variant1 != "" && other.Variant1 == this.Variant1 {
+ w.w1 += weightVariant1
+ }
+
+ if other.Variant1 != "" && other.Variant2 == this.Variant2 {
+ w.w1 += weightVariant2
+ }
+
+ return w
+}
+
+func (d TemplateDescriptor) IsZero() bool {
+ return d == TemplateDescriptor{}
+}
+
+//lint:ignore ST1006 this vs other makes it easier to reason about.
+func (this TemplateDescriptor) isKindInLayout(layout string) bool {
+ if this.Kind == "" {
+ return true
+ }
+ if this.Kind != kinds.KindPage {
+ return layout != layoutSingle
+ }
+ return layout != layoutList
+}
diff --git a/tpl/tplimpl/templatedescriptor_test.go b/tpl/tplimpl/templatedescriptor_test.go
new file mode 100644
index 000000000..20ab47fba
--- /dev/null
+++ b/tpl/tplimpl/templatedescriptor_test.go
@@ -0,0 +1,104 @@
+package tplimpl
+
+import (
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/resources/kinds"
+)
+
+func TestTemplateDescriptorCompare(t *testing.T) {
+ c := qt.New(t)
+
+ dh := descriptorHandler{
+ opts: StoreOptions{
+ OutputFormats: output.DefaultFormats,
+ DefaultOutputFormat: "html",
+ },
+ }
+
+ less := func(category Category, this, other1, other2 TemplateDescriptor) {
+ c.Helper()
+ result1 := dh.compareDescriptors(category, false, this, other1)
+ result2 := dh.compareDescriptors(category, false, this, other2)
+ c.Assert(result1.w1 < result2.w1, qt.IsTrue, qt.Commentf("%d < %d", result1, result2))
+ }
+
+ check := func(category Category, this, other TemplateDescriptor, less bool) {
+ c.Helper()
+ result := dh.compareDescriptors(category, false, this, other)
+ if less {
+ c.Assert(result.w1 < 0, qt.IsTrue, qt.Commentf("%d", result))
+ } else {
+ c.Assert(result.w1 >= 0, qt.IsTrue, qt.Commentf("%d", result))
+ }
+ }
+
+ check(
+
+ CategoryBaseof,
+ TemplateDescriptor{Kind: "", LayoutFromTemplate: "", Lang: "", OutputFormat: "404", MediaType: "text/html"},
+ TemplateDescriptor{Kind: "", LayoutFromTemplate: "", Lang: "", OutputFormat: "html", MediaType: "text/html"},
+ false,
+ )
+
+ check(
+ CategoryLayout,
+ TemplateDescriptor{Kind: "", Lang: "en", OutputFormat: "404", MediaType: "text/html"},
+ TemplateDescriptor{Kind: "", LayoutFromTemplate: "", Lang: "", OutputFormat: "alias", MediaType: "text/html"},
+ true,
+ )
+
+ less(
+ CategoryLayout,
+ TemplateDescriptor{Kind: kinds.KindHome, LayoutFromTemplate: "list", OutputFormat: "html"},
+ TemplateDescriptor{LayoutFromTemplate: "list", OutputFormat: "html"},
+ TemplateDescriptor{Kind: kinds.KindHome, OutputFormat: "html"},
+ )
+
+ check(
+ CategoryLayout,
+ TemplateDescriptor{Kind: kinds.KindHome, LayoutFromTemplate: "list", OutputFormat: "html", MediaType: "text/html"},
+ TemplateDescriptor{Kind: kinds.KindHome, LayoutFromTemplate: "list", OutputFormat: "myformat", MediaType: "text/html"},
+ false,
+ )
+}
+
+// INFO timer: name resolveTemplate count 779 duration 5.482274ms average 7.037µs median 4µs
+func BenchmarkCompareDescriptors(b *testing.B) {
+ dh := descriptorHandler{
+ opts: StoreOptions{
+ OutputFormats: output.DefaultFormats,
+ DefaultOutputFormat: "html",
+ },
+ }
+
+ pairs := []struct {
+ d1, d2 TemplateDescriptor
+ }{
+ {
+ TemplateDescriptor{Kind: "", LayoutFromTemplate: "", OutputFormat: "404", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false},
+ TemplateDescriptor{Kind: "", LayoutFromTemplate: "", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false},
+ },
+ {
+ TemplateDescriptor{Kind: "page", LayoutFromTemplate: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false},
+ TemplateDescriptor{Kind: "", LayoutFromTemplate: "list", OutputFormat: "", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false},
+ },
+ {
+ TemplateDescriptor{Kind: "page", LayoutFromTemplate: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false},
+ TemplateDescriptor{Kind: "", LayoutFromTemplate: "", OutputFormat: "alias", MediaType: "text/html", Lang: "", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false},
+ },
+ {
+ TemplateDescriptor{Kind: "page", LayoutFromTemplate: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "en", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false},
+ TemplateDescriptor{Kind: "", LayoutFromTemplate: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "nn", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false},
+ },
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ for _, pair := range pairs {
+ _ = dh.compareDescriptors(CategoryLayout, false, pair.d1, pair.d2)
+ }
+ }
+}
diff --git a/tpl/tplimpl/templates.go b/tpl/tplimpl/templates.go
new file mode 100644
index 000000000..7aeb7e2b9
--- /dev/null
+++ b/tpl/tplimpl/templates.go
@@ -0,0 +1,366 @@
+package tplimpl
+
+import (
+ "io"
+ "iter"
+ "regexp"
+ "strconv"
+ "strings"
+ "sync/atomic"
+ "unicode"
+ "unicode/utf8"
+
+ "github.com/gohugoio/hugo/common/paths"
+ "github.com/gohugoio/hugo/tpl"
+ htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
+ texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
+)
+
+func (t *templateNamespace) readTemplateInto(templ *TemplInfo) error {
+ if err := func() error {
+ meta := templ.Fi.Meta()
+ f, err := meta.Open()
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ b, err := io.ReadAll(f)
+ if err != nil {
+ return err
+ }
+ templ.content = removeLeadingBOM(string(b))
+ if !templ.noBaseOf {
+ templ.noBaseOf = !needsBaseTemplate(templ.content)
+ }
+ return nil
+ }(); err != nil {
+ return err
+ }
+ return nil
+}
+
+// The tweet and twitter shortcodes were deprecated in favor of the x shortcode
+// in v0.141.0. We can remove these aliases in v0.155.0 or later.
+var embeddedTemplatesAliases = map[string][]string{
+ "_shortcodes/twitter.html": {"_shortcodes/tweet.html"},
+}
+
+func (s *TemplateStore) parseTemplate(ti *TemplInfo, replace bool) error {
+ err := s.tns.doParseTemplate(ti, replace)
+ if err != nil {
+ return s.addFileContext(ti, "parse of template failed", err)
+ }
+ return err
+}
+
+func (t *templateNamespace) doParseTemplate(ti *TemplInfo, replace bool) error {
+ if !ti.noBaseOf || ti.category == CategoryBaseof {
+ // Delay parsing until we have the base template.
+ return nil
+ }
+ pi := ti.PathInfo
+ name := pi.PathNoLeadingSlash()
+
+ var (
+ templ tpl.Template
+ err error
+ )
+
+ if ti.D.IsPlainText {
+ prototype := t.parseText
+ if !replace && prototype.Lookup(name) != nil {
+ name += "-" + strconv.FormatUint(t.nameCounter.Add(1), 10)
+ }
+ templ, err = prototype.New(name).Parse(ti.content)
+ if err != nil {
+ return err
+ }
+ } else {
+ prototype := t.parseHTML
+ if !replace && prototype.Lookup(name) != nil {
+ name += "-" + strconv.FormatUint(t.nameCounter.Add(1), 10)
+ }
+ templ, err = prototype.New(name).Parse(ti.content)
+ if err != nil {
+ return err
+ }
+
+ if ti.subCategory == SubCategoryEmbedded {
+ // In Hugo 0.146.0 we moved the internal templates around.
+ // For the "_internal/twitter_cards.html" style templates, they
+ // were moved to the _partials directory.
+ // But we need to make them accessible from the old path for a while.
+ if pi.Type() == paths.TypePartial {
+ aliasName := strings.TrimPrefix(name, "_partials/")
+ aliasName = "_internal/" + aliasName
+ _, err = prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree)
+ if err != nil {
+ return err
+ }
+ }
+
+ // This was also possible before Hugo 0.146.0, but this should be deprecated.
+ if pi.Type() == paths.TypeShortcode {
+ aliasName := strings.TrimPrefix(name, "_shortcodes/")
+ aliasName = "_internal/shortcodes/" + aliasName
+ _, err = prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ // Issue #13599.
+ if ti.category == CategoryPartial && ti.Fi != nil && ti.Fi.Meta().PathInfo.Section() == "partials" {
+ aliasName := strings.TrimPrefix(name, "_")
+ if _, err := prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree); err != nil {
+ return err
+ }
+ }
+ }
+
+ ti.Template = templ
+
+ return nil
+}
+
+func (t *templateNamespace) applyBaseTemplate(overlay *TemplInfo, base keyTemplateInfo) error {
+ tb := &TemplWithBaseApplied{
+ Overlay: overlay,
+ Base: base.Info,
+ }
+
+ base.Info.overlays = append(base.Info.overlays, overlay)
+
+ var templ tpl.Template
+ if overlay.D.IsPlainText {
+ tt := texttemplate.Must(t.parseText.Clone()).New(overlay.PathInfo.PathNoLeadingSlash())
+ var err error
+ tt, err = tt.Parse(base.Info.content)
+ if err != nil {
+ return err
+ }
+ tt, err = tt.Parse(overlay.content)
+ if err != nil {
+ return err
+ }
+ templ = tt
+ t.baseofTextClones = append(t.baseofTextClones, tt)
+ } else {
+ tt := htmltemplate.Must(t.parseHTML.CloneShallow()).New(overlay.PathInfo.PathNoLeadingSlash())
+ var err error
+ tt, err = tt.Parse(base.Info.content)
+ if err != nil {
+ return err
+ }
+ tt, err = tt.Parse(overlay.content)
+ if err != nil {
+ return err
+ }
+ templ = tt
+
+ t.baseofHtmlClones = append(t.baseofHtmlClones, tt)
+
+ }
+
+ tb.Template = &TemplInfo{
+ Template: templ,
+ base: base.Info,
+ PathInfo: overlay.PathInfo,
+ Fi: overlay.Fi,
+ D: overlay.D,
+ noBaseOf: true,
+ }
+
+ variants := overlay.baseVariants.Get(base.Key)
+ if variants == nil {
+ variants = make(map[TemplateDescriptor]*TemplWithBaseApplied)
+ overlay.baseVariants.Insert(base.Key, variants)
+ }
+ variants[base.Info.D] = tb
+ return nil
+}
+
+func (t *templateNamespace) templatesIn(in tpl.Template) iter.Seq[tpl.Template] {
+ return func(yield func(t tpl.Template) bool) {
+ switch in := in.(type) {
+ case *htmltemplate.Template:
+ for t := range in.All() {
+ if !yield(t) {
+ return
+ }
+ }
+
+ case *texttemplate.Template:
+ for t := range in.All() {
+ if !yield(t) {
+ return
+ }
+ }
+ }
+ }
+}
+
+/*
+
+
+func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Template, error) {
+ if overlay.isText {
+ var (
+ templ = t.main.getPrototypeText(prototypeCloneIDBaseof).New(overlay.name)
+ err error
+ )
+
+ if !base.IsZero() {
+ templ, err = templ.Parse(base.template)
+ if err != nil {
+ return nil, base.errWithFileContext("text: base: parse failed", err)
+ }
+ }
+
+ templ, err = texttemplate.Must(templ.Clone()).Parse(overlay.template)
+ if err != nil {
+ return nil, overlay.errWithFileContext("text: overlay: parse failed", err)
+ }
+
+ // The extra lookup is a workaround, see
+ // * https://github.com/golang/go/issues/16101
+ // * https://github.com/gohugoio/hugo/issues/2549
+ // templ = templ.Lookup(templ.Name())
+
+ return templ, nil
+ }
+
+ var (
+ templ = t.main.getPrototypeHTML(prototypeCloneIDBaseof).New(overlay.name)
+ err error
+ )
+
+ if !base.IsZero() {
+ templ, err = templ.Parse(base.template)
+ if err != nil {
+ return nil, base.errWithFileContext("html: base: parse failed", err)
+ }
+ }
+
+ templ, err = htmltemplate.Must(templ.Clone()).Parse(overlay.template)
+ if err != nil {
+ return nil, overlay.errWithFileContext("html: overlay: parse failed", err)
+ }
+
+ // The extra lookup is a workaround, see
+ // * https://github.com/golang/go/issues/16101
+ // * https://github.com/gohugoio/hugo/issues/2549
+ templ = templ.Lookup(templ.Name())
+
+ return templ, err
+}
+
+*/
+
+var baseTemplateDefineRe = regexp.MustCompile(`^{{-?\s*define`)
+
+// needsBaseTemplate returns true if the first non-comment template block is a
+// define block.
+func needsBaseTemplate(templ string) bool {
+ idx := -1
+ inComment := false
+ for i := 0; i < len(templ); {
+ if !inComment && strings.HasPrefix(templ[i:], "{{/*") {
+ inComment = true
+ i += 4
+ } else if !inComment && strings.HasPrefix(templ[i:], "{{- /*") {
+ inComment = true
+ i += 6
+ } else if inComment && strings.HasPrefix(templ[i:], "*/}}") {
+ inComment = false
+ i += 4
+ } else if inComment && strings.HasPrefix(templ[i:], "*/ -}}") {
+ inComment = false
+ i += 6
+ } else {
+ r, size := utf8.DecodeRuneInString(templ[i:])
+ if !inComment {
+ if strings.HasPrefix(templ[i:], "{{") {
+ idx = i
+ break
+ } else if !unicode.IsSpace(r) {
+ break
+ }
+ }
+ i += size
+ }
+ }
+
+ if idx == -1 {
+ return false
+ }
+
+ return baseTemplateDefineRe.MatchString(templ[idx:])
+}
+
+func removeLeadingBOM(s string) string {
+ const bom = '\ufeff'
+
+ for i, r := range s {
+ if i == 0 && r != bom {
+ return s
+ }
+ if i > 0 {
+ return s[i:]
+ }
+ }
+
+ return s
+}
+
+type templateNamespace struct {
+ parseText *texttemplate.Template
+ parseHTML *htmltemplate.Template
+ prototypeText *texttemplate.Template
+ prototypeHTML *htmltemplate.Template
+
+ nameCounter atomic.Uint64
+
+ standaloneText *texttemplate.Template
+
+ baseofTextClones []*texttemplate.Template
+ baseofHtmlClones []*htmltemplate.Template
+}
+
+func (t *templateNamespace) createPrototypesParse() error {
+ if t.prototypeHTML == nil {
+ panic("prototypeHTML not set")
+ }
+ t.parseHTML = htmltemplate.Must(t.prototypeHTML.Clone())
+ t.parseText = texttemplate.Must(t.prototypeText.Clone())
+ return nil
+}
+
+func (t *templateNamespace) createPrototypes(init bool) error {
+ if init {
+ t.prototypeHTML = htmltemplate.Must(t.parseHTML.Clone())
+ t.prototypeText = texttemplate.Must(t.parseText.Clone())
+ }
+
+ return nil
+}
+
+func newTemplateNamespace(funcs map[string]any) *templateNamespace {
+ return &templateNamespace{
+ parseHTML: htmltemplate.New("").Funcs(funcs),
+ parseText: texttemplate.New("").Funcs(funcs),
+ standaloneText: texttemplate.New("").Funcs(funcs),
+ }
+}
+
+func isText(t tpl.Template) bool {
+ switch t.(type) {
+ case *texttemplate.Template:
+ return true
+ case *htmltemplate.Template:
+ return false
+ default:
+ panic("unknown template type")
+ }
+}
diff --git a/tpl/tplimpl/templatestore.go b/tpl/tplimpl/templatestore.go
new file mode 100644
index 000000000..23c821cac
--- /dev/null
+++ b/tpl/tplimpl/templatestore.go
@@ -0,0 +1,2073 @@
+// Copyright 2025 The Hugo Authors. All rights reserved.
+//
+// Portions Copyright The Go Authors.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tplimpl
+
+import (
+ "bytes"
+ "context"
+ "embed"
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "iter"
+ "os"
+ "path"
+ "path/filepath"
+ "reflect"
+ "regexp"
+ "sort"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/paths"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/hugofs/files"
+ "github.com/gohugoio/hugo/hugolib/doctree"
+ "github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/metrics"
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/resources/kinds"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/tpl"
+ htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
+ texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
+ "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
+ "github.com/spf13/afero"
+)
+
+const (
+ CategoryLayout Category = iota + 1
+ CategoryBaseof
+ CategoryMarkup
+ CategoryShortcode
+ CategoryPartial
+ // Internal categories
+ CategoryServer
+ CategoryHugo
+)
+
+const (
+ SubCategoryMain SubCategory = iota
+ SubCategoryEmbedded // Internal Hugo templates
+ SubCategoryInline // Inline partials
+)
+
+const (
+ containerMarkup = "_markup"
+ containerShortcodes = "_shortcodes"
+ shortcodesPathIdentifier = "/_shortcodes/"
+ containerPartials = "_partials"
+)
+
+const (
+ layoutAll = "all"
+ layoutList = "list"
+ layoutSingle = "single"
+)
+
+var (
+ _ identity.IdentityProvider = (*TemplInfo)(nil)
+ _ identity.IsProbablyDependentProvider = (*TemplInfo)(nil)
+ _ identity.IsProbablyDependencyProvider = (*TemplInfo)(nil)
+)
+
+const (
+ processingStateInitial processingState = iota
+ processingStateTransformed
+)
+
+// The identifiers may be truncated in the log, e.g.
+// "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image"
+// We need this to identify position in templates with base templates applied.
+var identifiersRe = regexp.MustCompile(`at \<(.*?)(\.{3})?\>:`)
+
+var weightNoMatch = weight{w1: -1}
+
+//
+//go:embed all:embedded/templates/*
+var embeddedTemplatesFs embed.FS
+
+func NewStore(opts StoreOptions, siteOpts SiteOptions) (*TemplateStore, error) {
+ html, ok := opts.OutputFormats.GetByName("html")
+ if !ok {
+ panic("HTML output format not found")
+ }
+ s := &TemplateStore{
+ opts: opts,
+ siteOpts: siteOpts,
+ optsOrig: opts,
+ siteOptsOrig: siteOpts,
+ htmlFormat: html,
+ storeSite: configureSiteStorage(siteOpts, opts.Watching),
+ treeMain: doctree.NewSimpleTree[map[nodeKey]*TemplInfo](),
+ treeShortcodes: doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](),
+ templatesByPath: maps.NewCache[string, *TemplInfo](),
+ shortcodesByName: maps.NewCache[string, *TemplInfo](),
+ cacheLookupPartials: maps.NewCache[string, *TemplInfo](),
+ templatesSnapshotSet: maps.NewCache[*parse.Tree, struct{}](),
+
+ // Note that the funcs passed below is just for name validation.
+ tns: newTemplateNamespace(siteOpts.TemplateFuncs),
+
+ dh: descriptorHandler{
+ opts: opts,
+ },
+ }
+
+ if err := s.init(); err != nil {
+ return nil, err
+ }
+ if err := s.insertTemplates(nil, false); err != nil {
+ return nil, err
+ }
+ if err := s.insertEmbedded(); err != nil {
+ return nil, err
+ }
+ if err := s.parseTemplates(false); err != nil {
+ return nil, err
+ }
+ if err := s.extractInlinePartials(false); err != nil {
+ return nil, err
+ }
+ if err := s.transformTemplates(); err != nil {
+ return nil, err
+ }
+ if err := s.tns.createPrototypes(true); err != nil {
+ return nil, err
+ }
+ if err := s.prepareTemplates(); err != nil {
+ return nil, err
+ }
+ return s, nil
+}
+
+//go:generate stringer -type Category
+
+type Category int
+
+type SiteOptions struct {
+ Site page.Site
+ TemplateFuncs map[string]any
+}
+
+type StoreOptions struct {
+ // The filesystem to use.
+ Fs afero.Fs
+
+ // The logger to use.
+ Log loggers.Logger
+
+ // The path parser to use.
+ PathParser *paths.PathParser
+
+ // Set when --enableTemplateMetrics is set.
+ Metrics metrics.Provider
+
+ // All configured output formats.
+ OutputFormats output.Formats
+
+ // All configured media types.
+ MediaTypes media.Types
+
+ // The default content language.
+ DefaultContentLanguage string
+
+ // The default output format.
+ DefaultOutputFormat string
+
+ // Taxonomy config.
+ TaxonomySingularPlural map[string]string
+
+ // Whether we are in watch or server mode.
+ Watching bool
+
+ // compiled.
+ legacyMappingTaxonomy map[string]legacyOrdinalMapping
+ legacyMappingTerm map[string]legacyOrdinalMapping
+ legacyMappingSection map[string]legacyOrdinalMapping
+}
+
+//go:generate stringer -type SubCategory
+
+type SubCategory int
+
+type TemplInfo struct {
+ // The category of this template.
+ category Category
+
+ subCategory SubCategory
+
+ // PathInfo info.
+ PathInfo *paths.Path
+
+ // Set when backed by a file.
+ Fi hugofs.FileMetaInfo
+
+ // The template content with any leading BOM removed.
+ content string
+
+ // The parsed template.
+ // Note that any baseof template will be applied later.
+ Template tpl.Template
+
+ // If no baseof is needed, this will be set to true.
+ // E.g. shortcode templates do not need a baseof.
+ noBaseOf bool
+
+ // If NoBaseOf is false, we will look for the final template in this tree.
+ baseVariants *doctree.SimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied]
+
+ // The template variants that are based on this template.
+ overlays []*TemplInfo
+
+ // The base template used, if any.
+ base *TemplInfo
+
+ // The descriptior that this template represents.
+ D TemplateDescriptor
+
+ // Parser state.
+ ParseInfo ParseInfo
+
+ // The execution counter for this template.
+ executionCounter atomic.Uint64
+
+ // processing state.
+ state processingState
+ isLegacyMapped bool
+}
+
+func (ti *TemplInfo) SubCategory() SubCategory {
+ return ti.subCategory
+}
+
+func (ti *TemplInfo) BaseVariantsSeq() iter.Seq[*TemplWithBaseApplied] {
+ return func(yield func(*TemplWithBaseApplied) bool) {
+ ti.baseVariants.Walk(func(key string, v map[TemplateDescriptor]*TemplWithBaseApplied) (bool, error) {
+ for _, vv := range v {
+ if !yield(vv) {
+ return true, nil
+ }
+ }
+ return false, nil
+ })
+ }
+}
+
+func (t *TemplInfo) IdentifierBase() string {
+ if t.PathInfo == nil {
+ return t.Name()
+ }
+ return t.PathInfo.IdentifierBase()
+}
+
+func (t *TemplInfo) GetIdentity() identity.Identity {
+ return t
+}
+
+func (ti *TemplInfo) Name() string {
+ if ti.Template == nil {
+ if ti.PathInfo != nil {
+ return ti.PathInfo.PathNoLeadingSlash()
+ }
+ }
+ return ti.Template.Name()
+}
+
+func (ti *TemplInfo) Prepare() (*texttemplate.Template, error) {
+ return ti.Template.Prepare()
+}
+
+func (t *TemplInfo) IsProbablyDependency(other identity.Identity) bool {
+ return t.isProbablyTheSameIDAs(other)
+}
+
+func (t *TemplInfo) IsProbablyDependent(other identity.Identity) bool {
+ for _, overlay := range t.overlays {
+ if overlay.isProbablyTheSameIDAs(other) {
+ return true
+ }
+ }
+ return t.isProbablyTheSameIDAs(other)
+}
+
+func (ti *TemplInfo) String() string {
+ if ti == nil {
+ return "
"
+ }
+ return ti.PathInfo.String()
+}
+
+func (ti *TemplInfo) findBestMatchBaseof(s *TemplateStore, d1 TemplateDescriptor, k1 string, slashCountK1 int, best *bestMatch) {
+ if ti.baseVariants == nil {
+ return
+ }
+
+ ti.baseVariants.WalkPath(k1, func(k2 string, v map[TemplateDescriptor]*TemplWithBaseApplied) (bool, error) {
+ if !s.inPath(k1, k2) {
+ return false, nil
+ }
+ slashCountK2 := strings.Count(k2, "/")
+ distance := slashCountK1 - slashCountK2
+
+ for d2, vv := range v {
+ weight := s.dh.compareDescriptors(CategoryBaseof, false, d1, d2)
+ weight.distance = distance
+ if best.isBetter(weight, vv.Template) {
+ best.updateValues(weight, k2, d2, vv.Template)
+ }
+ }
+ return false, nil
+ })
+}
+
+func (t *TemplInfo) isProbablyTheSameIDAs(other identity.Identity) bool {
+ if t.IdentifierBase() == other.IdentifierBase() {
+ return true
+ }
+
+ if t.Fi != nil && t.Fi.Meta().PathInfo != t.PathInfo {
+ return other.IdentifierBase() == t.Fi.Meta().PathInfo.IdentifierBase()
+ }
+
+ return false
+}
+
+// Implements the additional methods in tpl.CurrentTemplateInfoOps.
+func (ti *TemplInfo) Base() tpl.CurrentTemplateInfoCommonOps {
+ return ti.base
+}
+
+func (ti *TemplInfo) Filename() string {
+ if ti.Fi == nil {
+ return ""
+ }
+ return ti.Fi.Meta().Filename
+}
+
+type TemplWithBaseApplied struct {
+ // The template that's overlaid on top of the base template.
+ Overlay *TemplInfo
+ // The base template.
+ Base *TemplInfo
+ // This is the final template that can be used to render a page.
+ Template *TemplInfo
+}
+
+// TemplateQuery is used in LookupPagesLayout to find the best matching template.
+type TemplateQuery struct {
+ // The path to walk down to.
+ Path string
+
+ // The name to look for. Used for shortcode queries.
+ Name string
+
+ // The category to look in.
+ Category Category
+
+ // The template descriptor to match against.
+ Desc TemplateDescriptor
+
+ // Whether to even consider this candidate.
+ Consider func(candidate *TemplInfo) bool
+}
+
+func (q *TemplateQuery) init() {
+ if q.Desc.Kind == kinds.KindTemporary {
+ q.Desc.Kind = ""
+ } else if kinds.GetKindMain(q.Desc.Kind) == "" {
+ q.Desc.Kind = ""
+ }
+ if q.Desc.LayoutFromTemplate == "" && q.Desc.Kind != "" {
+ if q.Desc.Kind == kinds.KindPage {
+ q.Desc.LayoutFromTemplate = layoutSingle
+ } else {
+ q.Desc.LayoutFromTemplate = layoutList
+ }
+ }
+
+ if q.Consider == nil {
+ q.Consider = func(match *TemplInfo) bool {
+ return true
+ }
+ }
+
+ q.Name = strings.ToLower(q.Name)
+
+ if q.Category == 0 {
+ panic("category not set")
+ }
+}
+
+type TemplateStore struct {
+ opts StoreOptions
+ siteOpts SiteOptions
+ htmlFormat output.Format
+
+ treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo]
+ treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo]
+ templatesByPath *maps.Cache[string, *TemplInfo]
+ shortcodesByName *maps.Cache[string, *TemplInfo]
+ templatesSnapshotSet *maps.Cache[*parse.Tree, struct{}]
+
+ dh descriptorHandler
+
+ // The template namespace.
+ tns *templateNamespace
+
+ // Site specific state.
+ // All above this is reused.
+ storeSite *storeSite
+
+ // For testing benchmarking.
+ optsOrig StoreOptions
+ siteOptsOrig SiteOptions
+
+ // caches. These need to be refreshed when the templates are refreshed.
+ cacheLookupPartials *maps.Cache[string, *TemplInfo]
+}
+
+// NewFromOpts creates a new store with the same configuration as the original.
+// Used for testing/benchmarking.
+func (s *TemplateStore) NewFromOpts() (*TemplateStore, error) {
+ return NewStore(s.optsOrig, s.siteOptsOrig)
+}
+
+// In the previous implementation of base templates in Hugo, we parsed and applied these base templates on
+// request, e.g. in the middle of rendering. The idea was that we coulnd't know upfront which layoyt/base template
+// combination that would be used.
+// This, however, added a lot of complexity involving a careful dance of template cloning and parsing
+// (Go HTML tenplates cannot be parsed after any of the templates in the tree have been executed).
+// FindAllBaseTemplateCandidates finds all base template candidates for the given descriptor so we can apply them upfront.
+// In this setup we may end up with unused base templates, but not having to do the cloning should more than make up for that.
+func (s *TemplateStore) FindAllBaseTemplateCandidates(overlayKey string, desc TemplateDescriptor) []keyTemplateInfo {
+ var result []keyTemplateInfo
+ descBaseof := desc
+ s.treeMain.Walk(func(k string, v map[nodeKey]*TemplInfo) (bool, error) {
+ for _, vv := range v {
+ if vv.category != CategoryBaseof {
+ continue
+ }
+
+ if vv.D.isKindInLayout(desc.LayoutFromTemplate) && s.dh.compareDescriptors(CategoryBaseof, false, descBaseof, vv.D).w1 > 0 {
+ result = append(result, keyTemplateInfo{Key: k, Info: vv})
+ }
+ }
+ return false, nil
+ })
+
+ return result
+}
+
+func (t *TemplateStore) ExecuteWithContext(ctx context.Context, ti *TemplInfo, wr io.Writer, data any) error {
+ defer func() {
+ ti.executionCounter.Add(1)
+ if ti.base != nil {
+ ti.base.executionCounter.Add(1)
+ }
+ }()
+
+ templ := ti.Template
+
+ parent := tpl.Context.CurrentTemplate.Get(ctx)
+ var level int
+ if parent != nil {
+ level = parent.Level + 1
+ }
+ currentTi := &tpl.CurrentTemplateInfo{
+ Parent: parent,
+ Level: level,
+ CurrentTemplateInfoOps: ti,
+ }
+
+ ctx = tpl.Context.CurrentTemplate.Set(ctx, currentTi)
+
+ const levelThreshold = 999
+ if level > levelThreshold {
+ return fmt.Errorf("maximum template call stack size exceeded in %q", ti.Filename())
+ }
+
+ if t.opts.Metrics != nil {
+ defer t.opts.Metrics.MeasureSince(templ.Name(), time.Now())
+ }
+
+ execErr := t.storeSite.executer.ExecuteWithContext(ctx, ti, wr, data)
+ if execErr != nil {
+ return t.addFileContext(ti, "execute of template failed", execErr)
+ }
+ return nil
+}
+
+func (t *TemplateStore) GetFunc(name string) (reflect.Value, bool) {
+ v, found := t.storeSite.execHelper.funcs[name]
+ return v, found
+}
+
+func (s *TemplateStore) GetIdentity(p string) identity.Identity {
+ p = paths.AddLeadingSlash(p)
+ v, found := s.templatesByPath.Get(p)
+ if !found {
+ return nil
+ }
+ return v.GetIdentity()
+}
+
+func (t *TemplateStore) LookupByPath(templatePath string) *TemplInfo {
+ v, _ := t.templatesByPath.Get(templatePath)
+ return v
+}
+
+var bestPool = sync.Pool{
+ New: func() any {
+ return &bestMatch{}
+ },
+}
+
+func (s *TemplateStore) getBest() *bestMatch {
+ v := bestPool.Get()
+ b := v.(*bestMatch)
+ b.defaultOutputformat = s.opts.DefaultOutputFormat
+ return b
+}
+
+func (s *TemplateStore) putBest(b *bestMatch) {
+ b.reset()
+ bestPool.Put(b)
+}
+
+func (s *TemplateStore) LookupPagesLayout(q TemplateQuery) *TemplInfo {
+ q.init()
+ key := s.key(q.Path)
+
+ slashCountKey := strings.Count(key, "/")
+ best1 := s.getBest()
+ defer s.putBest(best1)
+ s.findBestMatchWalkPath(q, key, slashCountKey, best1)
+ if best1.w.w1 <= 0 {
+ return nil
+ }
+ m := best1.templ
+ if m.noBaseOf {
+ return m
+ }
+ best1.reset()
+ m.findBestMatchBaseof(s, q.Desc, key, slashCountKey, best1)
+ if best1.w.w1 <= 0 {
+ return nil
+ }
+ return best1.templ
+}
+
+func (s *TemplateStore) LookupPartial(pth string) *TemplInfo {
+ ti, _ := s.cacheLookupPartials.GetOrCreate(pth, func() (*TemplInfo, error) {
+ pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, pth).ForType(paths.TypePartial)
+ k1, _, _, desc, err := s.toKeyCategoryAndDescriptor(pi)
+ if err != nil {
+ return nil, err
+ }
+ if desc.OutputFormat == "" && desc.MediaType == "" {
+ // Assume HTML.
+ desc.OutputFormat = s.htmlFormat.Name
+ desc.MediaType = s.htmlFormat.MediaType.Type
+ desc.IsPlainText = s.htmlFormat.IsPlainText
+ }
+
+ best := s.getBest()
+ defer s.putBest(best)
+ s.findBestMatchGet(s.key(path.Join(containerPartials, k1)), CategoryPartial, nil, desc, best)
+ return best.templ, nil
+ })
+
+ return ti
+}
+
+func (s *TemplateStore) LookupShortcodeByName(name string) *TemplInfo {
+ name = strings.ToLower(name)
+ ti, _ := s.shortcodesByName.Get(name)
+ if ti == nil {
+ return nil
+ }
+ return ti
+}
+
+func (s *TemplateStore) LookupShortcode(q TemplateQuery) (*TemplInfo, error) {
+ q.init()
+ k1 := s.key(q.Path)
+
+ slashCountK1 := strings.Count(k1, "/")
+
+ best := s.getBest()
+ defer s.putBest(best)
+
+ s.treeShortcodes.WalkPath(k1, func(k2 string, m map[string]map[TemplateDescriptor]*TemplInfo) (bool, error) {
+ if !s.inPath(k1, k2) {
+ return false, nil
+ }
+ slashCountK2 := strings.Count(k2, "/")
+ distance := slashCountK1 - slashCountK2
+
+ v, found := m[q.Name]
+ if !found {
+ return false, nil
+ }
+
+ for k, vv := range v {
+ best.candidates = append(best.candidates, vv)
+ if !q.Consider(vv) {
+ continue
+ }
+
+ weight := s.dh.compareDescriptors(q.Category, vv.subCategory == SubCategoryEmbedded, q.Desc, k)
+ weight.distance = distance
+ isBetter := best.isBetter(weight, vv)
+ if isBetter {
+ best.updateValues(weight, k2, k, vv)
+ }
+ }
+
+ return false, nil
+ })
+
+ if best.w.w1 <= 0 {
+ var err error
+ if s := best.candidatesAsStringSlice(); s != nil {
+ msg := fmt.Sprintf("no compatible template found for shortcode %q in %s", q.Name, s)
+ if !q.Desc.IsPlainText {
+ msg += "; note that to use plain text template shortcodes in HTML you need to use the shortcode {{% delimiter"
+ }
+ err = errors.New(msg)
+ } else {
+ err = fmt.Errorf("no template found for shortcode %q", q.Name)
+ }
+ return nil, err
+ }
+
+ return best.templ, nil
+}
+
+// PrintDebug is for testing/debugging only.
+func (s *TemplateStore) PrintDebug(prefix string, category Category, w io.Writer) {
+ if w == nil {
+ w = os.Stdout
+ }
+
+ printOne := func(key string, vv *TemplInfo) {
+ level := strings.Count(key, "/")
+ if category != vv.category {
+ return
+ }
+ s := strings.ReplaceAll(strings.TrimSpace(vv.content), "\n", " ")
+ ts := fmt.Sprintf("kind: %q layout: %q lang: %q content: %.30s", vv.D.Kind, vv.D.LayoutFromTemplate, vv.D.Lang, s)
+ fmt.Fprintf(w, "%s%s %s\n", strings.Repeat(" ", level), key, ts)
+ }
+ s.treeMain.WalkPrefix(prefix, func(key string, v map[nodeKey]*TemplInfo) (bool, error) {
+ for _, vv := range v {
+ printOne(key, vv)
+ }
+ return false, nil
+ })
+ s.treeShortcodes.WalkPrefix(prefix, func(key string, v map[string]map[TemplateDescriptor]*TemplInfo) (bool, error) {
+ for _, vv := range v {
+ for _, vv2 := range vv {
+ printOne(key, vv2)
+ }
+ }
+ return false, nil
+ })
+}
+
+func (s *TemplateStore) clearCaches() {
+ s.cacheLookupPartials.Reset()
+}
+
+// RefreshFiles refreshes this store for the files matching the given predicate.
+func (s *TemplateStore) RefreshFiles(include func(fi hugofs.FileMetaInfo) bool) error {
+ s.clearCaches()
+
+ if err := s.tns.createPrototypesParse(); err != nil {
+ return err
+ }
+ if err := s.insertTemplates(include, true); err != nil {
+ return err
+ }
+ if err := s.createTemplatesSnapshot(); err != nil {
+ return err
+ }
+ if err := s.parseTemplates(true); err != nil {
+ return err
+ }
+ if err := s.extractInlinePartials(true); err != nil {
+ return err
+ }
+
+ if err := s.transformTemplates(); err != nil {
+ return err
+ }
+ if err := s.tns.createPrototypes(false); err != nil {
+ return err
+ }
+ if err := s.prepareTemplates(); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (s *TemplateStore) HasTemplate(templatePath string) bool {
+ templatePath = strings.ToLower(templatePath)
+ templatePath = paths.AddLeadingSlash(templatePath)
+ return s.templatesByPath.Contains(templatePath)
+}
+
+func (t *TemplateStore) TextLookup(name string) *TemplInfo {
+ templ := t.tns.standaloneText.Lookup(name)
+ if templ == nil {
+ return nil
+ }
+ return &TemplInfo{
+ Template: templ,
+ }
+}
+
+func (t *TemplateStore) TextParse(name, tpl string) (*TemplInfo, error) {
+ templ, err := t.tns.standaloneText.New(name).Parse(tpl)
+ if err != nil {
+ return nil, err
+ }
+ return &TemplInfo{
+ Template: templ,
+ }, nil
+}
+
+func (t *TemplateStore) UnusedTemplates() []*TemplInfo {
+ var unused []*TemplInfo
+
+ for vv := range t.templates() {
+ if vv.subCategory != SubCategoryMain || vv.isLegacyMapped {
+ // Skip inline partials and internal templates.
+ continue
+ }
+ if vv.executionCounter.Load() == 0 {
+ unused = append(unused, vv)
+ }
+ }
+
+ sort.Sort(byPath(unused))
+ return unused
+}
+
+// WithSiteOpts creates a new store with the given site options.
+// This is used to create per site template store, all sharing the same templates,
+// but with a different template function execution context.
+func (s TemplateStore) WithSiteOpts(opts SiteOptions) *TemplateStore {
+ s.siteOpts = opts
+ s.storeSite = configureSiteStorage(opts, s.opts.Watching)
+ return &s
+}
+
+func (s *TemplateStore) findBestMatchGet(key string, category Category, consider func(candidate *TemplInfo) bool, desc TemplateDescriptor, best *bestMatch) {
+ key = strings.ToLower(key)
+
+ v := s.treeMain.Get(key)
+ if v == nil {
+ return
+ }
+
+ for k, vv := range v {
+ if vv.category != category {
+ continue
+ }
+
+ if consider != nil && !consider(vv) {
+ continue
+ }
+
+ weight := s.dh.compareDescriptors(category, vv.subCategory == SubCategoryEmbedded, desc, k.d)
+ if best.isBetter(weight, vv) {
+ best.updateValues(weight, key, k.d, vv)
+ }
+ }
+}
+
+func (s *TemplateStore) inPath(k1, k2 string) bool {
+ if k1 != k2 && !strings.HasPrefix(k1, k2+"/") {
+ return false
+ }
+ return true
+}
+
+func (s *TemplateStore) findBestMatchWalkPath(q TemplateQuery, k1 string, slashCountK1 int, best *bestMatch) {
+ s.treeMain.WalkPath(k1, func(k2 string, v map[nodeKey]*TemplInfo) (bool, error) {
+ if !s.inPath(k1, k2) {
+ return false, nil
+ }
+ slashCountK2 := strings.Count(k2, "/")
+ distance := slashCountK1 - slashCountK2
+
+ for k, vv := range v {
+ if vv.category != q.Category {
+ continue
+ }
+
+ if !q.Consider(vv) {
+ continue
+ }
+
+ weight := s.dh.compareDescriptors(q.Category, vv.subCategory == SubCategoryEmbedded, q.Desc, k.d)
+
+ weight.distance = distance
+ isBetter := best.isBetter(weight, vv)
+
+ if isBetter {
+ best.updateValues(weight, k2, k.d, vv)
+ }
+ }
+
+ return false, nil
+ })
+}
+
+func (t *TemplateStore) addDeferredTemplate(owner *TemplInfo, name string, n *parse.ListNode) error {
+ if _, found := t.templatesByPath.Get(name); found {
+ return nil
+ }
+
+ var templ tpl.Template
+
+ if owner.D.IsPlainText {
+ prototype := t.tns.parseText
+ tt, err := prototype.New(name).Parse("")
+ if err != nil {
+ return fmt.Errorf("failed to parse empty text template %q: %w", name, err)
+ }
+ tt.Tree.Root = n
+ templ = tt
+ } else {
+ prototype := t.tns.parseHTML
+ tt, err := prototype.New(name).Parse("")
+ if err != nil {
+ return fmt.Errorf("failed to parse empty HTML template %q: %w", name, err)
+ }
+ tt.Tree.Root = n
+ templ = tt
+ }
+
+ t.templatesByPath.Set(name, &TemplInfo{
+ Fi: owner.Fi,
+ PathInfo: owner.PathInfo,
+ D: owner.D,
+ Template: templ,
+ })
+
+ return nil
+}
+
+func (s *TemplateStore) addFileContext(ti *TemplInfo, what string, inerr error) error {
+ if ti.Fi == nil {
+ return inerr
+ }
+
+ identifiers := s.extractIdentifiers(inerr.Error())
+
+ checkFilename := func(fi hugofs.FileMetaInfo, inErr error) (error, bool) {
+ lineMatcher := func(m herrors.LineMatcher) int {
+ if m.Position.LineNumber != m.LineNumber {
+ return -1
+ }
+
+ for _, id := range identifiers {
+ if strings.Contains(m.Line, id) {
+ // We found the line, but return a 0 to signal to
+ // use the column from the error message.
+ return 0
+ }
+ }
+ return -1
+ }
+
+ f, err := fi.Meta().Open()
+ if err != nil {
+ return inErr, false
+ }
+ defer f.Close()
+
+ fe := herrors.NewFileErrorFromName(inErr, fi.Meta().Filename)
+ fe.UpdateContent(f, lineMatcher)
+
+ return fe, fe.ErrorContext().Position.IsValid()
+ }
+
+ inerr = fmt.Errorf("%s: %w", what, inerr)
+
+ var (
+ currentErr error
+ ok bool
+ )
+
+ if currentErr, ok = checkFilename(ti.Fi, inerr); ok {
+ return currentErr
+ }
+
+ if ti.base != nil {
+ if currentErr, ok = checkFilename(ti.base.Fi, inerr); ok {
+ return currentErr
+ }
+ }
+
+ return currentErr
+}
+
+func (s *TemplateStore) extractIdentifiers(line string) []string {
+ m := identifiersRe.FindAllStringSubmatch(line, -1)
+ identifiers := make([]string, len(m))
+ for i := range m {
+ identifiers[i] = m[i][1]
+ }
+ return identifiers
+}
+
+func (s *TemplateStore) extractInlinePartials(rebuild bool) error {
+ isPartialName := func(s string) bool {
+ return strings.HasPrefix(s, "partials/") || strings.HasPrefix(s, "_partials/")
+ }
+
+ // We may find both inline and external partials in the current template namespaces,
+ // so only add the ones we have not seen before.
+ for templ := range s.allRawTemplates() {
+ if templ.Name() == "" || !isPartialName(templ.Name()) {
+ continue
+ }
+ if rebuild && s.templatesSnapshotSet.Contains(getParseTree(templ)) {
+ // This partial was not created during this build.
+ continue
+ }
+ name := templ.Name()
+ if !paths.HasExt(name) {
+ // Assume HTML. This in line with how the lookup works.
+ name = name + s.htmlFormat.MediaType.FirstSuffix.FullSuffix
+ }
+ if !strings.HasPrefix(name, "_") {
+ name = "_" + name
+ }
+ pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, name)
+ ti, err := s.insertTemplate(pi, nil, SubCategoryInline, false, s.treeMain)
+ if err != nil {
+ return err
+ }
+
+ if ti != nil {
+ ti.Template = templ
+ ti.noBaseOf = true
+ ti.subCategory = SubCategoryInline
+ ti.D.IsPlainText = isText(templ)
+ }
+ }
+
+ return nil
+}
+
+func (s *TemplateStore) allRawTemplates() iter.Seq[tpl.Template] {
+ p := s.tns
+ return func(yield func(tpl.Template) bool) {
+ for t := range p.templatesIn(p.parseHTML) {
+ if !yield(t) {
+ return
+ }
+ }
+ for t := range p.templatesIn(p.parseText) {
+ if !yield(t) {
+ return
+ }
+ }
+
+ for _, tt := range p.baseofHtmlClones {
+ for t := range p.templatesIn(tt) {
+ if !yield(t) {
+ return
+ }
+ }
+ }
+ for _, tt := range p.baseofTextClones {
+ for t := range p.templatesIn(tt) {
+ if !yield(t) {
+ return
+ }
+ }
+ }
+ }
+}
+
+func (s *TemplateStore) insertEmbedded() error {
+ return fs.WalkDir(embeddedTemplatesFs, ".", func(tpath string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if d == nil || d.IsDir() || strings.HasPrefix(d.Name(), ".") {
+ return nil
+ }
+
+ templb, err := embeddedTemplatesFs.ReadFile(tpath)
+ if err != nil {
+ return err
+ }
+
+ // Get the newlines on Windows in line with how we had it back when we used Go Generate
+ // to write the templates to Go files.
+ templ := string(bytes.ReplaceAll(templb, []byte("\r\n"), []byte("\n")))
+ name := strings.TrimPrefix(filepath.ToSlash(tpath), "embedded/templates/")
+
+ insertOne := func(name, content string) error {
+ pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, name)
+ var (
+ ti *TemplInfo
+ err error
+ )
+ if pi.Section() == containerShortcodes {
+ ti, err = s.insertShortcode(pi, nil, false, s.treeShortcodes)
+ if err != nil {
+ return err
+ }
+ } else {
+ ti, err = s.insertTemplate(pi, nil, SubCategoryEmbedded, false, s.treeMain)
+ if err != nil {
+ return err
+ }
+ }
+
+ if ti != nil {
+ // Currently none of the embedded templates need a baseof template.
+ ti.noBaseOf = true
+ ti.content = content
+ ti.subCategory = SubCategoryEmbedded
+ }
+
+ return nil
+ }
+
+ // Copy the embedded HTML table render hook to each output format.
+ // See https://github.com/gohugoio/hugo/issues/13351.
+ if name == path.Join(containerMarkup, "render-table.html") {
+ for _, of := range s.opts.OutputFormats {
+ path := paths.TrimExt(name) + "." + of.Name + of.MediaType.FirstSuffix.FullSuffix
+ if err := insertOne(path, templ); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ }
+
+ if err := insertOne(name, templ); err != nil {
+ return err
+ }
+
+ if aliases, found := embeddedTemplatesAliases[name]; found {
+ for _, alias := range aliases {
+ if err := insertOne(alias, templ); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+ })
+}
+
+func (s *TemplateStore) setTemplateByPath(p string, ti *TemplInfo) {
+ s.templatesByPath.Set(p, ti)
+}
+
+func (s *TemplateStore) insertShortcode(pi *paths.Path, fi hugofs.FileMetaInfo, replace bool, tree doctree.Tree[map[string]map[TemplateDescriptor]*TemplInfo]) (*TemplInfo, error) {
+ k1, k2, _, d, err := s.toKeyCategoryAndDescriptor(pi)
+ if err != nil {
+ return nil, err
+ }
+ m := tree.Get(k1)
+ if m == nil {
+ m = make(map[string]map[TemplateDescriptor]*TemplInfo)
+ tree.Insert(k1, m)
+ }
+
+ m1, found := m[k2]
+ if found {
+ if _, found := m1[d]; found {
+ if !replace {
+ return nil, nil
+ }
+ }
+ } else {
+ m1 = make(map[TemplateDescriptor]*TemplInfo)
+ m[k2] = m1
+ }
+
+ ti := &TemplInfo{
+ PathInfo: pi,
+ Fi: fi,
+ D: d,
+ category: CategoryShortcode,
+ noBaseOf: true,
+ }
+
+ m1[d] = ti
+
+ s.shortcodesByName.Set(k2, ti)
+ s.setTemplateByPath(pi.Path(), ti)
+
+ if fi != nil {
+ if pi2 := fi.Meta().PathInfo; pi2 != pi {
+ s.setTemplateByPath(pi2.Path(), ti)
+ }
+ }
+
+ return ti, nil
+}
+
+func (s *TemplateStore) insertTemplate(pi *paths.Path, fi hugofs.FileMetaInfo, subCategory SubCategory, replace bool, tree doctree.Tree[map[nodeKey]*TemplInfo]) (*TemplInfo, error) {
+ key, _, category, d, err := s.toKeyCategoryAndDescriptor(pi)
+ // See #13577. Warn for now.
+ if err != nil {
+ var loc string
+ if fi != nil {
+ loc = fmt.Sprintf("file %q", fi.Meta().Filename)
+ } else {
+ loc = fmt.Sprintf("path %q", pi.Path())
+ }
+ s.opts.Log.Warnf("skipping template %s: %s", loc, err)
+ return nil, nil
+ }
+
+ return s.insertTemplate2(pi, fi, key, category, subCategory, d, replace, false, tree)
+}
+
+func (s *TemplateStore) insertTemplate2(
+ pi *paths.Path,
+ fi hugofs.FileMetaInfo,
+ key string,
+ category Category,
+ subCategory SubCategory,
+ d TemplateDescriptor,
+ replace, isLegacyMapped bool,
+ tree doctree.Tree[map[nodeKey]*TemplInfo],
+) (*TemplInfo, error) {
+ if category == 0 {
+ panic("category not set")
+ }
+
+ if category == CategoryPartial && d.OutputFormat == "" && d.MediaType == "" {
+ // See issue #13601.
+ d.OutputFormat = s.htmlFormat.Name
+ d.MediaType = s.htmlFormat.MediaType.Type
+ }
+
+ m := tree.Get(key)
+ nk := nodeKey{c: category, d: d}
+
+ if m == nil {
+ m = make(map[nodeKey]*TemplInfo)
+ tree.Insert(key, m)
+ }
+
+ nkExisting, existingFound := m[nk]
+ if !replace && existingFound && fi != nil && nkExisting.Fi != nil {
+ // See issue #13715.
+ // We do the merge on the file system level, but from Hugo v0.146.0 we have a situation where
+ // the project may well have a different layouts layout compared to the theme(s) it uses.
+ // We could possibly have fixed that on a lower (file system) level, but since this is just
+ // a temporary situation (until all projects are updated),
+ // do a replace here if the file comes from higher up in the module chain.
+ replace = fi.Meta().ModuleOrdinal < nkExisting.Fi.Meta().ModuleOrdinal
+ }
+
+ if !replace && existingFound {
+ // Always replace inline partials to allow for reloading.
+ replace = subCategory == SubCategoryInline && nkExisting.subCategory == SubCategoryInline
+ }
+
+ if !replace && existingFound {
+ if len(pi.Identifiers()) >= len(nkExisting.PathInfo.Identifiers()) {
+ // e.g. /pages/home.foo.html and /pages/home.html where foo may be a valid language name in another site.
+ return nil, nil
+ }
+ }
+
+ ti := &TemplInfo{
+ PathInfo: pi,
+ Fi: fi,
+ D: d,
+ category: category,
+ noBaseOf: category > CategoryLayout,
+ isLegacyMapped: isLegacyMapped,
+ }
+
+ m[nk] = ti
+
+ if !isLegacyMapped {
+ s.setTemplateByPath(pi.Path(), ti)
+ if fi != nil {
+ if pi2 := fi.Meta().PathInfo; pi2 != pi {
+ s.setTemplateByPath(pi2.Path(), ti)
+ }
+ }
+ }
+
+ return ti, nil
+}
+
+func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) bool, partialRebuild bool) error {
+ if include == nil {
+ include = func(fi hugofs.FileMetaInfo) bool {
+ return true
+ }
+ }
+
+ // Set if we need to reset the base variants.
+ var (
+ resetBaseVariants bool
+ )
+
+ legacyOrdinalMappings := map[legacyTargetPathIdentifiers]legacyOrdinalMappingFi{}
+
+ walker := func(pth string, fi hugofs.FileMetaInfo) error {
+ if fi.IsDir() {
+ return nil
+ }
+
+ if isDotFile(pth) || isBackupFile(pth) {
+ return nil
+ }
+
+ if !include(fi) {
+ return nil
+ }
+
+ piOrig := fi.Meta().PathInfo
+
+ // Convert any legacy value to new format.
+ fromLegacyPath := func(pi *paths.Path) *paths.Path {
+ p := pi.Path()
+ p = strings.TrimPrefix(p, "/_default")
+ if strings.HasPrefix(p, "/shortcodes") || strings.HasPrefix(p, "/partials") {
+ // Insert an underscore so it becomes /_shortcodes or /_partials.
+ p = "/_" + p[1:]
+ }
+
+ if strings.Contains(p, "-"+baseNameBaseof) {
+ // Before Hugo 0.146.0 we prepended one identifier (layout, type or kind) in front of the baseof keyword,
+ // and then separated with a hyphen before the baseof keyword.
+ // This identifier needs to be moved right after the baseof keyword and the hyphen removed, e.g.
+ // /docs/list-baseof.html => /docs/baseof.list.html.
+ dir, name := path.Split(p)
+ hyphenIdx := strings.Index(name, "-")
+ if hyphenIdx > 0 {
+ id := name[:hyphenIdx]
+ name = name[hyphenIdx+1+len(baseNameBaseof):]
+ if !strings.HasPrefix(name, ".") {
+ name = "." + name
+ }
+ p = path.Join(dir, baseNameBaseof+"."+id+name)
+ }
+ }
+ if p == pi.Path() {
+ return pi
+ }
+ return s.opts.PathParser.Parse(files.ComponentFolderLayouts, p)
+ }
+
+ pi := piOrig
+ var applyLegacyMapping bool
+ switch pi.Section() {
+ case containerPartials, containerShortcodes, containerMarkup:
+ // OK.
+ default:
+ pi = fromLegacyPath(pi)
+ applyLegacyMapping = strings.Count(pi.Path(), "/") <= 2
+ }
+
+ if applyLegacyMapping {
+ handleMapping := func(m1 legacyOrdinalMapping) {
+ key := legacyTargetPathIdentifiers{
+ targetPath: m1.mapping.targetPath,
+ targetCategory: m1.mapping.targetCategory,
+ kind: m1.mapping.targetDesc.Kind,
+ lang: pi.Lang(),
+ ext: pi.Ext(),
+ outputFormat: pi.OutputFormat(),
+ }
+
+ if m2, ok := legacyOrdinalMappings[key]; ok {
+ if m1.ordinal < m2.m.ordinal {
+ // Higher up == better match.
+ legacyOrdinalMappings[key] = legacyOrdinalMappingFi{m1, fi}
+ }
+ } else {
+ legacyOrdinalMappings[key] = legacyOrdinalMappingFi{m1, fi}
+ }
+ }
+
+ if m1, ok := s.opts.legacyMappingTaxonomy[piOrig.PathBeforeLangAndOutputFormatAndExt()]; ok {
+ handleMapping(m1)
+ }
+
+ if m1, ok := s.opts.legacyMappingTerm[piOrig.PathBeforeLangAndOutputFormatAndExt()]; ok {
+ handleMapping(m1)
+ }
+
+ const (
+ sectionKindToken = "SECTIONKIND"
+ sectionToken = "THESECTION"
+ )
+
+ base := piOrig.PathBeforeLangAndOutputFormatAndExt()
+ identifiers := []string{}
+ if pi.Layout() != "" {
+ identifiers = append(identifiers, pi.Layout())
+ }
+ if pi.Kind() != "" {
+ identifiers = append(identifiers, pi.Kind())
+ }
+
+ shouldIncludeSection := func(section string) bool {
+ switch section {
+ case containerShortcodes, containerPartials, containerMarkup:
+ return false
+ case "taxonomy", "":
+ return false
+ default:
+ for k, v := range s.opts.TaxonomySingularPlural {
+ if k == section || v == section {
+ return false
+ }
+ }
+ return true
+ }
+ }
+ if shouldIncludeSection(pi.Section()) {
+ identifiers = append(identifiers, pi.Section())
+ }
+
+ identifiers = helpers.UniqueStrings(identifiers)
+
+ // Tokens on e.g. form /SECTIONKIND/THESECTION
+ insertSectionTokens := func(section string) []string {
+ kindOnly := isLayoutStandard(section)
+ var ss []string
+ s1 := base
+ if !kindOnly {
+ s1 = strings.ReplaceAll(s1, section, sectionToken)
+ }
+ s1 = strings.ReplaceAll(s1, kinds.KindSection, sectionKindToken)
+ if s1 != base {
+ ss = append(ss, s1)
+ }
+ s1 = strings.ReplaceAll(base, kinds.KindSection, sectionKindToken)
+ if !kindOnly {
+ s1 = strings.ReplaceAll(s1, section, sectionToken)
+ }
+ if s1 != base {
+ ss = append(ss, s1)
+ }
+
+ helpers.UniqueStringsReuse(ss)
+
+ return ss
+ }
+
+ for _, id := range identifiers {
+ if id == "" {
+ continue
+ }
+
+ p := insertSectionTokens(id)
+ for _, ss := range p {
+ if m1, ok := s.opts.legacyMappingSection[ss]; ok {
+ targetPath := m1.mapping.targetPath
+
+ if targetPath != "" {
+ targetPath = strings.ReplaceAll(targetPath, sectionToken, id)
+ targetPath = strings.ReplaceAll(targetPath, sectionKindToken, id)
+ targetPath = strings.ReplaceAll(targetPath, "//", "/")
+ }
+ m1.mapping.targetPath = targetPath
+ handleMapping(m1)
+ }
+ }
+ }
+
+ }
+
+ if partialRebuild && pi.NameNoIdentifier() == baseNameBaseof {
+ // A baseof file has changed.
+ resetBaseVariants = true
+ }
+
+ var ti *TemplInfo
+ var err error
+ if pi.Type() == paths.TypeShortcode {
+ ti, err = s.insertShortcode(pi, fi, partialRebuild, s.treeShortcodes)
+ if err != nil || ti == nil {
+ return err
+ }
+ } else {
+ ti, err = s.insertTemplate(pi, fi, SubCategoryMain, partialRebuild, s.treeMain)
+ if err != nil || ti == nil {
+ return err
+ }
+ }
+
+ if err := s.tns.readTemplateInto(ti); err != nil {
+ return err
+ }
+
+ return nil
+ }
+
+ if err := helpers.Walk(s.opts.Fs, "", walker); err != nil {
+ if !herrors.IsNotExist(err) {
+ return err
+ }
+ return nil
+ }
+
+ for k, v := range legacyOrdinalMappings {
+ targetPath := k.targetPath
+ m := v.m.mapping
+ fi := v.fi
+ pi := fi.Meta().PathInfo
+ outputFormat, mediaType := s.resolveOutputFormatAndOrMediaType(k.outputFormat, k.ext)
+ category := m.targetCategory
+ desc := m.targetDesc
+ desc.Kind = k.kind
+ desc.Lang = k.lang
+ desc.OutputFormat = outputFormat.Name
+ desc.IsPlainText = outputFormat.IsPlainText
+ desc.MediaType = mediaType.Type
+
+ ti, err := s.insertTemplate2(pi, fi, targetPath, category, SubCategoryMain, desc, true, true, s.treeMain)
+ if err != nil {
+ return err
+ }
+ if ti == nil {
+ continue
+ }
+ ti.isLegacyMapped = true
+ if err := s.tns.readTemplateInto(ti); err != nil {
+ return err
+ }
+
+ }
+
+ if resetBaseVariants {
+ s.tns.baseofHtmlClones = nil
+ s.tns.baseofTextClones = nil
+ s.treeMain.Walk(func(key string, v map[nodeKey]*TemplInfo) (bool, error) {
+ for _, vv := range v {
+ if !vv.noBaseOf {
+ vv.state = processingStateInitial
+ }
+ }
+ return false, nil
+ })
+ }
+
+ return nil
+}
+
+func (s *TemplateStore) key(dir string) string {
+ dir = paths.AddLeadingSlash(dir)
+ if dir == "/" {
+ return ""
+ }
+ return paths.TrimTrailing(dir)
+}
+
+func (s *TemplateStore) createTemplatesSnapshot() error {
+ s.templatesSnapshotSet.Reset()
+ for t := range s.allRawTemplates() {
+ s.templatesSnapshotSet.Set(getParseTree(t), struct{}{})
+ }
+ return nil
+}
+
+func (s *TemplateStore) parseTemplates(replace bool) error {
+ if err := func() error {
+ // Read and parse all templates.
+ for _, v := range s.treeMain.All() {
+ for _, vv := range v {
+ if vv.state == processingStateTransformed {
+ continue
+ }
+ if err := s.parseTemplate(vv, replace); err != nil {
+ return err
+ }
+ }
+ }
+
+ // Lookup and apply base templates where needed.
+ for key, v := range s.treeMain.All() {
+ for _, vv := range v {
+ if vv.state == processingStateTransformed {
+ continue
+ }
+ if !vv.noBaseOf {
+ d := vv.D
+ // Find all compatible base templates.
+ baseTemplates := s.FindAllBaseTemplateCandidates(key, d)
+ if len(baseTemplates) == 0 {
+ // The regular expression used to detect if a template needs a base template has some
+ // rare false positives. Assume we don't need one.
+ vv.noBaseOf = true
+ if err := s.parseTemplate(vv, replace); err != nil {
+ return err
+ }
+ continue
+ }
+ vv.baseVariants = doctree.NewSimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied]()
+
+ for _, base := range baseTemplates {
+ if err := s.tns.applyBaseTemplate(vv, base); err != nil {
+ return err
+ }
+ }
+
+ }
+ }
+ }
+
+ return nil
+ }(); err != nil {
+ return err
+ }
+
+ // Prese shortcodes.
+ for _, v := range s.treeShortcodes.All() {
+ for _, vv := range v {
+ for _, vvv := range vv {
+ if vvv.state == processingStateTransformed {
+ continue
+ }
+ if err := s.parseTemplate(vvv, replace); err != nil {
+ return err
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+// prepareTemplates prepares all templates for execution.
+func (s *TemplateStore) prepareTemplates() error {
+ for t := range s.templates() {
+ if t.category == CategoryBaseof {
+ continue
+ }
+ if _, err := t.Prepare(); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+type PathTemplateDescriptor struct {
+ Path string
+ Desc TemplateDescriptor
+}
+
+// resolveOutputFormatAndOrMediaType resolves the output format and/or media type
+// based on the given output format suffix and media type suffix.
+// Either of the suffixes can be empty, and the function will try to find a match
+// based on the other suffix. If both are empty, the function will return zero values.
+func (s *TemplateStore) resolveOutputFormatAndOrMediaType(ofs, mns string) (output.Format, media.Type) {
+ var outputFormat output.Format
+ var mediaType media.Type
+
+ if ofs != "" {
+ if of, found := s.opts.OutputFormats.GetByName(ofs); found {
+ outputFormat = of
+ mediaType = of.MediaType
+ }
+ }
+
+ if mns != "" && mediaType.IsZero() {
+ if of, found := s.opts.OutputFormats.GetBySuffix(mns); found {
+ outputFormat = of
+ mediaType = of.MediaType
+ } else {
+ if mt, _, found := s.opts.MediaTypes.GetFirstBySuffix(mns); found {
+ mediaType = mt
+ if outputFormat.IsZero() {
+ // For e.g. index.xml we will in the default confg now have the application/rss+xml media type.
+ // Try a last time to find the output format using the SubType as the name.
+ // As to template resolution, this value is currently only used to
+ // decide if this is a text or HTML template.
+ outputFormat, _ = s.opts.OutputFormats.GetByName(mt.SubType)
+ }
+ }
+ }
+ }
+
+ return outputFormat, mediaType
+}
+
+// templates iterates over all templates in the store.
+// Note that for templates with one or more base templates applied,
+// we will yield the variants, e.g. the templates that's actually in use.
+func (s *TemplateStore) templates() iter.Seq[*TemplInfo] {
+ return func(yield func(*TemplInfo) bool) {
+ for _, v := range s.treeMain.All() {
+ for _, vv := range v {
+ if !vv.noBaseOf {
+ for vvv := range vv.BaseVariantsSeq() {
+ if !yield(vvv.Template) {
+ return
+ }
+ }
+ } else {
+ if !yield(vv) {
+ return
+ }
+ }
+ }
+ }
+ for _, v := range s.treeShortcodes.All() {
+ for _, vv := range v {
+ for _, vvv := range vv {
+ if !yield(vvv) {
+ return
+ }
+ }
+ }
+ }
+ }
+}
+
+func (s *TemplateStore) toKeyCategoryAndDescriptor(p *paths.Path) (string, string, Category, TemplateDescriptor, error) {
+ k1 := p.Dir()
+ k2 := ""
+
+ outputFormat, mediaType := s.resolveOutputFormatAndOrMediaType(p.OutputFormat(), p.Ext())
+ nameNoIdentifier := p.NameNoIdentifier()
+
+ d := TemplateDescriptor{
+ Lang: p.Lang(),
+ OutputFormat: p.OutputFormat(),
+ MediaType: mediaType.Type,
+ Kind: p.Kind(),
+ LayoutFromTemplate: p.Layout(),
+ IsPlainText: outputFormat.IsPlainText,
+ }
+
+ d.normalizeFromFile()
+
+ section := p.Section()
+
+ var category Category
+ switch p.Type() {
+ case paths.TypeShortcode:
+ category = CategoryShortcode
+ case paths.TypePartial:
+ category = CategoryPartial
+ case paths.TypeMarkup:
+ category = CategoryMarkup
+ }
+
+ if category == 0 {
+ if nameNoIdentifier == baseNameBaseof {
+ category = CategoryBaseof
+ } else {
+ switch section {
+ case "_hugo":
+ category = CategoryHugo
+ case "_server":
+ category = CategoryServer
+ default:
+ category = CategoryLayout
+ }
+ }
+ }
+
+ if category == CategoryPartial {
+ d.LayoutFromTemplate = ""
+ k1 = p.PathNoIdentifier()
+ }
+
+ if category == CategoryShortcode {
+ k1 = p.PathNoIdentifier()
+
+ parts := strings.Split(k1, "/"+containerShortcodes+"/")
+ k1 = parts[0]
+ if len(parts) > 1 {
+ k2 = parts[1]
+ }
+ k1 = s.key(k1)
+ }
+
+ // Legacy layout for home page.
+ if d.LayoutFromTemplate == "index" {
+ if d.Kind == "" {
+ d.Kind = kinds.KindHome
+ }
+ d.LayoutFromTemplate = ""
+ }
+
+ if d.LayoutFromTemplate == d.Kind {
+ d.LayoutFromTemplate = ""
+ }
+
+ k1 = strings.TrimPrefix(k1, "/_default")
+ if k1 == "/" {
+ k1 = ""
+ }
+
+ if category == CategoryMarkup {
+ // We store all template nodes for a given directory on the same level.
+ k1 = strings.TrimSuffix(k1, "/_markup")
+ parts := strings.Split(d.LayoutFromTemplate, "-")
+ if len(parts) < 2 {
+ return "", "", 0, TemplateDescriptor{}, fmt.Errorf("unrecognized render hook template")
+ }
+ // Either 2 or 3 parts, e.g. render-codeblock-go.
+ d.Variant1 = parts[1]
+ if len(parts) > 2 {
+ d.Variant2 = parts[2]
+ }
+ d.LayoutFromTemplate = "" // This allows using page layout as part of the key for lookups.
+ }
+
+ return k1, k2, category, d, nil
+}
+
+func (s *TemplateStore) transformTemplates() error {
+ lookup := func(name string, in *TemplInfo) *TemplInfo {
+ if in.D.IsPlainText {
+ templ := in.Template.(*texttemplate.Template).Lookup(name)
+ if templ != nil {
+ return &TemplInfo{
+ Template: templ,
+ }
+ }
+ } else {
+ templ := in.Template.(*htmltemplate.Template).Lookup(name)
+ if templ != nil {
+ return &TemplInfo{
+ Template: templ,
+ }
+ }
+ }
+
+ return nil
+ }
+
+ for vv := range s.templates() {
+ if vv.state == processingStateTransformed {
+ continue
+ }
+ vv.state = processingStateTransformed
+ if vv.category == CategoryBaseof {
+ continue
+ }
+ tctx, err := applyTemplateTransformers(vv, lookup)
+ if err != nil {
+ return err
+ }
+ for name, node := range tctx.deferNodes {
+ if err := s.addDeferredTemplate(vv, name, node); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+func (s *TemplateStore) init() error {
+ // Before Hugo 0.146 we had a very elaborate template lookup system, especially for
+ // terms and taxonomies. This is a way of preserving backwards compatibility
+ // by mapping old paths into the new tree.
+ s.opts.legacyMappingTaxonomy = make(map[string]legacyOrdinalMapping)
+ s.opts.legacyMappingTerm = make(map[string]legacyOrdinalMapping)
+ s.opts.legacyMappingSection = make(map[string]legacyOrdinalMapping)
+
+ // Placeholders.
+ const singular = "SINGULAR"
+ const plural = "PLURAL"
+
+ replaceTokens := func(s, singularv, pluralv string) string {
+ s = strings.Replace(s, singular, singularv, -1)
+ s = strings.Replace(s, plural, pluralv, -1)
+ return s
+ }
+
+ hasSingularOrPlural := func(s string) bool {
+ return strings.Contains(s, singular) || strings.Contains(s, plural)
+ }
+
+ expand := func(v layoutLegacyMapping) []layoutLegacyMapping {
+ var result []layoutLegacyMapping
+
+ if hasSingularOrPlural(v.sourcePath) || hasSingularOrPlural(v.target.targetPath) {
+ for s, p := range s.opts.TaxonomySingularPlural {
+ target := v.target
+ target.targetPath = replaceTokens(target.targetPath, s, p)
+ vv := replaceTokens(v.sourcePath, s, p)
+ result = append(result, layoutLegacyMapping{sourcePath: vv, target: target})
+ }
+ } else {
+ result = append(result, v)
+ }
+ return result
+ }
+
+ expandSections := func(v layoutLegacyMapping) []layoutLegacyMapping {
+ var result []layoutLegacyMapping
+ result = append(result, v)
+ baseofVariant := v
+ baseofVariant.sourcePath += "-" + baseNameBaseof
+ baseofVariant.target.targetCategory = CategoryBaseof
+ result = append(result, baseofVariant)
+ return result
+ }
+
+ var terms []layoutLegacyMapping
+ for _, v := range legacyTermMappings {
+ terms = append(terms, expand(v)...)
+ }
+ var taxonomies []layoutLegacyMapping
+ for _, v := range legacyTaxonomyMappings {
+ taxonomies = append(taxonomies, expand(v)...)
+ }
+ var sections []layoutLegacyMapping
+ for _, v := range legacySectionMappings {
+ sections = append(sections, expandSections(v)...)
+ }
+
+ for i, m := range terms {
+ s.opts.legacyMappingTerm[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
+ }
+ for i, m := range taxonomies {
+ s.opts.legacyMappingTaxonomy[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
+ }
+ for i, m := range sections {
+ s.opts.legacyMappingSection[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
+ }
+
+ return nil
+}
+
+type TemplateStoreProvider interface {
+ GetTemplateStore() *TemplateStore
+}
+
+type TextTemplatHandler interface {
+ ExecuteWithContext(ctx context.Context, ti *TemplInfo, wr io.Writer, data any) error
+ TextLookup(name string) *TemplInfo
+ TextParse(name, tpl string) (*TemplInfo, error)
+}
+
+type bestMatch struct {
+ templ *TemplInfo
+ desc TemplateDescriptor
+ w weight
+ key string
+ candidates []*TemplInfo
+
+ // settings.
+ defaultOutputformat string
+}
+
+func (best *bestMatch) reset() {
+ best.templ = nil
+ best.w = weight{}
+ best.desc = TemplateDescriptor{}
+ best.key = ""
+ best.candidates = nil
+}
+
+func (best *bestMatch) candidatesAsStringSlice() []string {
+ if len(best.candidates) == 0 {
+ return nil
+ }
+ candidates := make([]string, len(best.candidates))
+ for i, v := range best.candidates {
+ candidates[i] = v.PathInfo.Path()
+ }
+ return candidates
+}
+
+func (best *bestMatch) isBetter(w weight, ti *TemplInfo) bool {
+ if best.templ == nil {
+ // Anything is better than nothing.
+ return true
+ }
+
+ if w.w1 <= 0 {
+ if best.w.w1 <= 0 {
+ return ti.PathInfo.Path() < best.templ.PathInfo.Path()
+ }
+ return false
+ }
+
+ // Note that for render hook templates, we need to make
+ // the embedded render hook template wih if they're a better match,
+ // e.g. render-codeblock-goat.html.
+ if best.templ.category != CategoryMarkup && best.w.w1 > 0 {
+ currentBestIsEmbedded := best.templ.subCategory == SubCategoryEmbedded
+ if currentBestIsEmbedded {
+ if ti.subCategory != SubCategoryEmbedded {
+ return true
+ }
+ } else {
+ if ti.subCategory == SubCategoryEmbedded {
+ // Prefer user provided template.
+ return false
+ }
+ }
+ }
+
+ if w.distance < best.w.distance {
+ if w.w2 < best.w.w2 {
+ return false
+ }
+ if w.w3 < best.w.w3 {
+ return false
+ }
+ } else {
+ if w.w1 < best.w.w1 {
+ return false
+ }
+ }
+
+ if w.isEqualWeights(best.w) {
+ // Tie breakers.
+ if w.distance < best.w.distance {
+ return true
+ }
+
+ return ti.PathInfo.Path() < best.templ.PathInfo.Path()
+ }
+
+ return true
+}
+
+func (best *bestMatch) updateValues(w weight, key string, k TemplateDescriptor, vv *TemplInfo) {
+ best.w = w
+ best.templ = vv
+ best.desc = k
+ best.key = key
+}
+
+type byPath []*TemplInfo
+
+func (a byPath) Len() int { return len(a) }
+func (a byPath) Less(i, j int) bool {
+ return a[i].PathInfo.Path() < a[j].PathInfo.Path()
+}
+
+func (a byPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+
+type keyTemplateInfo struct {
+ Key string
+ Info *TemplInfo
+}
+
+type nodeKey struct {
+ c Category
+ d TemplateDescriptor
+}
+
+type processingState int
+
+// the parts of a template store that's set per site.
+type storeSite struct {
+ opts SiteOptions
+ execHelper *templateExecHelper
+ executer texttemplate.Executer
+}
+
+type weight struct {
+ w1 int
+ w2 int
+ w3 int
+ distance int
+}
+
+func isLayoutStandard(s string) bool {
+ switch s {
+ case layoutAll, layoutList, layoutSingle:
+ return true
+ default:
+ return false
+ }
+}
+
+func (w weight) isEqualWeights(other weight) bool {
+ return w.w1 == other.w1 && w.w2 == other.w2 && w.w3 == other.w3
+}
+
+func configureSiteStorage(opts SiteOptions, watching bool) *storeSite {
+ funcsv := make(map[string]reflect.Value)
+
+ for k, v := range opts.TemplateFuncs {
+ vv := reflect.ValueOf(v)
+ funcsv[k] = vv
+ }
+
+ // Duplicate Go's internal funcs here for faster lookups.
+ for k, v := range htmltemplate.GoFuncs {
+ if _, exists := funcsv[k]; !exists {
+ vv, ok := v.(reflect.Value)
+ if !ok {
+ vv = reflect.ValueOf(v)
+ }
+ funcsv[k] = vv
+ }
+ }
+
+ for k, v := range texttemplate.GoFuncs {
+ if _, exists := funcsv[k]; !exists {
+ funcsv[k] = v
+ }
+ }
+
+ s := &storeSite{
+ opts: opts,
+ execHelper: &templateExecHelper{
+ watching: watching,
+ funcs: funcsv,
+ site: reflect.ValueOf(opts.Site),
+ siteParams: reflect.ValueOf(opts.Site.Params()),
+ },
+ }
+
+ s.executer = texttemplate.NewExecuter(s.execHelper)
+
+ return s
+}
+
+func isBackupFile(path string) bool {
+ return path[len(path)-1] == '~'
+}
+
+func isDotFile(path string) bool {
+ return filepath.Base(path)[0] == '.'
+}
diff --git a/tpl/tplimpl/templatestore_integration_test.go b/tpl/tplimpl/templatestore_integration_test.go
new file mode 100644
index 000000000..9da959350
--- /dev/null
+++ b/tpl/tplimpl/templatestore_integration_test.go
@@ -0,0 +1,1550 @@
+package tplimpl_test
+
+import (
+ "context"
+ "io"
+ "strings"
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/resources/kinds"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
+)
+
+// Old as in before Hugo v0.146.0.
+func TestLayoutsOldSetup(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+[languages]
+[languages.en]
+title = "Title in English"
+weight = 1
+[languages.nn]
+title = "Tittel på nynorsk"
+weight = 2
+-- layouts/index.html --
+Home.
+{{ template "_internal/twitter_cards.html" . }}
+-- layouts/_default/single.html --
+Single.
+-- layouts/_default/single.nn.html --
+Single NN.
+-- layouts/_default/list.html --
+List HTML.
+-- layouts/docs/list-baseof.html --
+Docs Baseof List HTML.
+{{ block "main" . }}Docs Baseof List HTML main block.{{ end }}
+-- layouts/docs/list.section.html --
+{{ define "main" }}
+Docs List HTML.
+{{ end }}
+-- layouts/_default/list.json --
+List JSON.
+-- layouts/_default/list.rss.xml --
+List RSS.
+-- layouts/_default/list.nn.rss.xml --
+List NN RSS.
+-- layouts/_default/baseof.html --
+Base.
+-- layouts/partials/mypartial.html --
+Partial.
+-- layouts/shortcodes/myshortcode.html --
+Shortcode.
+-- content/docs/p1.md --
+---
+title: "P1"
+---
+
+ `
+
+ b := hugolib.Test(t, files)
+
+ // b.DebugPrint("", tplimpl.CategoryBaseof)
+
+ b.AssertFileContent("public/en/docs/index.html", "Docs Baseof List HTML.\n\nDocs List HTML.")
+}
+
+func TestLayoutsOldSetupBaseofPrefix(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/_default/layout1-baseof.html --
+Baseof layout1. {{ block "main" . }}{{ end }}
+-- layouts/_default/layout2-baseof.html --
+Baseof layout2. {{ block "main" . }}{{ end }}
+-- layouts/_default/layout1.html --
+{{ define "main" }}Layout1. {{ .Title }}{{ end }}
+-- layouts/_default/layout2.html --
+{{ define "main" }}Layout2. {{ .Title }}{{ end }}
+-- content/p1.md --
+---
+title: "P1"
+layout: "layout1"
+---
+-- content/p2.md --
+---
+title: "P2"
+layout: "layout2"
+---
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html", "Baseof layout1. Layout1. P1")
+ b.AssertFileContent("public/p2/index.html", "Baseof layout2. Layout2. P2")
+}
+
+func TestLayoutsOldSetupTaxonomyAndTerm(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+[taxonomies]
+cat = 'cats'
+dog = 'dogs'
+# Templates for term taxonomy, old setup.
+-- layouts/dogs/terms.html --
+Dogs Terms. Most specific taxonomy template.
+-- layouts/taxonomy/terms.html --
+Taxonomy Terms. Down the list.
+# Templates for term term, old setup.
+-- layouts/dogs/term.html --
+Dogs Term. Most specific term template.
+-- layouts/term/term.html --
+Term Term. Down the list.
+-- layouts/dogs/max/list.html --
+max: {{ .Title }}
+-- layouts/_default/list.html --
+Default list.
+-- layouts/_default/single.html --
+Default single.
+-- content/p1.md --
+---
+title: "P1"
+dogs: ["luna", "daisy", "max"]
+---
+
+`
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+ b.AssertLogContains("! WARN")
+
+ b.AssertFileContent("public/dogs/index.html", "Dogs Terms. Most specific taxonomy template.")
+ b.AssertFileContent("public/dogs/luna/index.html", "Dogs Term. Most specific term template.")
+ b.AssertFileContent("public/dogs/max/index.html", "max: Max") // layouts/dogs/max/list.html wins over layouts/term/term.html because of distance.
+}
+
+func TestLayoutsOldSetupCustomRSS(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "page"]
+[outputs]
+home = ["rss"]
+-- layouts/_default/list.rss.xml --
+List RSS.
+`
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.xml", "List RSS.")
+}
+
+var newSetupTestSites = `
+-- hugo.toml --
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+[languages]
+[languages.en]
+title = "Title in English"
+weight = 1
+[languages.nn]
+title = "Tittel på nynorsk"
+weight = 2
+[languages.fr]
+title = "Titre en français"
+weight = 3
+
+[outputs]
+home = ["html", "rss", "redir"]
+
+[outputFormats]
+[outputFormats.redir]
+mediatype = "text/plain"
+baseName = "_redirects"
+isPlainText = true
+-- layouts/404.html --
+{{ define "main" }}
+404.
+{{ end }}
+-- layouts/home.html --
+{{ define "main" }}
+Home: {{ .Title }}|{{ .Content }}|
+Inline Partial: {{ partial "my-inline-partial.html" . }}
+{{ end }}
+{{ define "hero" }}
+Home hero.
+{{ end }}
+{{ define "partials/my-inline-partial.html" }}
+{{ $value := 32 }}
+{{ return $value }}
+{{ end }}
+-- layouts/index.redir --
+Redir.
+-- layouts/single.html --
+{{ define "main" }}
+Single needs base.
+{{ end }}
+-- layouts/foo/bar/single.html --
+{{ define "main" }}
+Single sub path.
+{{ end }}
+-- layouts/_markup/render-codeblock.html --
+Render codeblock.
+-- layouts/_markup/render-blockquote.html --
+Render blockquote.
+-- layouts/_markup/render-codeblock-go.html --
+ Render codeblock go.
+-- layouts/_markup/render-link.html --
+Link: {{ .Destination | safeURL }}
+-- layouts/foo/baseof.html --
+Base sub path.{{ block "main" . }}{{ end }}
+-- layouts/foo/bar/baseof.page.html --
+Base sub path.{{ block "main" . }}{{ end }}
+-- layouts/list.html --
+{{ define "main" }}
+List needs base.
+{{ end }}
+-- layouts/section.html --
+Section.
+-- layouts/mysectionlayout.section.fr.amp.html --
+Section with layout.
+-- layouts/baseof.html --
+Base.{{ block "main" . }}{{ end }}
+Hero:{{ block "hero" . }}{{ end }}:
+{{ with (templates.Defer (dict "key" "global")) }}
+Defer Block.
+{{ end }}
+-- layouts/baseof.fr.html --
+Base fr.{{ block "main" . }}{{ end }}
+-- layouts/baseof.term.html --
+Base term.
+-- layouts/baseof.section.fr.amp.html --
+Base with identifiers.{{ block "main" . }}{{ end }}
+-- layouts/partials/mypartial.html --
+Partial. {{ partial "_inline/my-inline-partial-in-partial-with-no-ext" . }}
+{{ define "partials/_inline/my-inline-partial-in-partial-with-no-ext" }}
+Partial in partial.
+{{ end }}
+-- layouts/partials/returnfoo.html --
+{{ $v := "foo" }}
+{{ return $v }}
+-- layouts/shortcodes/myshortcode.html --
+Shortcode. {{ partial "mypartial.html" . }}|return:{{ partial "returnfoo.html" . }}|
+-- content/_index.md --
+---
+title: Home sweet home!
+---
+
+{{< myshortcode >}}
+
+> My blockquote.
+
+
+Markdown link: [Foo](/foo)
+-- content/p1.md --
+---
+title: "P1"
+---
+-- content/foo/bar/index.md --
+---
+title: "Foo Bar"
+---
+
+{{< myshortcode >}}
+
+-- content/single-list.md --
+---
+title: "Single List"
+layout: "list"
+---
+
+`
+
+func TestLayoutsType(t *testing.T) {
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+-- layouts/list.html --
+List.
+-- layouts/mysection/single.html --
+mysection/single|{{ .Title }}
+-- layouts/mytype/single.html --
+mytype/single|{{ .Title }}
+-- content/mysection/_index.md --
+-- content/mysection/mysubsection/_index.md --
+-- content/mysection/mysubsection/p1.md --
+---
+title: "P1"
+---
+-- content/mysection/mysubsection/p2.md --
+---
+title: "P2"
+type: "mytype"
+---
+
+`
+
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+ b.AssertLogContains("! WARN")
+
+ b.AssertFileContent("public/mysection/mysubsection/p1/index.html", "mysection/single|P1")
+ b.AssertFileContent("public/mysection/mysubsection/p2/index.html", "mytype/single|P2")
+}
+
+// New, as in from Hugo v0.146.0.
+func TestLayoutsNewSetup(t *testing.T) {
+ const numIterations = 1
+ for range numIterations {
+
+ b := hugolib.Test(t, newSetupTestSites, hugolib.TestOptWarn())
+
+ b.AssertLogContains("! WARN")
+
+ b.AssertFileContent("public/en/index.html",
+ "Base.\nHome: Home sweet home!|",
+ "|Shortcode.\n|",
+ "Markdown link: Link: /foo
",
+ "|return:foo|",
+ "Defer Block.",
+ "Home hero.",
+ "Render blockquote.",
+ )
+
+ b.AssertFileContent("public/en/p1/index.html", "Base.\nSingle needs base.\n\nHero::\n\nDefer Block.")
+ b.AssertFileContent("public/en/404.html", "404.")
+ b.AssertFileContent("public/nn/404.html", "404.")
+ b.AssertFileContent("public/fr/404.html", "404.")
+
+ }
+}
+
+func TestHomeRSSAndHTMLWithHTMLOnlyShortcode(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+[outputs]
+home = ["html", "rss"]
+-- layouts/home.html --
+Home: {{ .Title }}|{{ .Content }}|
+-- layouts/single.html --
+Single: {{ .Title }}|{{ .Content }}|
+-- layouts/shortcodes/myshortcode.html --
+Myshortcode: Count: {{ math.Counter }}|
+-- content/p1.md --
+---
+title: "P1"
+---
+
+{{< myshortcode >}}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html", "Single: P1|Myshortcode: Count: 1|")
+ b.AssertFileContent("public/index.xml", "Myshortcode: Count: 1")
+}
+
+func TestHomeRSSAndHTMLWithHTMLOnlyRenderHook(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+[outputs]
+home = ["html", "rss"]
+-- layouts/home.html --
+Home: {{ .Title }}|{{ .Content }}|
+-- layouts/single.html --
+Single: {{ .Title }}|{{ .Content }}|
+-- layouts/_markup/render-link.html --
+Render Link: {{ math.Counter }}|
+-- content/p1.md --
+---
+title: "P1"
+---
+
+Link: [Foo](/foo)
+`
+
+ for range 2 {
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.xml", "Link: Render Link: 1|")
+ b.AssertFileContent("public/p1/index.html", "Single: P1|Link: Render Link: 1|<")
+ }
+}
+
+func TestRenderCodeblockSpecificity(t *testing.T) {
+ files := `
+-- hugo.toml --
+-- layouts/_markup/render-codeblock.html --
+Render codeblock.|{{ .Inner }}|
+-- layouts/_markup/render-codeblock-go.html --
+Render codeblock go.|{{ .Inner }}|
+-- layouts/single.html --
+{{ .Title }}|{{ .Content }}|
+-- content/p1.md --
+---
+title: "P1"
+---
+
+§§§
+Basic
+§§§
+
+§§§ go
+Go
+§§§
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html", "P1|Render codeblock.|Basic|Render codeblock go.|Go|")
+}
+
+func TestPrintUnusedTemplates(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- config.toml --
+baseURL = 'http://example.com/'
+printUnusedTemplates=true
+-- content/p1.md --
+---
+title: "P1"
+---
+{{< usedshortcode >}}
+-- layouts/baseof.html --
+{{ block "main" . }}{{ end }}
+-- layouts/baseof.json --
+{{ block "main" . }}{{ end }}
+-- layouts/index.html --
+{{ define "main" }}FOO{{ end }}
+-- layouts/_default/single.json --
+-- layouts/_default/single.html --
+{{ define "main" }}MAIN /_default/single.html{{ end }}
+-- layouts/post/single.html --
+{{ define "main" }}MAIN{{ end }}
+-- layouts/_partials/usedpartial.html --
+-- layouts/_partials/unusedpartial.html --
+-- layouts/_shortcodes/usedshortcode.html --
+{{ partial "usedpartial.html" }}
+-- layouts/shortcodes/unusedshortcode.html --
+
+ `
+
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ TxtarString: files,
+ NeedsOsFS: true,
+ },
+ )
+ b.Build()
+
+ b.AssertFileContent("public/p1/index.html", "MAIN /_default/single.html")
+
+ unused := b.H.GetTemplateStore().UnusedTemplates()
+ var names []string
+ for _, tmpl := range unused {
+ if fi := tmpl.Fi; fi != nil {
+ names = append(names, fi.Meta().PathInfo.PathNoLeadingSlash())
+ }
+ }
+
+ b.Assert(names, qt.DeepEquals, []string{"_partials/unusedpartial.html", "shortcodes/unusedshortcode.html", "baseof.json", "post/single.html", "_default/single.json"})
+ b.Assert(len(unused), qt.Equals, 5, qt.Commentf("%#v", names))
+}
+
+func TestCreateManyTemplateStores(t *testing.T) {
+ t.Parallel()
+ b := hugolib.Test(t, newSetupTestSites)
+ store := b.H.TemplateStore
+
+ for range 70 {
+ newStore, err := store.NewFromOpts()
+ b.Assert(err, qt.IsNil)
+ b.Assert(newStore, qt.Not(qt.IsNil))
+ }
+}
+
+func BenchmarkLookupPagesLayout(b *testing.B) {
+ files := `
+-- hugo.toml --
+-- layouts/single.html --
+{{ define "main" }}
+ Main.
+{{ end }}
+-- layouts/baseof.html --
+baseof: {{ block "main" . }}{{ end }}
+-- layouts/foo/bar/single.html --
+{{ define "main" }}
+ Main.
+{{ end }}
+
+`
+ bb := hugolib.Test(b, files)
+ store := bb.H.TemplateStore
+
+ b.ResetTimer()
+ b.Run("Single root", func(b *testing.B) {
+ q := tplimpl.TemplateQuery{
+ Path: "/baz",
+ Category: tplimpl.CategoryLayout,
+ Desc: tplimpl.TemplateDescriptor{Kind: kinds.KindPage, LayoutFromTemplate: "single", OutputFormat: "html"},
+ }
+ for i := 0; i < b.N; i++ {
+ store.LookupPagesLayout(q)
+ }
+ })
+
+ b.Run("Single sub folder", func(b *testing.B) {
+ q := tplimpl.TemplateQuery{
+ Path: "/foo/bar",
+ Category: tplimpl.CategoryLayout,
+ Desc: tplimpl.TemplateDescriptor{Kind: kinds.KindPage, LayoutFromTemplate: "single", OutputFormat: "html"},
+ }
+ for i := 0; i < b.N; i++ {
+ store.LookupPagesLayout(q)
+ }
+ })
+}
+
+func BenchmarkNewTemplateStore(b *testing.B) {
+ bb := hugolib.Test(b, newSetupTestSites)
+ store := bb.H.TemplateStore
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ newStore, err := store.NewFromOpts()
+ if err != nil {
+ b.Fatal(err)
+ }
+ if newStore == nil {
+ b.Fatal("newStore is nil")
+ }
+ }
+}
+
+func TestLayoutsLookupVariants(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+[outputs]
+home = ["html", "rss"]
+page = ["html", "rss", "amp"]
+section = ["html", "rss"]
+
+[languages]
+[languages.en]
+title = "Title in English"
+weight = 1
+[languages.nn]
+title = "Tittel på nynorsk"
+weight = 2
+-- layouts/list.xml --
+layouts/list.xml
+-- layouts/_shortcodes/myshortcode.html --
+layouts/shortcodes/myshortcode.html
+-- layouts/foo/bar/_shortcodes/myshortcode.html --
+layouts/foo/bar/_shortcodes/myshortcode.html
+-- layouts/_markup/render-codeblock.html --
+layouts/_markup/render-codeblock.html|{{ .Type }}|
+-- layouts/_markup/render-codeblock-go.html --
+layouts/_markup/render-codeblock-go.html|{{ .Type }}|
+-- layouts/single.xml --
+layouts/single.xml
+-- layouts/single.rss.xml --
+layouts/single.rss.xml
+-- layouts/single.nn.rss.xml --
+layouts/single.nn.rss.xml
+-- layouts/list.html --
+layouts/list.html
+-- layouts/single.html --
+layouts/single.html
+{{ .Content }}
+-- layouts/mylayout.html --
+layouts/mylayout.html
+-- layouts/mylayout.nn.html --
+layouts/mylayout.nn.html
+-- layouts/foo/single.rss.xml --
+layouts/foo/single.rss.xml
+-- layouts/foo/single.amp.html --
+layouts/foo/single.amp.html
+-- layouts/foo/bar/page.html --
+layouts/foo/bar/page.html
+-- layouts/foo/bar/baz/single.html --
+layouts/foo/bar/baz/single.html
+{{ .Content }}
+-- layouts/qux/mylayout.html --
+layouts/qux/mylayout.html
+-- layouts/qux/single.xml --
+layouts/qux/single.xml
+-- layouts/qux/mylayout.section.html --
+layouts/qux/mylayout.section.html
+-- content/p.md --
+---
+---
+§§§
+code
+§§§
+
+§§§ go
+code
+§§§
+
+{{< myshortcode >}}
+-- content/foo/p.md --
+-- content/foo/p.nn.md --
+-- content/foo/bar/p.md --
+-- content/foo/bar/withmylayout.md --
+---
+layout: mylayout
+---
+-- content/foo/bar/_index.md --
+-- content/foo/bar/baz/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/qux/p.md --
+-- content/qux/_index.md --
+---
+layout: mylayout
+---
+-- content/qux/quux/p.md --
+-- content/qux/quux/withmylayout.md --
+---
+layout: mylayout
+---
+-- content/qux/quux/withmylayout.nn.md --
+---
+layout: mylayout
+---
+
+
+`
+
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+ b.AssertLogContains("! WARN")
+
+ // Single pages.
+ // output format: html.
+ b.AssertFileContent("public/en/p/index.html", "layouts/single.html",
+ "layouts/_markup/render-codeblock.html|",
+ "layouts/_markup/render-codeblock-go.html|go|",
+ "layouts/shortcodes/myshortcode.html",
+ )
+ b.AssertFileContent("public/en/foo/p/index.html", "layouts/single.html")
+ b.AssertFileContent("public/en/foo/bar/p/index.html", "layouts/foo/bar/page.html")
+ b.AssertFileContent("public/en/foo/bar/withmylayout/index.html", "layouts/mylayout.html")
+ b.AssertFileContent("public/en/foo/bar/baz/p/index.html", "layouts/foo/bar/baz/single.html", "layouts/foo/bar/_shortcodes/myshortcode.html")
+ b.AssertFileContent("public/en/qux/quux/withmylayout/index.html", "layouts/qux/mylayout.html")
+ // output format: amp.
+ b.AssertFileContent("public/en/amp/p/index.html", "layouts/single.html")
+ b.AssertFileContent("public/en/amp/foo/p/index.html", "layouts/foo/single.amp.html")
+ // output format: rss.
+ b.AssertFileContent("public/en/p/index.xml", "layouts/single.rss.xml")
+ b.AssertFileContent("public/en/foo/p/index.xml", "layouts/foo/single.rss.xml")
+ b.AssertFileContent("public/nn/foo/p/index.xml", "layouts/single.nn.rss.xml")
+
+ // Note: There is qux/single.xml that's closer, but the one in the root is used becaulse of the output format match.
+ b.AssertFileContent("public/en/qux/p/index.xml", "layouts/single.rss.xml")
+
+ // Note.
+ b.AssertFileContent("public/nn/qux/quux/withmylayout/index.html", "layouts/mylayout.nn.html")
+
+ // Section pages.
+ // output format: html.
+ b.AssertFileContent("public/en/foo/index.html", "layouts/list.html")
+ b.AssertFileContent("public/en/qux/index.html", "layouts/qux/mylayout.section.html")
+ // output format: rss.
+ b.AssertFileContent("public/en/foo/index.xml", "layouts/list.xml")
+}
+
+func TestLookupOrderIssue13636(t *testing.T) {
+ t.Parallel()
+
+ filesTemplate := `
+-- hugo.toml --
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+[languages]
+[languages.en]
+weight = 1
+[languages.nn]
+weight = 2
+-- content/s1/p1.en.md --
+---
+outputs: ["html", "amp", "json"]
+---
+-- content/s1/p1.nn.md --
+---
+outputs: ["html", "amp", "json"]
+---
+-- layouts/L1 --
+L1
+-- layouts/L2 --
+L2
+-- layouts/L3 --
+L3
+
+`
+
+ tests := []struct {
+ Lang string
+ L1 string
+ L2 string
+ L3 string
+ ExpectHTML string
+ ExpectAmp string
+ ExpectJSON string
+ }{
+ {"en", "all.en.html", "all.html", "single.html", "single.html", "single.html", ""},
+ {"en", "all.amp.html", "all.html", "page.html", "page.html", "all.amp.html", ""},
+ {"en", "all.amp.html", "all.html", "list.html", "all.html", "all.amp.html", ""},
+ {"en", "all.en.html", "all.json", "single.html", "single.html", "single.html", "all.json"},
+ {"en", "all.en.html", "single.json", "single.html", "single.html", "single.html", "single.json"},
+ {"en", "all.en.html", "all.html", "list.html", "all.en.html", "all.en.html", ""},
+ {"en", "list.en.html", "list.html", "list.en.html", "", "", ""},
+ {"nn", "all.en.html", "all.html", "single.html", "single.html", "single.html", ""},
+ {"nn", "all.en.html", "all.nn.html", "single.html", "single.html", "single.html", ""},
+ {"nn", "all.en.html", "all.nn.html", "single.nn.html", "single.nn.html", "single.nn.html", ""},
+ {"nn", "single.json", "single.nn.json", "all.json", "", "", "single.nn.json"},
+ {"nn", "single.json", "single.en.json", "all.nn.json", "", "", "single.json"},
+ }
+
+ for i, test := range tests {
+ if i != 8 {
+ // continue
+ }
+ files := strings.ReplaceAll(filesTemplate, "L1", test.L1)
+ files = strings.ReplaceAll(files, "L2", test.L2)
+ files = strings.ReplaceAll(files, "L3", test.L3)
+ t.Logf("Test %d: %s %s %s %s", i, test.Lang, test.L1, test.L2, test.L3)
+
+ for range 3 {
+ b := hugolib.Test(t, files)
+ b.Assert(len(b.H.Sites), qt.Equals, 2)
+
+ var (
+ pubhHTML = "public/LANG/s1/p1/index.html"
+ pubhAmp = "public/LANG/amp/s1/p1/index.html"
+ pubhJSON = "public/LANG/s1/p1/index.json"
+ )
+
+ pubhHTML = strings.ReplaceAll(pubhHTML, "LANG", test.Lang)
+ pubhAmp = strings.ReplaceAll(pubhAmp, "LANG", test.Lang)
+ pubhJSON = strings.ReplaceAll(pubhJSON, "LANG", test.Lang)
+
+ if test.ExpectHTML != "" {
+ b.AssertFileContent(pubhHTML, test.ExpectHTML)
+ } else {
+ b.AssertFileExists(pubhHTML, false)
+ }
+
+ if test.ExpectAmp != "" {
+ b.AssertFileContent(pubhAmp, test.ExpectAmp)
+ } else {
+ b.AssertFileExists(pubhAmp, false)
+ }
+
+ if test.ExpectJSON != "" {
+ b.AssertFileContent(pubhJSON, test.ExpectJSON)
+ } else {
+ b.AssertFileExists(pubhJSON, false)
+ }
+ }
+ }
+}
+
+func TestLookupShortcodeDepth(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/_shortcodes/myshortcode.html --
+layouts/_shortcodes/myshortcode.html
+-- layouts/foo/_shortcodes/myshortcode.html --
+layouts/foo/_shortcodes/myshortcode.html
+-- layouts/single.html --
+{{ .Content }}|
+-- content/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/foo/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/foo/bar/p.md --
+---
+---
+{{< myshortcode >}}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p/index.html", "layouts/_shortcodes/myshortcode.html")
+ b.AssertFileContent("public/foo/p/index.html", "layouts/foo/_shortcodes/myshortcode.html")
+ b.AssertFileContent("public/foo/bar/p/index.html", "layouts/foo/_shortcodes/myshortcode.html")
+}
+
+func TestLookupShortcodeLayout(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/_shortcodes/myshortcode.single.html --
+layouts/_shortcodes/myshortcode.single.html
+-- layouts/_shortcodes/myshortcode.list.html --
+layouts/_shortcodes/myshortcode.list.html
+-- layouts/single.html --
+{{ .Content }}|
+-- layouts/list.html --
+{{ .Content }}|
+-- content/_index.md --
+---
+---
+{{< myshortcode >}}
+-- content/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/foo/p.md --
+---
+---
+{{< myshortcode >}}
+-- content/foo/bar/p.md --
+---
+---
+{{< myshortcode >}}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p/index.html", "layouts/_shortcodes/myshortcode.single.html")
+ b.AssertFileContent("public/index.html", "layouts/_shortcodes/myshortcode.list.html")
+}
+
+func TestLayoutAll(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/single.html --
+Single.
+-- layouts/all.html --
+All.
+-- content/p1.md --
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html", "Single.")
+ b.AssertFileContent("public/index.html", "All.")
+}
+
+func TestLayoutAllNested(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['rss','sitemap','taxonomy','term']
+-- content/s1/p1.md --
+---
+title: p1
+---
+-- content/s2/p2.md --
+---
+title: p2
+---
+-- layouts/single.html --
+layouts/single.html
+-- layouts/list.html --
+layouts/list.html
+-- layouts/s1/all.html --
+layouts/s1/all.html
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "layouts/list.html")
+ b.AssertFileContent("public/s1/index.html", "layouts/s1/all.html")
+ b.AssertFileContent("public/s1/p1/index.html", "layouts/s1/all.html")
+ b.AssertFileContent("public/s2/index.html", "layouts/list.html")
+ b.AssertFileContent("public/s2/p2/index.html", "layouts/single.html")
+}
+
+func TestPartialHTML(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/all.html --
+
+
+{{ partial "css.html" .}}
+
+
+-- layouts/partials/css.html --
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", " ")
+}
+
+func TestPartialPlainTextInHTML(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/all.html --
+
+
+{{ partial "mypartial.txt" . }}
+
+
+-- layouts/partials/mypartial.txt --
+My partial
.
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "My <div>partial</div>.")
+}
+
+// Issue #13593.
+func TestGoatAndNoGoat(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- content/_index.md --
+---
+title: "Home"
+---
+
+
+§§§
+printf "Hello, world!"
+§§§
+
+
+§§§ goat
+.---. .-. .-. .-. .---.
+| A +--->| 1 |<--->| 2 |<--->| 3 |<---+ B |
+'---' '-' '+' '+' '---'
+§§§
+
+
+
+-- layouts/all.html --
+{{ .Content }}
+
+`
+
+ b := hugolib.Test(t, files)
+
+ // Basic code block.
+ b.AssertFileContent("public/index.html", "printf "Hello, world!"\n
")
+
+ // Goat code block.
+ b.AssertFileContent("public/index.html", "Menlo,Lucida")
+}
+
+// Issue #13595.
+func TestGoatAndNoGoatCustomTemplate(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- content/_index.md --
+---
+title: "Home"
+---
+
+§§§
+printf "Hello, world!"
+§§§
+
+§§§ goat
+.---. .-. .-. .-. .---.
+| A +--->| 1 |<--->| 2 |<--->| 3 |<---+ B |
+'---' '-' '+' '+' '---'
+§§§
+
+
+
+-- layouts/_markup/render-codeblock.html --
+_markup/render-codeblock.html
+-- layouts/all.html --
+{{ .Content }}
+
+`
+
+ b := hugolib.Test(t, files)
+
+ // Basic code block.
+ b.AssertFileContent("public/index.html", "_markup/render-codeblock.html")
+
+ // Goat code block.
+ b.AssertFileContent("public/index.html", "Menlo,Lucida")
+}
+
+func TestGoatcustom(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- content/_index.md --
+---
+title: "Home"
+---
+
+§§§
+printf "Hello, world!"
+§§§
+
+§§§ goat
+.---. .-. .-. .-. .---.
+| A +--->| 1 |<--->| 2 |<--->| 3 |<---+ B |
+'---' '-' '+' '+' '---'
+§§§
+
+
+
+-- layouts/_markup/render-codeblock.html --
+_markup/render-codeblock.html
+-- layouts/_markup/render-codeblock-goat.html --
+_markup/render-codeblock-goat.html
+-- layouts/all.html --
+{{ .Content }}
+
+`
+
+ b := hugolib.Test(t, files)
+
+ // Basic code block.
+ b.AssertFileContent("public/index.html", "_markup/render-codeblock.html")
+
+ // Custom Goat code block.
+ b.AssertFileContent("public/index.html", "_markup/render-codeblock.html_markup/render-codeblock-goat.html")
+}
+
+func TestLookupCodeblockIssue13651(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/all.html --
+{{ .Content }}|
+-- layouts/_markup/render-codeblock-foo.html --
+render-codeblock-foo.html
+-- content/_index.md --
+---
+---
+
+§§§
+printf "Hello, world!"
+§§§
+
+§§§ foo
+printf "Hello, world again!"
+§§§
+`
+
+ b := hugolib.Test(t, files)
+
+ content := b.FileContent("public/index.html")
+ fooCount := strings.Count(content, "render-codeblock-foo.html")
+ b.Assert(fooCount, qt.Equals, 1)
+}
+
+// Issue #13515
+func TestPrintPathWarningOnDotRemoval(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+printPathWarnings = true
+-- content/v0.124.0.md --
+-- content/v0.123.0.md --
+-- layouts/all.html --
+All.
+-- layouts/_default/single.html --
+{{ .Title }}|
+`
+
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+ b.AssertLogContains("! Duplicate content path")
+}
+
+// Issue #13577.
+func TestPrintPathWarningOnInvalidMarkupFilename(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/all.html --
+All.
+-- layouts/_markup/sitemap.xml --
+`
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+
+ b.AssertLogContains("unrecognized render hook")
+}
+
+func TestLayoutNotFound(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/single.html --
+Single.
+`
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+ b.AssertLogContains("WARN found no layout file for \"html\" for kind \"home\"")
+}
+
+func TestLayoutOverrideThemeWhenThemeOnOldFormatIssue13715(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+theme = "mytheme"
+-- layouts/list.html --
+ layouts/list.html
+-- themes/mytheme/layouts/_default/list.html --
+mytheme/layouts/_default/list.html
+
+`
+
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.html", "layouts/list.html")
+}
+
+func BenchmarkExecuteWithContext(b *testing.B) {
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "home"]
+-- layouts/all.html --
+{{ .Title }}|
+{{ partial "p1.html" . }}
+-- layouts/_partials/p1.html --
+ p1.
+{{ partial "p2.html" . }}
+{{ partial "p2.html" . }}
+{{ partial "p3.html" . }}
+{{ partial "p2.html" . }}
+{{ partial "p2.html" . }}
+{{ partial "p2.html" . }}
+{{ partial "p3.html" . }}
+-- layouts/_partials/p2.html --
+{{ partial "p3.html" . }}
+-- layouts/_partials/p3.html --
+p3
+-- content/p1.md --
+`
+
+ bb := hugolib.Test(b, files)
+
+ store := bb.H.TemplateStore
+
+ ti := store.LookupByPath("/all.html")
+ bb.Assert(ti, qt.Not(qt.IsNil))
+ p := bb.H.Sites[0].RegularPages()[0]
+ bb.Assert(p, qt.Not(qt.IsNil))
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ err := store.ExecuteWithContext(context.Background(), ti, io.Discard, p)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func BenchmarkLookupPartial(b *testing.B) {
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "home"]
+-- layouts/all.html --
+{{ .Title }}|
+-- layouts/_partials/p1.html --
+-- layouts/_partials/p2.html --
+-- layouts/_partials/p2.json --
+-- layouts/_partials/p3.html --
+`
+ bb := hugolib.Test(b, files)
+
+ store := bb.H.TemplateStore
+
+ for i := 0; i < b.N; i++ {
+ fi := store.LookupPartial("p3.html")
+ if fi == nil {
+ b.Fatal("not found")
+ }
+ }
+}
+
+// Implemented by pageOutput.
+type getDescriptorProvider interface {
+ GetInternalTemplateBasePathAndDescriptor() (string, tplimpl.TemplateDescriptor)
+}
+
+func BenchmarkLookupShortcode(b *testing.B) {
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "home"]
+-- content/toplevelpage.md --
+-- content/a/b/c/nested.md --
+-- layouts/all.html --
+{{ .Title }}|
+-- layouts/_shortcodes/s.html --
+s1.
+-- layouts/_shortcodes/a/b/s.html --
+s2.
+
+`
+ bb := hugolib.Test(b, files)
+ store := bb.H.TemplateStore
+
+ runOne := func(p page.Page) {
+ pth, desc := p.(getDescriptorProvider).GetInternalTemplateBasePathAndDescriptor()
+ q := tplimpl.TemplateQuery{
+ Path: pth,
+ Name: "s",
+ Category: tplimpl.CategoryShortcode,
+ Desc: desc,
+ }
+ v, err := store.LookupShortcode(q)
+ if v == nil || err != nil {
+ b.Fatal("not found")
+ }
+ }
+
+ b.Run("toplevelpage", func(b *testing.B) {
+ toplevelpage, _ := bb.H.Sites[0].GetPage("/toplevelpage")
+ for i := 0; i < b.N; i++ {
+ runOne(toplevelpage)
+ }
+ })
+
+ b.Run("nestedpage", func(b *testing.B) {
+ toplevelpage, _ := bb.H.Sites[0].GetPage("/a/b/c/nested")
+ for i := 0; i < b.N; i++ {
+ runOne(toplevelpage)
+ }
+ })
+}
+
+func TestStandardLayoutInFrontMatter13588(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['home','page','rss','sitemap','taxonomy','term']
+-- content/s1/_index.md --
+---
+title: s1
+---
+-- content/s2/_index.md --
+---
+title: s2
+layout: list
+---
+-- content/s3/_index.md --
+---
+title: s3
+layout: single
+---
+-- layouts/list.html --
+list.html
+-- layouts/section.html --
+section.html
+-- layouts/single.html --
+single.html
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/s1/index.html", "section.html")
+ b.AssertFileContent("public/s2/index.html", "list.html") // fail
+ b.AssertFileContent("public/s3/index.html", "single.html") // fail
+}
+
+func TestIssue13605(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['home','rss','section','sitemap','taxonomy','term']
+-- content/s1/p1.md --
+---
+title: p1
+---
+{{< sc >}}
+-- layouts/s1/_shortcodes/sc.html --
+layouts/s1/_shortcodes/sc.html
+-- layouts/single.html --
+{{ .Content }}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/s1/p1/index.html", "layouts/s1/_shortcodes/sc.html")
+}
+
+func TestSkipDotFiles(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/all.html --
+All.
+-- layouts/.DS_Store --
+{{ foo }}
+`
+
+ // Just make sure it doesn't fail.
+ hugolib.Test(t, files)
+}
+
+func TestPartialsLangIssue13612(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','section','sitemap','taxonomy','term']
+
+defaultContentLanguage = 'ru'
+defaultContentLanguageInSubdir = true
+
+[languages.ru]
+weight = 1
+
+[languages.en]
+weight = 2
+
+[outputs]
+home = ['html','rss']
+
+-- layouts/_partials/comment.en.html --
+layouts/_partials/comment.en.html
+-- layouts/_partials/comment.en.xml --
+layouts/_partials/comment.en.xml
+-- layouts/_partials/comment.ru.html --
+layouts/_partials/comment.ru.html
+-- layouts/_partials/comment.ru.xml --
+layouts/_partials/comment.ru.xml
+-- layouts/home.html --
+{{ partial (print "comment." (default "ru" .Lang) ".html") . }}
+-- layouts/home.rss.xml --
+{{ partial (print "comment." (default "ru" .Lang) ".xml") . }}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/en/index.html", "layouts/_partials/comment.en.html")
+ b.AssertFileContent("public/en/index.xml", "layouts/_partials/comment.en.xml") // fail
+ b.AssertFileContent("public/ru/index.html", "layouts/_partials/comment.ru.html") // fail
+ b.AssertFileContent("public/ru/index.xml", "layouts/_partials/comment.ru.xml") // fail
+}
+
+func TestLayoutIssue13628(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['home','rss','sitemap','taxonomy','term']
+-- content/p1.md --
+---
+title: p1
+layout: foo
+---
+-- layouts/single.html --
+layouts/single.html
+-- layouts/list.html --
+layouts/list.html
+`
+
+ for range 5 {
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html", "layouts/single.html")
+ }
+}
+
+func TestTemplateLoop(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/_partials/p.html --
+p: {{ partial "p.html" . }}
+-- layouts/all.html --
+{{ partial "p.html" . }}
+
+`
+ b, err := hugolib.TestE(t, files)
+ b.Assert(err, qt.IsNotNil)
+ b.Assert(err.Error(), qt.Contains, "error calling partial: maximum template call stack size exceeded")
+}
+
+func TestIssue13630(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['rss','sitemap']
+-- content/p1.md --
+---
+title: p1
+layout: foo
+---
+-- layouts/list.html --
+layouts/list.html
+-- layouts/taxononmy.html.html --
+layouts/taxononmy.html.html
+`
+
+ var b *hugolib.IntegrationTestBuilder
+
+ for range 3 {
+ b = hugolib.Test(t, files)
+ b.AssertFileExists("public/p1/index.html", false)
+ }
+}
+
+func TestTemplateLoopBlogVsBlogrollIssue13672(t *testing.T) {
+ t.Parallel()
+ files := `
+-- hugo.toml --
+-- layouts/blog/_shortcodes/myshortcode.html --
+layouts/blog/_shortcodes/myshortcode.html
+-- layouts/blog/baseof.html --
+blog/baseof.html {{ block "main" . }}{{ end }}
+-- layouts/blog/all.html --
+{{ define "main" }}blog/all.html|{{ .Content}}{{ end }}
+-- layouts/blogroll/_shortcodes/myshortcode.html --
+layouts/blogroll/myshortcode.html
+-- layouts/blogroll/baseof.html --
+{{ block "main" . }}blogroll/baseof.html{{ end }}
+-- layouts/blogroll/all.html --
+{{ define "main" }}blogroll/all.html|{{ .Content}}{{ end }}
+-- content/blog/p1.md --
+---
+title: p1
+---
+{{< myshortcode >}}
+-- content/blogroll/p1.md --
+---
+title: p1
+---
+{{< myshortcode >}}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/blog/p1/index.html", "blog/baseof.html blog/all.html|layouts/blog/_shortcodes/myshortcode.html")
+ b.AssertFileContent("public/blogroll/p1/index.html", "blogroll/all.html|layouts/blogroll/myshortcode.html")
+}
+
+// See issue #13668.
+func TestPartialPlainTextVsHTML(t *testing.T) {
+ t.Parallel()
+
+ /*
+ Note that in the below, there's no output format named txt,
+ so the isPlainText is fetched from the only output format with that extension.
+ */
+ files := `
+-- hugo.toml --
+-- layouts/_partials/myhtml.html --
+myhtml
+-- layouts/_partials/mytext.txt --
+mytext
+-- layouts/all.html --
+myhtml: {{ partial "myhtml.html" . }}
+mytext: {{ partial "mytext.txt" . }}
+mytexts|safeHTML: {{ partial "mytext.txt" . | safeHTML }}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html",
+ "myhtml: myhtml
",
+ "mytext: <div>mytext</div>",
+ "mytexts|safeHTML: mytext
",
+ )
+}
+
+func TestIssue13351(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+[outputs]
+home = ['html','json']
+[outputFormats.html]
+weight = 1
+[outputFormats.json]
+weight = 2
+-- content/_index.md --
+---
+title: home
+---
+a|b
+:--|:--
+1|2
+-- layouts/index.html --
+{{ .Content }}
+-- layouts/index.json --
+{{ .Content }}
+`
+
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.html", "")
+ b.AssertFileContent("public/index.json", "")
+
+ f := strings.ReplaceAll(files, "weight = 1", "weight = 0")
+ b = hugolib.Test(t, f)
+ b.AssertFileContent("public/index.html", "")
+ b.AssertFileContent("public/index.json", "")
+
+ f = strings.ReplaceAll(files, "weight = 1", "")
+ b = hugolib.Test(t, f)
+ b.AssertFileContent("public/index.html", "")
+ b.AssertFileContent("public/index.json", "")
+}
diff --git a/tpl/tplimpl/template_ast_transformers.go b/tpl/tplimpl/templatetransform.go
similarity index 61%
rename from tpl/tplimpl/template_ast_transformers.go
rename to tpl/tplimpl/templatetransform.go
index 92558a903..eca9fdad1 100644
--- a/tpl/tplimpl/template_ast_transformers.go
+++ b/tpl/tplimpl/templatetransform.go
@@ -1,44 +1,27 @@
-// Copyright 2016 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
package tplimpl
import (
"errors"
"fmt"
+ "slices"
+ "strings"
+
+ "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
- "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
-
+ "github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/tpl"
"github.com/mitchellh/mapstructure"
)
-type templateType int
-
-const (
- templateUndefined templateType = iota
- templateShortcode
- templatePartial
-)
-
-type templateContext struct {
+type templateTransformContext struct {
visited map[string]bool
templateNotFound map[string]bool
- lookupFn func(name string) *templateState
+ deferNodes map[string]*parse.ListNode
+ lookupFn func(name string, in *TemplInfo) *TemplInfo
// The last error encountered.
err error
@@ -46,18 +29,18 @@ type templateContext struct {
// Set when we're done checking for config header.
configChecked bool
- t *templateState
+ t *TemplInfo
// Store away the return node in partials.
returnNode *parse.CommandNode
}
-func (c templateContext) getIfNotVisited(name string) *templateState {
+func (c templateTransformContext) getIfNotVisited(name string) *TemplInfo {
if c.visited[name] {
return nil
}
c.visited[name] = true
- templ := c.lookupFn(name)
+ templ := c.lookupFn(name, c.t)
if templ == nil {
// This may be a inline template defined outside of this file
// and not yet parsed. Unusual, but it happens.
@@ -68,34 +51,39 @@ func (c templateContext) getIfNotVisited(name string) *templateState {
return templ
}
-func newTemplateContext(
- t *templateState,
- lookupFn func(name string) *templateState,
-) *templateContext {
- return &templateContext{
+func newTemplateTransformContext(
+ t *TemplInfo,
+ lookupFn func(name string, in *TemplInfo) *TemplInfo,
+) *templateTransformContext {
+ return &templateTransformContext{
t: t,
lookupFn: lookupFn,
visited: make(map[string]bool),
templateNotFound: make(map[string]bool),
+ deferNodes: make(map[string]*parse.ListNode),
}
}
func applyTemplateTransformers(
- t *templateState,
- lookupFn func(name string) *templateState,
-) (*templateContext, error) {
+ t *TemplInfo,
+ lookupFn func(name string, in *TemplInfo) *TemplInfo,
+) (*templateTransformContext, error) {
if t == nil {
return nil, errors.New("expected template, but none provided")
}
- c := newTemplateContext(t, lookupFn)
+ c := newTemplateTransformContext(t, lookupFn)
+ c.t.ParseInfo = defaultParseInfo
tree := getParseTree(t.Template)
+ if tree == nil {
+ panic(fmt.Errorf("template %s not parsed", t))
+ }
_, err := c.applyTransformations(tree.Root)
if err == nil && c.returnNode != nil {
// This is a partial with a return statement.
- c.t.parseInfo.HasReturn = true
+ c.t.ParseInfo.HasReturn = true
tree.Root = c.wrapInPartialReturnWrapper(tree.Root)
}
@@ -103,7 +91,6 @@ func applyTemplateTransformers(
}
func getParseTree(templ tpl.Template) *parse.Tree {
- templ = unwrap(templ)
if text, ok := templ.(*texttemplate.Template); ok {
return text.Tree
}
@@ -116,9 +103,14 @@ const (
// "range" over a one-element slice so we can shift dot to the
// partial's argument, Arg, while allowing Arg to be falsy.
partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ range (slice .Arg) }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}`
+
+ doDeferTempl = `{{ doDefer ("PLACEHOLDER1") ("PLACEHOLDER2") }}`
)
-var partialReturnWrapper *parse.ListNode
+var (
+ partialReturnWrapper *parse.ListNode
+ doDefer *parse.ListNode
+)
func init() {
templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl)
@@ -126,11 +118,17 @@ func init() {
panic(err)
}
partialReturnWrapper = templ.Tree.Root
+
+ templ, err = texttemplate.New("").Funcs(texttemplate.FuncMap{"doDefer": func(string, string) string { return "" }}).Parse(doDeferTempl)
+ if err != nil {
+ panic(err)
+ }
+ doDefer = templ.Tree.Root
}
// wrapInPartialReturnWrapper copies and modifies the parsed nodes of a
// predefined partial return wrapper to insert those of a user-defined partial.
-func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
+func (c *templateTransformContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
wrapper := partialReturnWrapper.CopyList()
rangeNode := wrapper.Nodes[2].(*parse.RangeNode)
retn := rangeNode.List.Nodes[0]
@@ -147,7 +145,7 @@ func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.L
// applyTransformations do 2 things:
// 1) Parses partial return statement.
// 2) Tracks template (partial) dependencies and some other info.
-func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
+func (c *templateTransformContext) applyTransformations(n parse.Node) (bool, error) {
switch x := n.(type) {
case *parse.ListNode:
if x != nil {
@@ -158,6 +156,7 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
case *parse.IfNode:
c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
case *parse.WithNode:
+ c.handleDefer(x)
c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
case *parse.RangeNode:
c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
@@ -171,11 +170,14 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
for i, cmd := range x.Cmds {
keep, _ := c.applyTransformations(cmd)
if !keep {
- x.Cmds = append(x.Cmds[:i], x.Cmds[i+1:]...)
+ x.Cmds = slices.Delete(x.Cmds, i, i+1)
}
}
case *parse.CommandNode:
+ if x == nil {
+ return true, nil
+ }
c.collectInner(x)
keep := c.collectReturnNode(x)
@@ -191,19 +193,71 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
return true, c.err
}
-func (c *templateContext) applyTransformationsToNodes(nodes ...parse.Node) {
+func (c *templateTransformContext) handleDefer(withNode *parse.WithNode) {
+ if len(withNode.Pipe.Cmds) != 1 {
+ return
+ }
+ cmd := withNode.Pipe.Cmds[0]
+ if len(cmd.Args) != 1 {
+ return
+ }
+ idArg := cmd.Args[0]
+
+ p, ok := idArg.(*parse.PipeNode)
+ if !ok {
+ return
+ }
+
+ if len(p.Cmds) != 1 {
+ return
+ }
+
+ cmd = p.Cmds[0]
+
+ if len(cmd.Args) != 2 {
+ return
+ }
+
+ idArg = cmd.Args[0]
+
+ id, ok := idArg.(*parse.ChainNode)
+ if !ok || len(id.Field) != 1 || id.Field[0] != "Defer" {
+ return
+ }
+ if id2, ok := id.Node.(*parse.IdentifierNode); !ok || id2.Ident != "templates" {
+ return
+ }
+
+ deferArg := cmd.Args[1]
+ cmd.Args = []parse.Node{idArg}
+
+ l := doDefer.CopyList()
+ n := l.Nodes[0].(*parse.ActionNode)
+
+ inner := withNode.List.CopyList()
+ s := inner.String()
+ if strings.Contains(s, "resources.PostProcess") {
+ c.err = errors.New("resources.PostProcess cannot be used in a deferred template")
+ return
+ }
+ innerHash := hashing.XxHashFromStringHexEncoded(s)
+ deferredID := tpl.HugoDeferredTemplatePrefix + innerHash
+
+ c.deferNodes[deferredID] = inner
+ withNode.List = l
+
+ n.Pipe.Cmds[0].Args[1].(*parse.PipeNode).Cmds[0].Args[0].(*parse.StringNode).Text = deferredID
+ n.Pipe.Cmds[0].Args[2] = deferArg
+}
+
+func (c *templateTransformContext) applyTransformationsToNodes(nodes ...parse.Node) {
for _, node := range nodes {
c.applyTransformations(node)
}
}
-func (c *templateContext) hasIdent(idents []string, ident string) bool {
- for _, id := range idents {
- if id == ident {
- return true
- }
- }
- return false
+func (c *templateTransformContext) hasIdent(idents []string, ident string) bool {
+ return slices.Contains(idents, ident)
}
// collectConfig collects and parses any leading template config variable declaration.
@@ -211,8 +265,8 @@ func (c *templateContext) hasIdent(idents []string, ident string) bool {
// on the form:
//
// {{ $_hugo_config:= `{ "version": 1 }` }}
-func (c *templateContext) collectConfig(n *parse.PipeNode) {
- if c.t.typ != templateShortcode {
+func (c *templateTransformContext) collectConfig(n *parse.PipeNode) {
+ if c.t.category != CategoryShortcode {
return
}
if c.configChecked {
@@ -244,7 +298,7 @@ func (c *templateContext) collectConfig(n *parse.PipeNode) {
c.err = fmt.Errorf(errMsg, err)
return
}
- if err := mapstructure.WeakDecode(m, &c.t.parseInfo.Config); err != nil {
+ if err := mapstructure.WeakDecode(m, &c.t.ParseInfo.Config); err != nil {
c.err = fmt.Errorf(errMsg, err)
}
}
@@ -252,11 +306,11 @@ func (c *templateContext) collectConfig(n *parse.PipeNode) {
// collectInner determines if the given CommandNode represents a
// shortcode call to its .Inner.
-func (c *templateContext) collectInner(n *parse.CommandNode) {
- if c.t.typ != templateShortcode {
+func (c *templateTransformContext) collectInner(n *parse.CommandNode) {
+ if c.t.category != CategoryShortcode {
return
}
- if c.t.parseInfo.IsInner || len(n.Args) == 0 {
+ if c.t.ParseInfo.IsInner || len(n.Args) == 0 {
return
}
@@ -270,14 +324,14 @@ func (c *templateContext) collectInner(n *parse.CommandNode) {
}
if c.hasIdent(idents, "Inner") || c.hasIdent(idents, "InnerDeindent") {
- c.t.parseInfo.IsInner = true
+ c.t.ParseInfo.IsInner = true
break
}
}
}
-func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
- if c.t.typ != templatePartial || c.returnNode != nil {
+func (c *templateTransformContext) collectReturnNode(n *parse.CommandNode) bool {
+ if c.t.category != CategoryPartial || c.returnNode != nil {
return true
}
@@ -296,17 +350,3 @@ func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
return false
}
-
-func findTemplateIn(name string, in tpl.Template) (tpl.Template, bool) {
- in = unwrap(in)
- if text, ok := in.(*texttemplate.Template); ok {
- if templ := text.Lookup(name); templ != nil {
- return templ, true
- }
- return nil, false
- }
- if templ := in.(*htmltemplate.Template).Lookup(name); templ != nil {
- return templ, true
- }
- return nil, false
-}
diff --git a/tpl/tplimpl/tplimpl_integration_test.go b/tpl/tplimpl/tplimpl_integration_test.go
index 6b2664c4d..b62898923 100644
--- a/tpl/tplimpl/tplimpl_integration_test.go
+++ b/tpl/tplimpl/tplimpl_integration_test.go
@@ -1,66 +1,28 @@
+// Copyright 2025 The Hugo Authors. All rights reserved.
+//
+// Portions Copyright The Go Authors.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
package tplimpl_test
import (
- "path/filepath"
"strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugolib"
- "github.com/gohugoio/hugo/tpl"
)
-func TestPrintUnusedTemplates(t *testing.T) {
- t.Parallel()
-
- files := `
--- config.toml --
-baseURL = 'http://example.com/'
-printUnusedTemplates=true
--- content/p1.md --
----
-title: "P1"
----
-{{< usedshortcode >}}
--- layouts/baseof.html --
-{{ block "main" . }}{{ end }}
--- layouts/baseof.json --
-{{ block "main" . }}{{ end }}
--- layouts/index.html --
-{{ define "main" }}FOO{{ end }}
--- layouts/_default/single.json --
--- layouts/_default/single.html --
-{{ define "main" }}MAIN{{ end }}
--- layouts/post/single.html --
-{{ define "main" }}MAIN{{ end }}
--- layouts/partials/usedpartial.html --
--- layouts/partials/unusedpartial.html --
--- layouts/shortcodes/usedshortcode.html --
-{{ partial "usedpartial.html" }}
--- layouts/shortcodes/unusedshortcode.html --
-
- `
-
- b := hugolib.NewIntegrationTestBuilder(
- hugolib.IntegrationTestConfig{
- T: t,
- TxtarString: files,
- NeedsOsFS: true,
- },
- )
- b.Build()
-
- unused := b.H.Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
-
- var names []string
- for _, tmpl := range unused {
- names = append(names, tmpl.Name())
- }
-
- b.Assert(names, qt.DeepEquals, []string{"_default/single.json", "baseof.json", "partials/unusedpartial.html", "post/single.html", "shortcodes/unusedshortcode.html"})
- b.Assert(unused[0].Filename(), qt.Equals, filepath.Join(b.Cfg.WorkingDir, "layouts/_default/single.json"))
-}
-
// Verify that the new keywords in Go 1.18 is available.
func TestGo18Constructs(t *testing.T) {
t.Parallel()
@@ -116,6 +78,20 @@ counter2: 3
`)
}
+func TestGo23ElseWith(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+title = "Hugo"
+-- layouts/index.html --
+{{ with false }}{{ else with .Site }}{{ .Title }}{{ end }}|
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Hugo|")
+}
+
// Issue 10495
func TestCommentsBeforeBlockDefinition(t *testing.T) {
t.Parallel()
@@ -372,7 +348,7 @@ series: [series-1]
-
+
@@ -411,3 +387,211 @@ series: [series-1]
` `,
)
}
+
+// Issue 12432
+func TestSchema(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+capitalizeListTitles = false
+disableKinds = ['rss','sitemap']
+[markup.goldmark.renderer]
+unsafe = true
+[params]
+description = "m n and **o** can't."
+[taxonomies]
+tag = 'tags'
+-- layouts/_default/list.html --
+{{ template "_internal/schema.html" . }}
+-- layouts/_default/single.html --
+{{ template "_internal/schema.html" . }}
+-- content/s1/p1.md --
+---
+title: p1
+date: 2024-04-24T08:00:00-07:00
+lastmod: 2024-04-24T11:00:00-07:00
+images: [a.jpg,b.jpg]
+tags: [t1,t2]
+---
+a b and **c** can't.
+-- content/s1/p2.md --
+---
+title: p2
+---
+d e and **f** can't.
+
+-- content/s1/p3.md --
+---
+title: p3
+summary: g h and **i** can't.
+---
+-- content/s1/p4.md --
+---
+title: p4
+description: j k and **l** can't.
+---
+-- content/s1/p5.md --
+---
+title: p5
+---
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/s1/p1/index.html", `
+
+
+
+
+
+
+
+
+ `,
+ )
+
+ b.AssertFileContent("public/s1/p2/index.html",
+ ` `,
+ )
+
+ b.AssertFileContent("public/s1/p3/index.html",
+ ` `,
+ )
+
+ // The markdown is intentionally not rendered to HTML.
+ b.AssertFileContent("public/s1/p4/index.html",
+ ` `,
+ )
+
+ // The markdown is intentionally not rendered to HTML.
+ b.AssertFileContent("public/s1/p5/index.html",
+ ` `,
+ )
+}
+
+// Issue 12433
+func TestTwitterCards(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+capitalizeListTitles = false
+disableKinds = ['rss','sitemap','taxonomy','term']
+[markup.goldmark.renderer]
+unsafe = true
+[params]
+description = "m n and **o** can't."
+[params.social]
+twitter = 'foo'
+-- layouts/_default/list.html --
+{{ template "_internal/twitter_cards.html" . }}
+-- layouts/_default/single.html --
+{{ template "_internal/twitter_cards.html" . }}
+-- content/s1/p1.md --
+---
+title: p1
+images: [a.jpg,b.jpg]
+---
+a b and **c** can't.
+-- content/s1/p2.md --
+---
+title: p2
+---
+d e and **f** can't.
+
+-- content/s1/p3.md --
+---
+title: p3
+summary: g h and **i** can't.
+---
+-- content/s1/p4.md --
+---
+title: p4
+description: j k and **l** can't.
+---
+-- content/s1/p5.md --
+---
+title: p5
+---
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/s1/p1/index.html", `
+
+
+
+
+
+ `,
+ )
+
+ b.AssertFileContent("public/s1/p2/index.html",
+ ` `,
+ ` `,
+ )
+
+ b.AssertFileContent("public/s1/p3/index.html",
+ ` `,
+ )
+
+ // The markdown is intentionally not rendered to HTML.
+ b.AssertFileContent("public/s1/p4/index.html",
+ ` `,
+ )
+
+ // The markdown is intentionally not rendered to HTML.
+ b.AssertFileContent("public/s1/p5/index.html",
+ ` `,
+ )
+}
+
+// Issue 12963
+func TestEditBaseofParseAfterExecute(t *testing.T) {
+ files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+disableLiveReload = true
+disableKinds = ["taxonomy", "term", "rss", "404", "sitemap"]
+[internal]
+fastRenderMode = true
+-- layouts/_default/baseof.html --
+Baseof!
+{{ block "main" . }}default{{ end }}
+{{ with (templates.Defer (dict "key" "global")) }}
+Now. {{ now }}
+{{ end }}
+-- layouts/_default/single.html --
+{{ define "main" }}
+Single.
+{{ end }}
+-- layouts/_default/list.html --
+{{ define "main" }}
+List.
+{{ .Content }}
+{{ range .Pages }}{{ .Title }}{{ end }}|
+{{ end }}
+-- content/mybundle1/index.md --
+---
+title: "My Bundle 1"
+---
+-- content/mybundle2/index.md --
+---
+title: "My Bundle 2"
+---
+-- content/_index.md --
+---
+title: "Home"
+---
+Home!
+`
+
+ b := hugolib.TestRunning(t, files)
+ b.AssertFileContent("public/index.html", "Home!")
+ b.EditFileReplaceAll("layouts/_default/baseof.html", "baseof", "Baseof!").Build()
+ b.BuildPartial("/")
+ b.AssertFileContent("public/index.html", "Baseof!")
+ b.BuildPartial("/mybundle1/")
+ b.AssertFileContent("public/mybundle1/index.html", "Baseof!")
+}
diff --git a/tpl/tplimplinit/tplimplinit.go b/tpl/tplimplinit/tplimplinit.go
new file mode 100644
index 000000000..6316e8897
--- /dev/null
+++ b/tpl/tplimplinit/tplimplinit.go
@@ -0,0 +1,96 @@
+// Copyright 2025 The Hugo Authors. All rights reserved.
+//
+// Portions Copyright The Go Authors.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tplimplinit
+
+import (
+ // Init the template funcs namespaces
+ "context"
+ "html/template"
+
+ "github.com/gohugoio/hugo/deps"
+ _ "github.com/gohugoio/hugo/tpl/cast"
+ _ "github.com/gohugoio/hugo/tpl/collections"
+ _ "github.com/gohugoio/hugo/tpl/compare"
+ _ "github.com/gohugoio/hugo/tpl/crypto"
+ _ "github.com/gohugoio/hugo/tpl/css"
+ _ "github.com/gohugoio/hugo/tpl/data"
+ _ "github.com/gohugoio/hugo/tpl/debug"
+ _ "github.com/gohugoio/hugo/tpl/diagrams"
+ _ "github.com/gohugoio/hugo/tpl/encoding"
+ _ "github.com/gohugoio/hugo/tpl/fmt"
+ _ "github.com/gohugoio/hugo/tpl/hash"
+ _ "github.com/gohugoio/hugo/tpl/hugo"
+ _ "github.com/gohugoio/hugo/tpl/images"
+ _ "github.com/gohugoio/hugo/tpl/inflect"
+ "github.com/gohugoio/hugo/tpl/internal"
+ _ "github.com/gohugoio/hugo/tpl/js"
+ _ "github.com/gohugoio/hugo/tpl/lang"
+ _ "github.com/gohugoio/hugo/tpl/math"
+ _ "github.com/gohugoio/hugo/tpl/openapi/openapi3"
+ _ "github.com/gohugoio/hugo/tpl/os"
+ _ "github.com/gohugoio/hugo/tpl/page"
+ _ "github.com/gohugoio/hugo/tpl/partials"
+ _ "github.com/gohugoio/hugo/tpl/path"
+ _ "github.com/gohugoio/hugo/tpl/reflect"
+ _ "github.com/gohugoio/hugo/tpl/resources"
+ _ "github.com/gohugoio/hugo/tpl/safe"
+ _ "github.com/gohugoio/hugo/tpl/site"
+ _ "github.com/gohugoio/hugo/tpl/strings"
+ _ "github.com/gohugoio/hugo/tpl/templates"
+ _ "github.com/gohugoio/hugo/tpl/time"
+ _ "github.com/gohugoio/hugo/tpl/transform"
+ _ "github.com/gohugoio/hugo/tpl/urls"
+)
+
+// CreateFuncMap creates a template.FuncMap with all of Hugo's template funcs,
+// excluding the Go built-ins.
+func CreateFuncMap(d *deps.Deps) map[string]any {
+ funcMap := template.FuncMap{}
+ nsMap := make(map[string]any)
+ var onCreated []func(namespaces map[string]any)
+
+ // Merge the namespace funcs
+ for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
+ ns := nsf(d)
+ if _, exists := funcMap[ns.Name]; exists {
+ panic(ns.Name + " is a duplicate template func")
+ }
+ funcMap[ns.Name] = ns.Context
+ contextV, err := ns.Context(context.Background())
+ if err != nil {
+ panic(err)
+ }
+ nsMap[ns.Name] = contextV
+ for _, mm := range ns.MethodMappings {
+ for _, alias := range mm.Aliases {
+ if _, exists := funcMap[alias]; exists {
+ panic(alias + " is a duplicate template func")
+ }
+ funcMap[alias] = mm.Method
+ }
+ }
+
+ if ns.OnCreated != nil {
+ onCreated = append(onCreated, ns.OnCreated)
+ }
+ }
+
+ for _, f := range onCreated {
+ f(nsMap)
+ }
+
+ return funcMap
+}
diff --git a/tpl/transform/testdata/large-katex.md b/tpl/transform/testdata/large-katex.md
new file mode 100644
index 000000000..7f0fa9b92
--- /dev/null
+++ b/tpl/transform/testdata/large-katex.md
@@ -0,0 +1,373 @@
+---
+title: Large KaTeX
+source: https://math.stackexchange.com/questions/8337/different-methods-to-compute-sum-limits-k-1-infty-frac1k2-basel-pro
+license: https://creativecommons.org/licenses/by-sa/4.0/
+---
+
+
+As I have heard people did not trust Euler when he first discovered the formula (solution of the Basel problem) $$\zeta(2)=\sum_{k=1}^\infty \frac{1}{k^2}=\frac{\pi^2}{6}.$$ However, Euler was Euler and he gave other proofs.
+I believe many of you know some nice proofs of this, can you please share it with us?
+
+Hans Lundmark
+OK, here's my favorite. I thought of this after reading a proof from the book "Proofs from the book" by Aigner & Ziegler, but later I found more or less the same proof as mine in a paper published a few years earlier by Josef Hofbauer. On Robin's list, the proof most similar to this is number 9 (EDIT: ...which is actually the proof that I read in Aigner & Ziegler).
+When $0 < x < \pi/2$ we have $0<\sin x < x < \tan x$ and thus $$\frac{1}{\tan^2 x} < \frac{1}{x^2} < \frac{1}{\sin^2 x}.$$ Note that $1/\tan^2 x = 1/\sin^2 x - 1$. Split the interval $(0,\pi/2)$ into $2^n$ equal parts, and sum the inequality over the (inner) "gridpoints" $x_k=(\pi/2) \cdot (k/2^n)$: $$\sum_{k=1}^{2^n-1} \frac{1}{\sin^2 x_k} - \sum_{k=1}^{2^n-1} 1 < \sum_{k=1}^{2^n-1} \frac{1}{x_k^2} < \sum_{k=1}^{2^n-1} \frac{1}{\sin^2 x_k}.$$ Denoting the sum on the right-hand side by $S_n$, we can write this as $$S_n - (2^n - 1) < \sum_{k=1}^{2^n-1} \left( \frac{2 \cdot 2^n}{\pi} \right)^2 \frac{1}{k^2} < S_n.$$
+Although $S_n$ looks like a complicated sum, it can actually be computed fairly easily. To begin with, $$\frac{1}{\sin^2 x} + \frac{1}{\sin^2 (\frac{\pi}{2}-x)} = \frac{\cos^2 x + \sin^2 x}{\cos^2 x \cdot \sin^2 x} = \frac{4}{\sin^2 2x}.$$ Therefore, if we pair up the terms in the sum $S_n$ except the midpoint $\pi/4$ (take the point $x_k$ in the left half of the interval $(0,\pi/2)$ together with the point $\pi/2-x_k$ in the right half) we get 4 times a sum of the same form, but taking twice as big steps so that we only sum over every other gridpoint; that is, over those gridpoints that correspond to splitting the interval into $2^{n-1}$ parts. And the midpoint $\pi/4$ contributes with $1/\sin^2(\pi/4)=2$ to the sum. In short, $$S_n = 4 S_{n-1} + 2.$$ Since $S_1=2$, the solution of this recurrence is $$S_n = \frac{2(4^n-1)}{3}.$$ (For example like this: the particular (constant) solution $(S_p)_n = -2/3$ plus the general solution to the homogeneous equation $(S_h)_n = A \cdot 4^n$, with the constant $A$ determined by the initial condition $S_1=(S_p)_1+(S_h)_1=2$.)
+We now have $$ \frac{2(4^n-1)}{3} - (2^n-1) \leq \frac{4^{n+1}}{\pi^2} \sum_{k=1}^{2^n-1} \frac{1}{k^2} \leq \frac{2(4^n-1)}{3}.$$ Multiply by $\pi^2/4^{n+1}$ and let $n\to\infty$. This squeezes the partial sums between two sequences both tending to $\pi^2/6$. Voilà!
+
+Américo Tavares
+We can use the function $f(x)=x^{2}$ with $-\pi \leq x\leq \pi $ and find its expansion into a trigonometric Fourier series
+$$\dfrac{a_{0}}{2}+\sum_{n=1}^{\infty }(a_{n}\cos nx+b_{n}\sin nx),$$
+which is periodic and converges to $f(x)$ in $[-\pi, \pi] $.
+Observing that $f(x)$ is even, it is enough to determine the coefficients
+$$a_{n}=\dfrac{1}{\pi }\int_{-\pi }^{\pi }f(x)\cos nx\;dx\qquad n=0,1,2,3,...,$$
+because
+$$b_{n}=\dfrac{1}{\pi }\int_{-\pi }^{\pi }f(x)\sin nx\;dx=0\qquad n=1,2,3,... .$$
+For $n=0$ we have
+$$a_{0}=\dfrac{1}{\pi }\int_{-\pi }^{\pi }x^{2}dx=\dfrac{2}{\pi }\int_{0}^{\pi }x^{2}dx=\dfrac{2\pi ^{2}}{3}.$$
+And for $n=1,2,3,...$ we get
+$$a_{n}=\dfrac{1}{\pi }\int_{-\pi }^{\pi }x^{2}\cos nx\;dx$$
+$$=\dfrac{2}{\pi }\int_{0}^{\pi }x^{2}\cos nx\;dx=\dfrac{2}{\pi }\times \dfrac{ 2\pi }{n^{2}}(-1)^{n}=(-1)^{n}\dfrac{4}{n^{2}},$$
+because
+$$\int x^2\cos nx\;dx=\dfrac{2x}{n^{2}}\cos nx+\left( \frac{x^{2}}{ n}-\dfrac{2}{n^{3}}\right) \sin nx.$$
+Thus
+$$f(x)=\dfrac{\pi ^{2}}{3}+\sum_{n=1}^{\infty }\left( (-1)^{n}\dfrac{4}{n^{2}} \cos nx\right) .$$
+Since $f(\pi )=\pi ^{2}$, we obtain
+$$\pi ^{2}=\dfrac{\pi ^{2}}{3}+\sum_{n=1}^{\infty }\left( (-1)^{n}\dfrac{4}{ n^{2}}\cos \left( n\pi \right) \right) $$
+$$\pi ^{2}=\dfrac{\pi ^{2}}{3}+4\sum_{n=1}^{\infty }\left( (-1)^{n}(-1)^{n} \dfrac{1}{n^{2}}\right) $$
+$$\pi ^{2}=\dfrac{\pi ^{2}}{3}+4\sum_{n=1}^{\infty }\dfrac{1}{n^{2}}.$$
+Therefore
+$$\sum_{n=1}^{\infty }\dfrac{1}{n^{2}}=\dfrac{\pi ^{2}}{4}-\dfrac{\pi ^{2}}{12}= \dfrac{\pi ^{2}}{6}$$
+Second method (available on-line a few years ago) by Eric Rowland. From
+$$\log (1-t)=-\sum_{n=1}^{\infty}\dfrac{t^n}{n}$$
+and making the substitution $t=e^{ix}$ one gets the series expansion
+$$w=\text{Log}(1-e^{ix})=-\sum_{n=1}^{\infty }\dfrac{e^{inx}}{n}=-\sum_{n=1}^{ \infty }\dfrac{1}{n}\cos nx-i\sum_{n=1}^{\infty }\dfrac{1}{n}\sin nx,$$
+whose radius of convergence is $1$. Now if we take the imaginary part of both sides, the RHS becomes
+$$\Im w=-\sum_{n=1}^{\infty }\dfrac{1}{n}\sin nx,$$
+and the LHS
+$$\Im w=\arg \left( 1-\cos x-i\sin x\right) =\arctan \dfrac{-\sin x}{ 1-\cos x}.$$
+Since
+$$\arctan \dfrac{-\sin x}{1-\cos x}=-\arctan \dfrac{2\sin \dfrac{x}{2}\cdot \cos \dfrac{x}{2}}{2\sin ^{2}\dfrac{x}{2}}$$
+$$=-\arctan \cot \dfrac{x}{2}=-\arctan \tan \left( \dfrac{\pi }{2}-\dfrac{x}{2} \right) =\dfrac{x}{2}-\dfrac{\pi }{2},$$
+the following expansion holds
+$$\dfrac{\pi }{2}-\frac{x}{2}=\sum_{n=1}^{\infty }\dfrac{1}{n}\sin nx.\qquad (\ast )$$
+Integrating the identity $(\ast )$, we obtain
+$$\dfrac{\pi }{2}x-\dfrac{x^{2}}{4}+C=-\sum_{n=1}^{\infty }\dfrac{1}{n^{2}}\cos nx.\qquad (\ast \ast )$$
+Setting $x=0$, we get the relation between $C$ and $\zeta (2)$
+$$C=-\sum_{n=1}^{\infty }\dfrac{1}{n^{2}}=-\zeta (2).$$
+And for $x=\pi $, since
+$$\zeta (2)=2\sum_{n=1}^{\infty }\dfrac{(-1)^{n-1}}{n^{2}},$$
+we deduce
+$$\dfrac{\pi ^{2}}{4}+C=-\sum_{n=1}^{\infty }\dfrac{1}{n^{2}}\cos n\pi =\sum_{n=1}^{\infty }\dfrac{(-1)^{n-1}}{n^{2}}=\dfrac{1}{2}\zeta (2)=-\dfrac{1}{ 2}C.$$
+Solving for $C$
+$$C=-\dfrac{\pi ^{2}}{6},$$
+we thus prove
+$$\zeta (2)=\dfrac{\pi ^{2}}{6}.$$
+Note: this 2nd method can generate all the zeta values $\zeta (2n)$ by integrating repeatedly $(\ast\ast )$. This is the reason why I appreciate it. Unfortunately it does not work for $\zeta (2n+1)$.
+Note also the $$C=-\dfrac{\pi ^{2}}{6}$$ can be obtained by integrating $(\ast\ast )$ and substitute $$x=0,x=\pi$$ respectively.
+
+AD.
+Here is an other one which is more or less what Euler did in one of his proofs.
+The function $\sin x$ where $x\in\mathbb{R}$ is zero exactly at $x=n\pi$ for each integer $n$. If we factorized it as an infinite product we get
+$$\sin x = \cdots\left(1+\frac{x}{3\pi}\right)\left(1+\frac{x}{2\pi}\right)\left(1+\frac{x}{\pi}\right)x\left(1-\frac{x}{\pi}\right)\left(1-\frac{x}{2\pi}\right)\left(1-\frac{x}{3\pi}\right)\cdots =$$ $$= x\left(1-\frac{x^2}{\pi^2}\right)\left(1-\frac{x^2}{2^2\pi^2}\right)\left(1-\frac{x^2}{3^2\pi^2}\right)\cdots\quad.$$
+We can also represent $\sin x$ as a Taylor series at $x=0$:
+$$\sin x = x - \frac{x^3}{3!}+\frac{x^5}{5!}-\frac{x^7}{7!}+\cdots\quad.$$
+Multiplying the product and identifying the coefficient of $x^3$ we see that
+$$\frac{x^3}{3!}=x\left(\frac{x^2}{\pi^2} + \frac{x^2}{2^2\pi^2}+ \frac{x^2}{3^2\pi^2}+\cdots\right)=x^3\sum_{n=1}^{\infty}\frac{1}{n^2\pi^2}$$ or $$\sum_{n=1}^\infty\frac{1}{n^2}=\frac{\pi^2}{6}.$$
+
+Alfredo Z.
+Define the following series for $ x > 0 $
+$$\frac{\sin x}{x} = 1 - \frac{x^2}{3!}+\frac{x^4}{5!}-\frac{x^6}{7!}+\cdots\quad.$$
+Now substitute $ x = \sqrt{y}\ $ to arrive at
+$$\frac{\sin \sqrt{y}\ }{\sqrt{y}\ } = 1 - \frac{y}{3!}+\frac{y^2}{5!}-\frac{y^3}{7!}+\cdots\quad.$$
+if we find the roots of $\frac{\sin \sqrt{y}\ }{\sqrt{y}\ } = 0 $ we find that
+$ y = n^2\pi^2\ $ for $ n \neq 0 $ and $ n $ in the integers
+With all of this in mind, recall that for a polynomial
+$ P(x) = a_{n}x^n + a_{n-1}x^{n-1} +\cdots+a_{1}x + a_{0} $ with roots $ r_{1}, r_{2}, \cdots , r_{n} $
+$$\frac{1}{r_{1}} + \frac{1}{r_{2}} + \cdots + \frac{1}{r_{n}} = -\frac{a_{1}}{a_{0}}$$
+Treating the above series for $ \frac{\sin \sqrt{y}\ }{\sqrt{y}\ } $ as polynomial we see that
+$$\frac{1}{1^2\pi^2} + \frac{1}{2^2\pi^2} + \frac{1}{3^2\pi^2} + \cdots = -\frac{-\frac{1}{3!}}{1}$$
+then multiplying both sides by $ \pi^2 $ gives the desired series.
+$$\frac{1}{1^2} + \frac{1}{2^2} + \frac{1}{3^2} + \cdots = \frac{\pi^2}{6}$$
+
+Nameless
+This method apparently was used by Tom Apostol in $1983$. I will outline the main ideas of the proof, the details can be found in here or this presentation (page $27$)
+Consider
+$$\begin{align} \int_{0}^{1} \int_{0}^{1} \frac{1}{1 - xy} dy dx &= \int_{0}^{1} \int_{0}^{1} \sum_{n \geq 0} (xy)^n dy dx \\ &= \sum_{n \geq 0} \int_{0}^{1} \int_{0}^{1} x^n y^n dy dx \\ &= \sum_{n \geq 1} \frac{1}{n^2} \\ \end{align}$$
+You can verify that the left hand side is indeed $\frac{\pi^2}{6}$ by letting $x = u - v$ and $y = v + u.$
+
+Qiaochu Yuan
+I have two favorite proofs. One is the last proof in Robin Chapman's collection; you really should take a look at it.
+The other is a proof that generalizes to the evaluation of $\zeta(2n)$ for all $n$, although I'll do it "Euler-style" to shorten the presentation. The basic idea is that meromorphic functions have infinite partial fraction decompositions that generalize the partial fraction decompositions of rational functions.
+The particular function we're interested in is $B(x) = \frac{x}{e^x - 1}$, the exponential generating function of the Bernoulli numbers $B_n$. $B$ is meromorphic with poles at $x = 2 \pi i n, n \in \mathbb{Z}$, and at these poles it has residue $2\pi i n$. It follows that we can write, a la Euler,
+$$\frac{x}{e^x - 1} = \sum_{n \in \mathbb{Z}} \frac{2\pi i n}{x - 2 \pi i n} = \sum_{n \in \mathbb{Z}} - \left( \frac{1}{1 - \frac{x}{2\pi i n}} \right).$$
+Now we can expand each of the terms on the RHS as a geometric series, again a la Euler, to obtain
+$$\frac{x}{e^x - 1} = - \sum_{n \in \mathbb{Z}} \sum_{k \ge 0} \left( \frac{x}{2\pi i n} \right)^k = \sum_{k \ge 0} (-1)^{n+1} \frac{2 \zeta(2n)}{(2\pi )^{2n}} x^{2n}$$
+because, after rearranging terms, the sum over odd powers cancels out and the sum over even powers doesn't. (This is one indication of why there is no known closed form for $\zeta(2n+1)$.) Equating terms on both sides, it follows that
+$$B_{2n} = (-1)^{n+1} \frac{2 \zeta(2n)}{(2\pi)^{2n}}$$
+or
+$$\zeta(2n) = (-1)^{n+1} \frac{B_{2n} (2\pi)^{2n}}{2}$$
+as desired. To compute $\zeta(2)$ it suffices to compute that $B_2 = \frac{1}{6}$, which then gives the usual answer.
+Here is one more nice proof, I learned it from Grisha Mikhalkin:
+Lemma: Let $Z$ be a complex curve in $\mathbb{C}^2$. Let $R(Z) \subset \mathbb{R}^2$ be the projection of $Z$ onto its real parts and $I(Z)$ the projection onto its complex parts. If these projections are both one to one, then the area of $R(Z)$ is equal to the area of $I(Z)$.
+Proof: There is an obvious map from $R(Z)$ to $I(Z)$, given by lifting $(x_1, x_2) \in R(Z)$ to $(x_1+i y_1, x_2 + i y_2) \in Z$, and then projecting to $(y_1, y_2) \in I(Z)$. We must prove this map has Jacobian $1$. WLOG, translate $(x_1, y_1, x_2, y_2)$ to $(0,0,0,0)$ and let $Z$ obey $\partial z_2/\partial z_1 = a+bi$ near $(0,0)$. To first order, we have $x_2 = a x_1 - b y_1$ and $y_2 = a y_1 + b x_1$. So $y_1 = (a/b) x_1 - (1/b) x_2$ and $y_2 = (a^2 + b^2)/b x_1 - (a/b) x_2$. So the derivative of $(x_1, x_2) \mapsto (y_1, y_2)$ is $\left( \begin{smallmatrix} a/b & - 1/b \\ (a^2 + b^2)/b & -a/b \end{smallmatrix} \right)$ and the Jacobian is $1$. QED
+Now, consider the curve $e^{-z_1} + e^{-z_2} = 1$, where $z_1$ and $z_2$ obey the following inequalities: $x_1 \geq 0$, $x_2 \geq 0$, $-\pi \leq y_1 \leq 0$ and $0 \leq y_2 \leq \pi$.
+Given a point on $e^{-z_1} + e^{-z_2} = 1$, consider the triangle with vertices at $0$, $e^{-z_1}$ and $e^{-z_1} + e^{-z_2} = 1$. The inequalities on the $y$'s states that the triangle should lie above the real axis; the inequalities on the $x$'s state the horizontal base should be the longest side.
+Projecting onto the $x$ coordinates, we see that the triangle exists if and only if the triangle inequality $e^{-x_1} + e^{-x_2} \geq 1$ is obeyed. So $R(Z)$ is the region under the curve $x_2 = - \log(1-e^{-x_1})$. The area under this curve is $$\int_{0}^{\infty} - \log(1-e^{-x}) dx = \int_{0}^{\infty} \sum \frac{e^{-kx}}{k} dx = \sum \frac{1}{k^2}.$$
+Now, project onto the $y$ coordinates. Set $(y_1, y_2) = (-\theta_1, \theta_2)$ for convenience, so the angles of the triangle are $(\theta_1, \theta_2, \pi - \theta_1 - \theta_2)$. The largest angle of a triangle is opposite the largest side, so we want $\theta_1$, $\theta_2 \leq \pi - \theta_1 - \theta_2$, plus the obvious inequalities $\theta_1$, $\theta_2 \geq 0$. So $I(Z)$ is the quadrilateral with vertices at $(0,0)$, $(0, \pi/2)$, $(\pi/3, \pi/3)$ and $(\pi/2, 0)$ and, by elementary geometry, this has area $\pi^2/6$.
+
+David Speyer
+I'll post the one I know since it is Euler's, and is quite easy and stays in $\mathbb{R}$. (I'm guessing Euler didn't have tools like residues back then).
+
+Peter Tamaroff
+Let
+$$s = {\sin ^{ - 1}}x$$
+Then
+$$\int\limits_0^{\frac{\pi }{2}} {sds} = \frac{{{\pi ^2}}}{8}$$
+But then
+$$\int\limits_0^1 {\frac{{{{\sin }^{ - 1}}x}}{{\sqrt {1 - {x^2}} }}dx} = \frac{{{\pi ^2}}}{8}$$
+Since
+$${\sin ^{ - 1}}x = \int {\frac{{dx}}{{\sqrt {1 - {x^2}} }}} = x + \frac{1}{2}\frac{{{x^3}}}{3} + \frac{{1 \cdot 3}}{{2 \cdot 4}}\frac{{{x^5}}}{5} + \frac{{1 \cdot 3 \cdot 5}}{{2 \cdot 4 \cdot 6}}\frac{{{x^7}}}{7} + \cdots $$
+We have
+$$\int\limits_0^1 {\left\{ {\frac{{dx}}{{\sqrt {1 - {x^2}} }}\int {\frac{{dx}}{{\sqrt {1 - {x^2}} }}} } \right\}} = \int\limits_0^1 {\left\{ {x + \frac{1}{2}\frac{{{x^3}}}{3}\frac{{dx}}{{\sqrt {1 - {x^2}} }} + \frac{{1 \cdot 3}}{{2 \cdot 4}}\frac{{{x^5}}}{5}\frac{{dx}}{{\sqrt {1 - {x^2}} }} + \cdots } \right\}} $$
+But
+$$\int\limits_0^1 {\frac{{{x^{2n + 1}}}}{{\sqrt {1 - {x^2}} }}dx} = \frac{{2n}}{{2n + 1}}\int\limits_0^1 {\frac{{{x^{2n - 1}}}}{{\sqrt {1 - {x^2}} }}dx} $$
+which yields
+$$\int\limits_0^1 {\frac{{{x^{2n + 1}}}}{{\sqrt {1 - {x^2}} }}dx} = \frac{{\left( {2n} \right)!!}}{{\left( {2n + 1} \right)!!}}$$
+since all powers are odd.
+This ultimately produces:
+$$\frac{{{\pi ^2}}}{8} = 1 + \frac{1}{2}\frac{1}{3}\left( {\frac{2}{3}} \right) + \frac{{1 \cdot 3}}{{2 \cdot 4}}\frac{1}{5}\left( {\frac{{2 \cdot 4}}{{3 \cdot 5}}} \right) + \frac{{1 \cdot 3 \cdot 5}}{{2 \cdot 4 \cdot 6}}\frac{1}{7}\left( {\frac{{2 \cdot 4 \cdot 6}}{{3 \cdot 5 \cdot 7}}} \right) \cdots $$
+$$\frac{{{\pi ^2}}}{8} = 1 + \frac{1}{{{3^2}}} + \frac{1}{{{5^2}}} + \frac{1}{{{7^2}}} + \cdots $$
+Let
+$$1 + \frac{1}{{{2^2}}} + \frac{1}{{{3^2}}} + \frac{1}{{{4^2}}} + \cdots = \omega $$
+Then
+$$\frac{1}{{{2^2}}} + \frac{1}{{{4^2}}} + \frac{1}{{{6^2}}} + \frac{1}{{{8^2}}} + \cdots = \frac{\omega }{4}$$
+Which means
+$$\frac{\omega }{4} + \frac{{{\pi ^2}}}{8} = \omega $$
+or
+$$\omega = \frac{{{\pi ^2}}}{6}$$
+
+Mike Spivey
+The most recent issue of The American Mathematical Monthly (August-September 2011, pp. 641-643) has a new proof by Luigi Pace based on elementary probability. Here's the argument.
+Let $X_1$ and $X_2$ be independent, identically distributed standard half-Cauchy random variables. Thus their common pdf is $p(x) = \frac{2}{\pi (1+x^2)}$ for $x > 0$.
+Let $Y = X_1/X_2$. Then the pdf of $Y$ is, for $y > 0$, $$p_Y(y) = \int_0^{\infty} x p_{X_1} (xy) p_{X_2}(x) dx = \frac{4}{\pi^2} \int_0^\infty \frac{x}{(1+x^2 y^2)(1+x^2)}dx$$ $$=\frac{2}{\pi^2 (y^2-1)} \left[\log \left( \frac{1+x^2 y^2}{1+x^2}\right) \right]_{x=0}^{\infty} = \frac{2}{\pi^2} \frac{\log(y^2)}{y^2-1} = \frac{4}{\pi^2} \frac{\log(y)}{y^2-1}.$$
+Since $X_1$ and $X_2$ are equally likely to be the larger of the two, we have $P(Y < 1) = 1/2$. Thus $$\frac{1}{2} = \int_0^1 \frac{4}{\pi^2} \frac{\log(y)}{y^2-1} dy.$$ This is equivalent to $$\frac{\pi^2}{8} = \int_0^1 \frac{-\log(y)}{1-y^2} dy = -\int_0^1 \log(y) (1+y^2+y^4 + \cdots) dy = \sum_{k=0}^\infty \frac{1}{(2k+1)^2},$$ which, as others have pointed out, implies $\zeta(2) = \pi^2/6$.
+
+Hans Lundmark
+This is not really an answer, but rather a long comment prompted by David Speyer's answer. The proof that David gives seems to be the one in How to compute $\sum 1/n^2$ by solving triangles by Mikael Passare, although that paper uses a slightly different way of seeing that the area of the region $U_0$ (in Passare's notation) bounded by the positive axes and the curve $e^{-x}+e^{-y}=1$, $$\int_0^{\infty} -\ln(1-e^{-x}) dx,$$ is equal to $\sum_{n\ge 1} \frac{1}{n^2}$.
+This brings me to what I really wanted to mention, namely another curious way to see why $U_0$ has that area; I learned this from Johan Wästlund. Consider the region $D_N$ illustrated below for $N=8$:
+A shape with area = sum of reciprocal squares
+Although it's not immediately obvious, the area of $D_N$ is $\sum_{n=1}^N \frac{1}{n^2}$. Proof: The area of $D_1$ is 1. To get from $D_N$ to $D_{N+1}$ one removes the boxes along the top diagonal, and adds a new leftmost column of rectangles of width $1/(N+1)$ and heights $1/1,1/2,\ldots,1/N$, plus a new bottom row which is the "transpose" of the new column, plus a square of side $1/(N+1)$ in the bottom left corner. The $k$th rectangle from the top in the new column and the $k$th rectangle from the left in the new row (not counting the square) have a combined area which exactly matches the $k$th box in the removed diagonal: $$ \frac{1}{k} \frac{1}{N+1} + \frac{1}{N+1} \frac{1}{N+1-k} = \frac{1}{k} \frac{1}{N+1-k}. $$ Thus the area added in the process is just that of the square, $1/(N+1)^2$. Q.E.D.
+(Apparently this shape somehow comes up in connection with the "random assignment problem", where there's an expected value of something which turns out to be $\sum_{n=1}^N \frac{1}{n^2}$.)
+Now place $D_N$ in the first quadrant, with the lower left corner at the origin. Letting $N\to\infty$ gives nothing but the region $U_0$: for large $N$ and for $0<\alpha<1$, the upper corner of column number $\lceil \alpha N \rceil$ in $D_N$ lies at $$ (x,y) = \left( \sum_{n=\lceil (1-\alpha) N \rceil}^N \frac{1}{n}, \sum_{n=\lceil \alpha N \rceil}^N \frac{1}{n} \right) \sim \left(\ln\frac{1}{1-\alpha}, \ln\frac{1}{\alpha}\right),$$ hence (in the limit) on the curve $e^{-x}+e^{-y}=1$.
+
+xpaul
+Note that $$ \frac{\pi^2}{\sin^2\pi z}=\sum_{n=-\infty}^{\infty}\frac{1}{(z-n)^2} $$ from complex analysis and that both sides are analytic everywhere except $n=0,\pm 1,\pm 2,\cdots$. Then one can obtain $$ \frac{\pi^2}{\sin^2\pi z}-\frac{1}{z^2}=\sum_{n=1}^{\infty}\frac{1}{(z-n)^2}+\sum_{n=1}^{\infty}\frac{1}{(z+n)^2}. $$ Now the right hand side is analytic at $z=0$ and hence $$\lim_{z\to 0}\left(\frac{\pi^2}{\sin^2\pi z}-\frac{1}{z^2}\right)=2\sum_{n=1}^{\infty}\frac{1}{n^2}.$$ Note $$\lim_{z\to 0}\left(\frac{\pi^2}{\sin^2\pi z}-\frac{1}{z^2}\right)=\frac{\pi^2}{3}.$$ Thus $$\sum_{n=1}^{\infty}\frac{1}{n^2}=\frac{\pi^2}{6}.$$
+
+Jack D'Aurizio
+Just as a curiosity, a one-line-real-analytic-proof I found by combining different ideas from this thread and this question:
+$$\begin{eqnarray*}\zeta(2)&=&\frac{4}{3}\sum_{n=0}^{+\infty}\frac{1}{(2n+1)^2}=\frac{4}{3}\int_{0}^{1}\frac{\log y}{y^2-1}dy\\&=&\frac{2}{3}\int_{0}^{1}\frac{1}{y^2-1}\left[\log\left(\frac{1+x^2 y^2}{1+x^2}\right)\right]_{x=0}^{+\infty}dy\\&=&\frac{4}{3}\int_{0}^{1}\int_{0}^{+\infty}\frac{x}{(1+x^2)(1+x^2 y^2)}dx\,dy\\&=&\frac{4}{3}\int_{0}^{1}\int_{0}^{+\infty}\frac{dx\, dz}{(1+x^2)(1+z^2)}=\frac{4}{3}\cdot\frac{\pi}{4}\cdot\frac{\pi}{2}=\frac{\pi^2}{6}.\end{eqnarray*}$$
+Update. By collecting pieces, I have another nice proof. By Euler's acceleration method or just an iterated trick like my $(1)$ here we get: $$ \zeta(2) = \sum_{n\geq 1}\frac{1}{n^2} = \color{red}{\sum_{n\geq 1}\frac{3}{n^2\binom{2n}{n}}}\tag{A}$$ and the last series converges pretty fast. Then we may notice that the last series comes out from a squared arcsine. That just gives another proof of $ \zeta(2)=\frac{\pi^2}{6}$.
+A proof of the identity $$\sum_{n\geq 0}\frac{1}{(2n+1)^2}=\frac{\pi}{2}\sum_{k\geq 0}\frac{(-1)^k}{2k+1}=\frac{\pi}{2}\cdot\frac{\pi}{4}$$ is also hidden in tired's answer here. For short, the integral $$ I=\int_{-\infty}^{\infty}e^y\left(\frac{e^y-1}{y^2}-\frac{1}{y}\right)\frac{1}{e^{2y}+1}\,dy $$ is clearly real, so the imaginary part of the sum of residues of the integrand function has to be zero.
+Still another way (and a very efficient one) is to exploit the reflection formula for the trigamma function: $$\psi'(1-z)+\psi'(z)=\frac{\pi^2}{\sin^2(\pi z)}$$ immediately leads to: $$\frac{\pi^2}{2}=\psi'\left(\frac{1}{2}\right)=\sum_{n\geq 0}\frac{1}{\left(n+\frac{1}{2}\right)^2}=4\sum_{n\geq 0}\frac{1}{(2n+1)^2}=3\,\zeta(2).$$
+2018 update. We may consider that $\mathcal{J}=\int_{0}^{+\infty}\frac{\arctan x}{1+x^2}\,dx = \left[\frac{1}{2}\arctan^2 x\right]_0^{+\infty}=\frac{\pi^2}{8}$.
+On the other hand, by Feynman's trick or Fubini's theorem $$ \mathcal{J}=\int_{0}^{+\infty}\int_{0}^{1}\frac{x}{(1+x^2)(1+a^2 x^2)}\,da\,dx = \int_{0}^{1}\frac{-\log a}{1-a^2}\,da $$ and since $\int_{0}^{1}-\log(x)x^n\,dx = \frac{1}{(n+1)^2}$, by expanding $\frac{1}{1-a^2}$ as a geometric series we have $$ \frac{\pi^2}{8}=\mathcal{J}=\sum_{n\geq 0}\frac{1}{(2n+1)^2}. $$
+
+Andrey Rekalo
+Here is a complex-analytic proof.
+For $z\in D=\mathbb{C}\backslash${$0,1$}, let
+$$R(z)=\sum\frac{1}{\log^2 z}$$
+where the sum is taken over all branches of the logarithm. Each point in $D$ has a neighbourhood on which the branches of $\log(z)$ are analytic. Since the series converges uniformly away from $z=1$, $R(z)$ is analytic on $D$.
+Now a few observations:
+(i) Each term of the series tends to $0$ as $z\to0$. Thanks to the uniform convergence this implies that the singularity at $z=0$ is removable and we can set $R(0)=0$.
+(ii) The only singularity of $R$ is a double pole at $z=1$ due to the contribution of the principal branch of $\log z$. Moreover, $\lim_{z\to1}(z-1)^2R(z)=1$.
+(iii) $R(1/z)=R(z)$.
+By (i) and (iii) $R$ is meromorphic on the extended complex plane, therefore it is rational. By (ii) the denominator of $R(z)$ is $(z-1)^2$. Since $R(0)=R(\infty)=0$, the numerator has the form $az$. Then (ii) implies $a=1$, so that $$R(z)=\frac{z}{(z-1)^2}.$$
+Now, setting $z=e^{2\pi i w}$ yields $$\sum\limits_{n=-\infty}^{\infty}\frac{1}{(w-n)^2}=\frac{\pi^2}{\sin^2(\pi w)}$$ which implies that $$\sum\limits_{k=0}^{\infty}\frac{1}{(2k+1)^2}=\frac{\pi^2}{8},$$ and the identity $\zeta(2)=\pi^2/6$ follows.
+The proof is due to T. Marshall (American Mathematical Monthly, Vol. 117(4), 2010, P. 352).
+
+David Speyer
+In response to a request here: Compute $\oint z^{-2k} \cot (\pi z) dz$ where the integral is taken around a square of side $2N+1$. Routine estimates show that the integral goes to $0$ as $N \to \infty$.
+Now, let's compute the integral by residues. At $z=0$, the residue is $\pi^{2k-1} q$, where $q$ is some rational number coming from the power series for $\cot$. For example, if $k=1$, then we get $- \pi/3$.
+At $m \pi$, for $m \neq 0$, the residue is $z^{-2k} \pi^{-1}$. So $$\pi^{-1} \lim_{N \to \infty} \sum_{-N \leq m \leq N\ m \neq 0} m^{-2k} + \pi^{2k-1} q=0$$ or $$\sum_{m=1}^{\infty} m^{-2k} = -\pi^{2k} q/2$$ as desired. In particular, $\sum m^{-2} = - (\pi^2/3)/2 = \pi^2/6$.
+Common variants: We can replace $\cot$ with $\tan$, with $1/(e^{2 \pi i z}-1)$, or with similar formulas.
+This is reminiscent of Qiaochu's proof but, rather than actually establishing the relation $\pi^{-1} \cot(\pi z) = \sum (z-n)^{-1}$, one simply establishes that both sides contribute the same residues to a certain integral.
+
+Derek Jennings
+Another variation. We make use of the following identity (proved at the bottom of this note):
+$$\sum_{k=1}^n \cot^2 \left( \frac {2k-1}{2n} \frac{\pi}{2} \right) = 2n^2 - n. \quad (1)$$
+Now $1/\theta > \cot \theta > 1/\theta - \theta/3 > 0$ for $0< \theta< \pi/2 < \sqrt{3}$ and so $$ 1/\theta^2 - 2/3 < \cot^2 \theta < 1/\theta^2. \quad (2)$$
+With $\theta_k = (2k-1)\pi/4n,$ summing the inequalities $(2)$ from $k=1$ to $n$ we obtain
+$$2n^2 - n < \sum_{k=1}^n \left( \frac{2n}{2k-1}\frac{2}{\pi} \right)^2 < 2n^2 - n + 2n/3.$$
+Hence
+$$\frac{\pi^2}{16}\frac{2n^2-n}{n^2} < \sum_{k=1}^n \frac{1}{(2k-1)^2} < \frac{\pi^2}{16}\frac{2n^2-n/3}{n^2}.$$
+Taking the limit as $n \rightarrow \infty$ we obtain
+$$ \sum_{k=1}^\infty \frac{1}{(2k-1)^2} = \frac{\pi^2}{8},$$
+from which the result for $\sum_{k=1}^\infty 1/k^2$ follows easily.
+To prove $(1)$ we note that
+$$ \cos 2n\theta = \text{Re}(\cos\theta + i \sin\theta)^{2n} = \sum_{k=0}^n (-1)^k {2n \choose 2k}\cos^{2n-2k}\theta\sin^{2k}\theta.$$
+Therefore
+$$\frac{\cos 2n\theta}{\sin^{2n}\theta} = \sum_{k=0}^n (-1)^k {2n \choose 2k}\cot^{2n-2k}\theta.$$
+And so setting $x = \cot^2\theta$ we note that
+$$f(x) = \sum_{k=0}^n (-1)^k {2n \choose 2k}x^{n-k}$$
+has roots $x_j = \cot^2 (2j-1)\pi/4n,$ for $j=1,2,\ldots,n,$ from which $(1)$ follows since ${2n \choose 2n-2} = 2n^2-n.$
+
+xpaul
+A short way to get the sum is to use Fourier's expansion of $x^2$ in $x\in(-\pi,\pi)$. Recall that Fourier's expansion of $f(x)$ is $$ \tilde{f}(x)=\frac{1}{2}a_0+\sum_{n=1}^\infty(a_n\cos nx+b_n\sin nx), x\in(-\pi,\pi)$$ where $$ a_0=\frac{2}{\pi}\int_{-\pi}^{\pi}f(x)\;dx, a_n=\frac{2}{\pi}\int_{-\pi}^{\pi}f(x)\cos nx\; dx, b_n=\frac{2}{\pi}\int_{-\pi}^{\pi}f(x)\sin nx\; dx, n=1,2,3,\cdots $$ and $$ \tilde{f}(x)=\frac{f(x-0)+f(x+0)}{2}. $$ Easy calculation shows $$ x^2=\frac{\pi^2}{3}+4\sum_{n=1}^\infty(-1)^n\frac{\cos nx}{n^2}, x\in[-\pi,\pi]. $$ Letting $x=\pi$ in both sides gives $$ \sum_{n=1}^\infty\frac{1}{n^2}=\frac{\pi^2}{6}.$$
+Another way to get the sum is to use Parseval's Identity for Fourier's expansion of $x$ in $(-\pi,\pi)$. Recall that Parseval's Identity is $$ \int_{-\pi}^{\pi}|f(x)|^2dx=\frac{1}{2}a_0^2+\sum_{n=1}^\infty(a_n^2+b_n^2). $$ Note $$ x=2\sum_{n=1}^\infty(-1)^{n+1}\frac{\sin nx}{n}, x\in(-\pi,\pi). $$ Using Parseval's Identity gives $$ 4\sum_{n=1}^\infty\frac{1}{n^2}=\int_{-\pi}^{\pi}|x|^2dx$$ or $$ \sum_{n=1}^\infty\frac{1}{n^2}=\frac{\pi^2}{6}.$$
+
+Tomás
+I like this one:
+Let $f\in Lip(S^{1})$, where $Lip(S^{1})$ is the space of Lipschitz functions on $S^{1}$. So its well defined the number for $k\in \mathbb{Z}$ (called Fourier series of $f$) $$\hat{f}(k)=\frac{1}{2\pi}\int \hat{f}(\theta)e^{-ik\theta}d\theta.$$
+By the inversion formula, we have $$f(\theta)=\sum_{k\in\mathbb{Z}}\hat{f}(k)e^{ik\theta}.$$
+Now take $f(\theta)=|\theta|$, $\theta\in [-\pi,\pi]$. Note that $f\in Lip(S^{1})$
+We have $$ \hat{f}(k) = \left\{ \begin{array}{rl} \frac{\pi}{2} &\mbox{ if $k=0$} \\ 0 &\mbox{ if $|k|\neq 0$ and $|k|$ is even} \\ -\frac{2}{k^{2}\pi} &\mbox{if $|k|\neq 0$ and $|k|$ is odd} \end{array} \right. $$
+Using the inversion formula, we have on $\theta=0$ that $$0=\sum_{k\in\mathbb{Z}}\hat{f}(k).$$
+Then,
+\begin{eqnarray} 0 &=& \frac{\pi}{2}-\sum_{k\in\mathbb{Z}\ |k|\ odd}\frac{2}{k^{2}\pi} \nonumber \\ &=& \frac{\pi}{2}-\sum_{k\in\mathbb{N}\ |k|\ odd}\frac{4}{k^{2}\pi} \nonumber \\ \end{eqnarray}
+This implies $$\sum_{k\in\mathbb{N}\ |k|\ odd}\frac{1}{k^{2}} =\frac{\pi^{2}}{8}$$
+If we multiply the last equation by $\frac{1}{2^{2n}}$ with $n=0,1,2,...$ ,we get $$\sum_{k\in\mathbb{N}\ |k|\ odd}\frac{1}{(2^{n}k)^{2}} =\frac{\pi^{2}}{2^{2n}8}$$
+Now $$\sum_{n=0,1,...}(\sum_{k\in\mathbb{N}\ |k|\ odd}\frac{1}{(2^{n}k)^{2}}) =\sum_{n=0,1,...}\frac{\pi^{2}}{2^{2n}8}$$
+The sum in the left is equal to: $\sum_{k\in\mathbb{N}}\frac{1}{k^{2}}$
+The sum in the right is equal to :$\frac{\pi^{2}}{6}$
+So we conclude: $$\sum_{k\in\mathbb{N}}\frac{1}{k^{2}}=\frac{\pi^{2}}{6}$$
+Note: This is problem 9, Page 208 from the boof of Michael Eugene Taylor - Partial Differential Equation Volume 1.
+
+user91500
+Theorem: Let $\lbrace a_n\rbrace$ be a nonincreasing sequence of positive numbers such that $\sum a_n^2$ converges. Then both series $$s:=\sum_{n=0}^\infty(-1)^na_n,\,\delta_k:=\sum_{n=0}^\infty a_na_{n+k},\,k\in\mathbb N $$ converge. Morevere $\Delta:=\sum_{k=1}^\infty(-1)^{k-1}\delta_k$ also converges, and we have the formula $$\sum_{n=0}^\infty a_n^2=s^2+2\Delta.$$ Proof: Knopp. Konrad, Theory and Application of Infinite Series, page 323.
+If we let $a_n=\frac1{2n+1}$ in this theorem, then we have $$s=\sum_{n=0}^\infty(-1)^n\frac1{2n+1}=\frac\pi 4$$ $$\delta_k=\sum_{n=0}^\infty\frac1{(2n+1)(2n+2k+1)}=\frac1{2k}\sum_{n=0}^\infty\left(\frac1{2n+1}-\frac1{2n+2k+1}\right)=\frac{1}{2k}\left(1+\frac1 3+...+\frac1 {2k-1}\right)$$ Hence, $$\sum_{n=0}^\infty\frac1{(2n+1)^2}=\left(\frac\pi 4\right)^2+\sum_{k=1}^\infty\frac{(-1)^{k-1}}{k}\left(1+\frac1 3+...+\frac1 {2k-1}\right)=\frac{\pi^2}{16}+\frac{\pi^2}{16}=\frac{\pi^2}{8}$$ and now $$\zeta(2)=\frac4 3\sum_{n=0}^\infty\frac1{(2n+1)^2}=\frac{\pi^2}6.$$
+
+Markus Scheuer
+Here's a proof based upon periods and the fact that $\zeta(2)$ and $\frac{\pi^2}{6}$ are periods forming an accessible identity.
+The definition of periods below and the proof is from the fascinating introductory survey paper about periods by M. Kontsevich and D. Zagier.
+Periods are defined as complex numbers whose real and imaginary parts are values of absolutely convergent integrals of rational functions with rational coefficient over domains in $\mathbb{R}^n$ given by polynomial inequalities with rational coefficients.
+The set of periods is therefore a countable subset of the complex numbers. It contains the algebraic numbers, but also many of famous transcendental constants.
+In order to show the equality $\zeta(2)=\frac{\pi^2}{6}$ we have to show that both are periods and that $\zeta(2)$ and $\frac{\pi^2}{6}$ form a so-called accessible identity.
+First step of the proof: $\zeta(2)$ and $\pi$ are periods
+There are a lot of different proper representations of $\pi$ showing that this constant is a period. In the referred paper above following expressions (besides others) of $\pi$ are stated:
+\begin{align*} \pi= \iint \limits_{x^2+y^2\leq 1}dxdy=\int_{-\infty}^{\infty}\frac{dx}{1+x^2} \end{align*}
+showing that $\pi$ is a period. The known representation
+\begin{align*} \zeta(2)=\iint_{0 = \sum_n \frac{1}{\lambda_n} $$and $$\sum_n = \int_0^1 \sum_n f_n(x) \,dx = \int_0^1 G(x,x)\,dx~~.$$
+The latter quantity is $$ \int_0^1 x(1-x)\,dx = \frac 1 2 - \frac 1 3 = \frac 1 6~~.$$
+Hence, we have that $$\sum_n \frac 1 {n^2\pi^2} = \frac 1 6~~\text{, or}~~ \sum_n \frac 1 {n^2} = \frac {\pi^2} 6~~.$$
+
+Markus Scheuer
+Here is Euler's Other Proof by Gerald Kimble
+> $$
+\begin{align*} \frac{\pi^2}{6}&=\frac{4}{3}\frac{(\arcsin 1)^2}{2}\\ &=\frac{4}{3}\int_0^1\frac{\arcsin x}{\sqrt{1-x^2}}\,dx\\ &=\frac{4}{3}\int_0^1\frac{x+\sum_{n=1}^{\infty}\frac{(2n-1)!!}{(2n)!!}\frac{x^{2n+1}}{2n+1}}{\sqrt{1-x^2}}\,dx\\ &=\frac{4}{3}\int_0^1\frac{x}{\sqrt{1-x^2}}\,dx +\frac{4}{3}\sum_{n=1}^{\infty}\frac{(2n-1)!!}{(2n)!!(2n+1)}\int_0^1x^{2n}\frac{x}{\sqrt{1-x^2}}\,dx\\ &=\frac{4}{3}+\frac{4}{3}\sum_{n=1}^{\infty}\frac{(2n-1)!!}{(2n)!!(2n+1)}\left[\frac{(2n)!!}{(2n+1)!!}\right]\\ &=\frac{4}{3}\sum_{n=0}^{\infty}\frac{1}{(2n+1)^2}\\ &=\frac{4}{3}\left(\sum_{n=1}^{\infty}\frac{1}{n^2}-\frac{1}{4}\sum_{n=1}^{\infty}\frac{1}{n^2}\right)\\ &=\sum_{n=1}^{\infty}\frac{1}{n^2} \end{align*}
+$$
+
+B_Scheiner
+Consider the function $\pi \cot(\pi z)$ which has poles at $z=\pm n$ where n is an integer. Using the L'hopital rule you can see that the residue at these poles is 1.
+Now consider the integral $\int_{\gamma_N} \frac{\pi\cot(\pi z)}{z^2} dz$ where the contour $\gamma_N$ is the rectangle with corners given by ±(N + 1/2) ± i(N + 1/2) so that the contour avoids the poles of $\cot(\pi z)$. The integral is bouond in the following way: $\int_{\gamma_N} |\frac{\pi\cot(\pi z)}{z^2} |dz\le Max |(\frac{\pi\cot(\pi z)}{z^2}) | Length(\gamma_N)$. It can easily be shown that on the contour $\gamma_N$ that $\pi \cot(\pi z)< M$ where M is some constant. Then we have
+$\int_{\gamma_N} |\frac{\pi\cot(\pi z)}{z^2} |dz\le M Max |\frac{1}{z^2} | Length(\gamma_N) = (8N+4) \frac{M}{\sqrt{2(1/2+N)^2}^2}$
+where (8N+4) is the lenght of the contour and $\sqrt{2(1/2+N)^2}$ is half the diagonal of $\gamma_N$. In the limit that N goes to infinity the integral is bound by 0 so we have $\int_{\gamma_N} \frac{\pi\cot(\pi z)}{z^2} dz =0$
+by the cauchy residue theorem we have 2πiRes(z = 0) + 2πi$\sum$Residues(z$\ne$ 0) = 0. At z=0 we have Res(z=0)=$-\frac{\pi^2}{3}$, and $Res (z=n)=\frac{1}{n^2}$ so we have
+$2\pi iRes(z = 0) + 2\pi i\sum Residues(z\ne 0) = -\frac{\pi^2}{3}+2\sum_{1}^{\infty} \frac{1}{n^2} =0$
+Where the 2 in front of the residue at n is because they occur twice at +/- n.
+We now have the desired result $\sum_{1}^{\infty} \frac{1}{n^2}=\frac{\pi^2}{6}$.
+
+Meadara
+I saw this proof in an extract of the College Mathematics Journal.
+Consider the Integeral : $I = \int_0^{\pi/2}\ln(2\cos x)dx$
+From $2\cos(x) = e^{ix} + e^{-ix}$ , we have:
+$$\int_0^{\pi/2}\ln\left(e^{ix} + e^{-ix}\right)dx = \int_0^{\pi/2}\ln\left(e^{ix}(1 + e^{-2ix})\right)dx=\int_0^{\pi/2}ixdx + \int_0^{\pi/2}\ln(1 + e^{-2ix})dx$$
+The Taylor series expansion of $\ln(1+x)=x -\frac{x^2}{2} +\frac{x^3}{3}-\cdots$
+Thus , $\ln(1+e^{-2ix}) = e^{-2ix}- \frac{e^{-4ix}}{2} + \frac{e^{-6ix}}{3} - \cdots $, then for $I$ :
+$$I = \frac{i\pi^2}{8}+\left[-\frac{e^{-2ix}}{2i}+\frac{e^{-4ix}}{2\cdot 4i}-\frac{e^{-6ix}}{3\cdot 6i}-\cdots\right]_0^\frac{\pi}{2}$$
+$$I = \frac{i\pi^2}{8}-\frac{1}{2i}\left[\frac{e^{-2ix}}{1^2}-\frac{e^{-4ix}}{2^2}+\frac{e^{-6ix}}{3^2}-\cdots\right]_0^\frac{\pi}{2}$$
+By evaluating we get something like this..
+$$I = \frac{i\pi^2}{8}-\frac{1}{2i}\left[\frac{-2}{1^2}-\frac{0}{2^2}+\frac{-2}{3^2}-\cdots\right]_0^\frac{\pi}{2}$$
+Hence
+$$\int_0^{\pi/2}\ln(2\cos x)dx=\frac{i\pi^2}{8}-i\sum_{k=0}^\infty \frac{1}{(2k+1)^2}$$
+So now we have a real integral equal to an imaginary number, thus the value of the integral should be zero.
+Thus, $\sum_{k=0}^\infty \frac{1}{(2k+1)^2}=\frac{\pi^2}{8}$
+But let $\sum_{k=0}^\infty \frac{1}{k^2}=E$ .We get $\sum_{k=0}^\infty \frac{1}{(2k+1)^2}=\frac{3}{4} E$
+And as a result $$\sum_{k=0}^\infty \frac{1}{k^2} = \frac{\pi^2}{6}$$
+
+dustin
+I have another method as well. From skimming the previous solutions, I don't think it is a duplicate of any of them
+In Complex analysis, we learn that $\sin(\pi z) = \pi z\Pi_{n=1}^{\infty}\Big(1 - \frac{z^2}{n^2}\Big)$ which is an entire function with simple zer0s at the integers. We can differentiate term wise by uniform convergence. So by logarithmic differentiation we obtain a series for $\pi\cot(\pi z)$. $$ \frac{d}{dz}\ln(\sin(\pi z)) = \pi\cot(\pi z) = \frac{1}{z} - 2z\sum_{n=1}^{\infty}\frac{1}{n^2 - z^2} $$ Therefore, $$ -\sum_{n=1}^{\infty}\frac{1}{n^2 - z^2} = \frac{\pi\cot(\pi z) - \frac{1}{z}}{2z} $$ We can expand $\pi\cot(\pi z)$ as $$ \pi\cot(\pi z) = \frac{1}{z} - \frac{\pi^2}{3}z - \frac{\pi^4}{45}z^3 - \cdots $$ Thus, \begin{align} \frac{\pi\cot(\pi z) - \frac{1}{z}}{2z} &= \frac{- \frac{\pi^2}{3}z - \frac{\pi^4}{45}z^3-\cdots}{2z}\\ -\sum_{n=1}^{\infty}\frac{1}{n^2 - z^2}&= -\frac{\pi^2}{6} - \frac{\pi^4}{90}z^2 - \cdots\\ -\lim_{z\to 0}\sum_{n=1}^{\infty}\frac{1}{n^2 - z^2}&= \lim_{z\to 0}\Big(-\frac{\pi^2}{6} - \frac{\pi^4}{90}z^2 - \cdots\Big)\\ -\sum_{n=1}^{\infty}\frac{1}{n^2}&= -\frac{\pi^2}{6}\\ \sum_{n=1}^{\infty}\frac{1}{n^2}&= \frac{\pi^2}{6} \end{align}
+
+Elias
+See evaluations of Riemann Zeta Function $\zeta(2)=\sum_{n=1}^\infty\frac{1}{n^2}$ in mathworld.wolfram.com and a solution by in D. P. Giesy in Mathematics Magazine:
+D. P. Giesy, Still another elementary proof that $\sum_{n=1}^\infty \frac{1}{n^2}=\frac{\pi^2}{6}$, Math. Mag. 45 (1972) 148-149.
+Unfortunately I did not get a link to this article. But there is a link to a note from Robin Chapman seems to me a variation of proof's Giesy.
+
+barto
+Applying the usual trick 1 transforming a series to an integral, we obtain
+$$\sum_{n=1}^\infty\frac1{n^2}=\int_0^1\int_0^1\frac{dxdy}{1-xy}$$
+where we use the Monotone Convergence Theorem to integrate term-wise.
+Then there's this ingenious change of variables 2, which I learned from Don Zagier during a lecture, and which he in turn got from a colleague:
+$$(x,y)=\left(\frac{\cos v}{\cos u},\frac{\sin u}{\sin v}\right),\quad0\leq u\leq v\leq \frac\pi2$$
+One verifies that it is bijective between the rectangle $[0,1]^2$ and the triangle $0\leq u\leq v\leq \frac\pi2$, and that its Jacobian determinant is precisely $1-x^2y^2$, which means $\frac1{1-x^2y^2}$ would be a neater integrand. For the moment, we have found
+$$J=\int_0^1\int_0^1\frac{dxdy}{1-x^2y^2}=\frac{\pi^2}8$$ (the area of the triangular domain in the $(u,v)$ plane).
+There are two ways to transform $\int\frac1{1-xy}$ into something $\int\frac1{1-x^2y^2}$ish:
+Manipulate $S=\sum_{n=1}^\infty\frac1{n^2}$: We have $\sum_{n=1}^\infty\frac1{(2n)^2}=\frac14S$ so $\sum_{n=0}^\infty\frac1{(2n+1)^2}=\frac34S$. Applying the series-integral transformation, we get $\frac34S=J$ so $$S=\frac{\pi^2}6$$
+Manipulate $I=\int_0^1\int_0^1\frac{dxdy}{1-xy}$: Substituting $(x,y)\leftarrow(x^2,y^2)$ we have $I=\int_0^1\int_0^1\frac{4xydxdy}{1-x^2y^2}$ so $$J=\int_0^1\int_0^1\frac{dxdy}{1-x^2y^2}=\int_0^1\int_0^1\frac{(1+xy-xy)dxdy}{1-x^2y^2}=I-\frac14I$$ whence $$I=\frac43J=\frac{\pi^2}6$$
+(It may be seen that they are essentially the same methods.)
+After looking at the comments it seems that this looks a lot like Proof 2 in the article by R. Chapman.
+See also: Multiple Integral $\int\limits_0^1\!\!\int\limits_0^1\!\!\int\limits_0^1\!\!\int\limits_0^1\frac1{1-xyuv}\,dx\,dy\,du\,dv$
+1 See e.g. Proof 1 in Chapman's article.
+2 It may have been a different one; maybe as in the above article. Either way, the idea to do something trigonometric was not mine.
+
+Asier Calbet
+The sum can be written as the integral: $$\int_0^{\infty} \frac{x}{e^x-1} dx $$ This integral can be evaluated using a rectangular contour from 0 to $\infty$ to $\infty + \pi i$ to $ 0$ .
+
+John M. Campbell
+There is a simple way of proving that $\sum_{n=1}^{\infty}\frac{1}{n^2} = \frac{\pi^2}{6}$ using the following well-known series identity: $$\left(\sin^{-1}(x)\right)^{2} = \frac{1}{2}\sum_{n=1}^{\infty}\frac{(2x)^{2n}}{n^2 \binom{2n}{n}}.$$ From the above equality, we have that $$x^2 = \frac{1}{2}\sum_{n=1}^{\infty}\frac{(2 \sin(x))^{2n}}{n^2 \binom{2n}{n}},$$ and we thus have that: $$\int_{0}^{\pi} x^2 dx = \frac{\pi^3}{12} = \frac{1}{2}\sum_{n=1}^{\infty}\frac{\int_{0}^{\pi} (2 \sin(x))^{2n} dx}{n^2 \binom{2n}{n}}.$$ Since $$\int_{0}^{\pi} \left(\sin(x)\right)^{2n} dx = \frac{\sqrt{\pi} \ \Gamma\left(n + \frac{1}{2}\right)}{\Gamma(n+1)},$$ we thus have that: $$\frac{\pi^3}{12} = \frac{1}{2}\sum_{n=1}^{\infty}\frac{ 4^{n} \frac{\sqrt{\pi} \ \Gamma\left(n + \frac{1}{2}\right)}{\Gamma(n+1)} }{n^2 \binom{2n}{n}}.$$ Simplifying the summand, we have that $$\frac{\pi^3}{12} = \frac{1}{2}\sum_{n=1}^{\infty}\frac{\pi}{n^2},$$ and we thus have that $\sum_{n=1}^{\infty}\frac{1}{n^2} = \frac{\pi^2}{6}$ as desired.
+`
\ No newline at end of file
diff --git a/tpl/transform/transform.go b/tpl/transform/transform.go
index 5ef9bff21..380ea252b 100644
--- a/tpl/transform/transform.go
+++ b/tpl/transform/transform.go
@@ -17,17 +17,30 @@ package transform
import (
"bytes"
"context"
+ "encoding/json"
"encoding/xml"
+ "errors"
+ "fmt"
"html"
"html/template"
+ "io"
"strings"
+ "sync/atomic"
+
+ bp "github.com/gohugoio/hugo/bufferpool"
+
+ "github.com/bep/goportabletext"
"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 +55,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, template.HTML](
+ 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, template.HTML]
+
+ id atomic.Uint32
+ deps *deps.Deps
}
// Emojify returns a copy of s with all emoji codes replaced with actual emojis.
@@ -173,16 +194,133 @@ func (ns *Namespace) Markdownify(ctx context.Context, s any) (template.HTML, err
}
// Plainify returns a copy of s with all HTML tags removed.
-func (ns *Namespace) Plainify(s any) (string, error) {
+func (ns *Namespace) Plainify(s any) (template.HTML, error) {
ss, err := cast.ToStringE(s)
if err != nil {
return "", err
}
- return tpl.StripHTML(ss), nil
+ return template.HTML(tpl.StripHTML(ss)), nil
+}
+
+// PortableText converts the portable text in v to Markdown.
+// We may add more options in the future.
+func (ns *Namespace) PortableText(v any) (string, error) {
+ buf := bp.GetBuffer()
+ defer bp.PutBuffer(buf)
+ opts := goportabletext.ToMarkdownOptions{
+ Dst: buf,
+ Src: v,
+ }
+ if err := goportabletext.ToMarkdown(opts); err != nil {
+ return "", err
+ }
+ return buf.String(), 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) (template.HTML, 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",
+ MinRuleThickness: 0.04,
+ ErrorColor: "#cc0000",
+ ThrowOnError: true,
+ Strict: "error",
+ },
+ }
+
+ if len(args) > 1 {
+ if err := mapstructure.WeakDecode(args[1], &katexInput.Options); err != nil {
+ return "", err
+ }
+ }
+
+ switch katexInput.Options.Strict {
+ case "error", "ignore", "warn":
+ // Valid strict mode, continue
+ default:
+ return "", fmt.Errorf("invalid strict mode; expected one of error, ignore, or warn; received %s", katexInput.Options.Strict)
+ }
+
+ type fileCacheEntry struct {
+ Version string `json:"version"`
+ Output string `json:"output"`
+ Warnings []string `json:"warnings,omitempty"`
+ }
+
+ const fileCacheEntryVersion = "v1" // Increment on incompatible changes.
+
+ s := hashing.HashString(args...)
+ key := "tomath/" + fileCacheEntryVersion + "/" + s[:2] + "/" + s[2:]
+ fileCache := ns.deps.ResourceSpec.FileCaches.MiscCache()
+
+ 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{
+ Version: 1,
+ 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
+ }
+
+ e := fileCacheEntry{
+ Version: fileCacheEntryVersion,
+ Output: result.Data.Output,
+ Warnings: result.Header.Warnings,
+ }
+
+ buf := &bytes.Buffer{}
+ enc := json.NewEncoder(buf)
+ enc.SetEscapeHTML(false)
+ if err := enc.Encode(e); err != nil {
+ return nil, fmt.Errorf("failed to encode file cache entry: %w", err)
+ }
+ return hugio.NewReadSeekerNoOpCloserFromBytes(buf.Bytes()), nil
+ })
+ if err != nil {
+ return "", err
+ }
+
+ var e fileCacheEntry
+ if err := json.NewDecoder(r).Decode(&e); err != nil {
+ return "", fmt.Errorf("failed to decode file cache entry: %w", err)
+ }
+
+ for _, warning := range e.Warnings {
+ ns.deps.Log.Warnf("transform.ToMath: %s", warning)
+ }
+
+ return template.HTML(e.Output), err
+ })
+ if err != nil {
+ return "", err
+ }
+
+ return v, nil
}
// 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..8197b3e3d 100644
--- a/tpl/transform/transform_integration_test.go
+++ b/tpl/transform/transform_integration_test.go
@@ -14,6 +14,8 @@
package transform_test
import (
+ "fmt"
+ "strings"
"testing"
qt "github.com/frankban/quicktest"
@@ -133,3 +135,403 @@ 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) {
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- layouts/index.html --
+{{ transform.ToMath "c = \\pm\\sqrt{a^2 + b^2}" }}
+ `
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", `
+: error calling ToMath: KaTeX parse error: Undefined control sequence: \\foo at position 5: c = \\̲f̲o̲o̲{a^2 + b^2}")
+ })
+
+ // See issue 13239.
+ t.Run("Handle in template, old Err construct", func(t *testing.T) {
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- layouts/index.html --
+{{ with transform.ToMath "c = \\pm\\sqrt{a^2 + b^2}" }}
+ {{ with .Err }}
+ {{ warnf "error: %s" . }}
+ {{ else }}
+ {{ . }}
+ {{ end }}
+{{ end }}
+ `
+ b, err := hugolib.TestE(t, files, hugolib.TestOptWarn())
+
+ b.Assert(err, qt.IsNotNil)
+ b.Assert(err.Error(), qt.Contains, "the return type of transform.ToMath was changed in Hugo v0.141.0 and the error handling replaced with a new try keyword, see https://gohugo.io/functions/go-template/try/")
+ })
+}
+
+func TestToMathBigAndManyExpressions(t *testing.T) {
+ filesTemplate := `
+-- hugo.toml --
+disableKinds = ['rss','section','sitemap','taxonomy','term']
+[markup.goldmark.extensions.passthrough]
+enable = true
+[markup.goldmark.extensions.passthrough.delimiters]
+block = [['\[', '\]'], ['$$', '$$']]
+inline = [['\(', '\)'], ['$', '$']]
+-- content/p1.md --
+P1_CONTENT
+-- layouts/index.html --
+Home.
+-- layouts/_default/single.html --
+Content: {{ .Content }}|
+-- layouts/_default/_markup/render-passthrough.html --
+{{ $opts := dict "throwOnError" false "displayMode" true }}
+{{ transform.ToMath .Inner $opts }}
+ `
+
+ t.Run("Very large file with many complex KaTeX expressions", func(t *testing.T) {
+ files := strings.ReplaceAll(filesTemplate, "P1_CONTENT", "sourcefilename: testdata/large-katex.md")
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/p1/index.html", `
+ y
+ `)
+}
+
+// Issue #12977
+func TestUnmarshalWithIndentedYAML(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- layouts/index.html --
+{{ $yaml := "\n a:\n b: 1\n c:\n d: 2\n" }}
+{{ $yaml | transform.Unmarshal | encoding.Jsonify }}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileExists("public/index.html", true)
+ b.AssertFileContent("public/index.html", `{"a":{"b":1},"c":{"d":2}}`)
+}
+
+func TestPortableText(t *testing.T) {
+ files := `
+-- hugo.toml --
+-- assets/sample.json --
+[
+ {
+ "_key": "a",
+ "_type": "block",
+ "children": [
+ {
+ "_key": "b",
+ "_type": "span",
+ "marks": [],
+ "text": "Heading 2"
+ }
+ ],
+ "markDefs": [],
+ "style": "h2"
+ }
+]
+-- layouts/index.html --
+{{ $markdown := resources.Get "sample.json" | transform.Unmarshal | transform.PortableText }}
+Markdown: {{ $markdown }}|
+
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Markdown: ## Heading 2\n|")
+}
+
+func TestUnmarshalCSV(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- layouts/all.html --
+{{ $opts := OPTS }}
+{{ with resources.Get "pets.csv" | transform.Unmarshal $opts }}
+ {{ jsonify . }}
+{{ end }}
+-- assets/pets.csv --
+DATA
+`
+
+ // targetType = map
+ f := strings.ReplaceAll(files, "OPTS", `dict "targetType" "map"`)
+ f = strings.ReplaceAll(f, "DATA",
+ "name,type,breed,age\nSpot,dog,Collie,3\nFelix,cat,Malicious,7",
+ )
+ b := hugolib.Test(t, f)
+ b.AssertFileContent("public/index.html",
+ `[{"age":"3","breed":"Collie","name":"Spot","type":"dog"},{"age":"7","breed":"Malicious","name":"Felix","type":"cat"}]`,
+ )
+
+ // targetType = map (no data)
+ f = strings.ReplaceAll(files, "OPTS", `dict "targetType" "map"`)
+ f = strings.ReplaceAll(f, "DATA", "")
+ b = hugolib.Test(t, f)
+ b.AssertFileContent("public/index.html", "")
+
+ // targetType = slice
+ f = strings.ReplaceAll(files, "OPTS", `dict "targetType" "slice"`)
+ f = strings.ReplaceAll(f, "DATA",
+ "name,type,breed,age\nSpot,dog,Collie,3\nFelix,cat,Malicious,7",
+ )
+ b = hugolib.Test(t, f)
+ b.AssertFileContent("public/index.html",
+ `[["name","type","breed","age"],["Spot","dog","Collie","3"],["Felix","cat","Malicious","7"]]`,
+ )
+
+ // targetType = slice (no data)
+ f = strings.ReplaceAll(files, "OPTS", `dict "targetType" "slice"`)
+ f = strings.ReplaceAll(f, "DATA", "")
+ b = hugolib.Test(t, f)
+ b.AssertFileContent("public/index.html", "")
+
+ // targetType not specified
+ f = strings.ReplaceAll(files, "OPTS", "dict")
+ f = strings.ReplaceAll(f, "DATA",
+ "name,type,breed,age\nSpot,dog,Collie,3\nFelix,cat,Malicious,7",
+ )
+ b = hugolib.Test(t, f)
+ b.AssertFileContent("public/index.html",
+ `[["name","type","breed","age"],["Spot","dog","Collie","3"],["Felix","cat","Malicious","7"]]`,
+ )
+
+ // targetType not specified (no data)
+ f = strings.ReplaceAll(files, "OPTS", "dict")
+ f = strings.ReplaceAll(f, "DATA", "")
+ b = hugolib.Test(t, f)
+ b.AssertFileContent("public/index.html", "")
+
+ // targetType = foo
+ f = strings.ReplaceAll(files, "OPTS", `dict "targetType" "foo"`)
+ _, err := hugolib.TestE(t, f)
+ if err == nil {
+ t.Errorf("expected error")
+ } else {
+ if !strings.Contains(err.Error(), `invalid targetType: expected either slice or map, received foo`) {
+ t.Log(err.Error())
+ t.Errorf("error message does not match expected error message")
+ }
+ }
+
+ // targetType = foo (no data)
+ f = strings.ReplaceAll(files, "OPTS", `dict "targetType" "foo"`)
+ f = strings.ReplaceAll(f, "DATA", "")
+ _, err = hugolib.TestE(t, f)
+ if err == nil {
+ t.Errorf("expected error")
+ } else {
+ if !strings.Contains(err.Error(), `invalid targetType: expected either slice or map, received foo`) {
+ t.Log(err.Error())
+ t.Errorf("error message does not match expected error message")
+ }
+ }
+
+ // targetType = map (error: expected at least a header row and one data row)
+ f = strings.ReplaceAll(files, "OPTS", `dict "targetType" "map"`)
+ _, err = hugolib.TestE(t, f)
+ if err == nil {
+ t.Errorf("expected error")
+ } else {
+ if !strings.Contains(err.Error(), `expected at least a header row and one data row`) {
+ t.Log(err.Error())
+ t.Errorf("error message does not match expected error message")
+ }
+ }
+
+ // targetType = map (error: header row contains duplicate field names)
+ f = strings.ReplaceAll(files, "OPTS", `dict "targetType" "map"`)
+ f = strings.ReplaceAll(f, "DATA",
+ "name,name,breed,age\nSpot,dog,Collie,3\nFelix,cat,Malicious,7",
+ )
+ _, err = hugolib.TestE(t, f)
+ if err == nil {
+ t.Errorf("expected error")
+ } else {
+ if !strings.Contains(err.Error(), `header row contains duplicate field names`) {
+ t.Log(err.Error())
+ t.Errorf("error message does not match expected error message")
+ }
+ }
+}
+
+// Issue 13729
+func TestToMathStrictMode(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- layouts/all.html --
+{{ transform.ToMath "a %" dict }}
+-- foo --
+`
+
+ // strict mode: default
+ f := strings.ReplaceAll(files, "dict", "")
+ b, err := hugolib.TestE(t, f)
+ b.Assert(err.Error(), qt.Contains, "[commentAtEnd]")
+
+ // strict mode: error
+ f = strings.ReplaceAll(files, "dict", `(dict "strict" "error")`)
+ b, err = hugolib.TestE(t, f)
+ b.Assert(err.Error(), qt.Contains, "[commentAtEnd]")
+
+ // strict mode: ignore
+ f = strings.ReplaceAll(files, "dict", `(dict "strict" "ignore")`)
+ b = hugolib.Test(t, f, hugolib.TestOptWarn())
+ b.AssertLogMatches("")
+ b.AssertFileContent("public/index.html", `a % `)
+
+ // strict: warn
+ f = strings.ReplaceAll(files, "dict", `(dict "strict" "warn")`)
+ b = hugolib.Test(t, f, hugolib.TestOptWarn())
+ b.AssertLogMatches("[commentAtEnd]")
+ b.AssertFileContent("public/index.html", `a % `)
+
+ // strict mode: invalid value
+ f = strings.ReplaceAll(files, "dict", `(dict "strict" "foo")`)
+ b, err = hugolib.TestE(t, f)
+ b.Assert(err.Error(), qt.Contains, "invalid strict mode")
+}
diff --git a/tpl/transform/transform_test.go b/tpl/transform/transform_test.go
index d645ca8e2..1f6c99ec4 100644
--- a/tpl/transform/transform_test.go
+++ b/tpl/transform/transform_test.go
@@ -244,6 +244,6 @@ func TestPlainify(t *testing.T) {
}
b.Assert(err, qt.IsNil)
- b.Assert(result, qt.Equals, test.expect)
+ b.Assert(result, qt.Equals, template.HTML(test.expect.(string)))
}
}
diff --git a/tpl/transform/unmarshal.go b/tpl/transform/unmarshal.go
index dc9029c8d..a3a9c040a 100644
--- a/tpl/transform/unmarshal.go
+++ b/tpl/transform/unmarshal.go
@@ -22,11 +22,11 @@ import (
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource"
+ "github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/types"
"github.com/mitchellh/mapstructure"
- "github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/spf13/cast"
@@ -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())
@@ -113,13 +113,13 @@ func (ns *Namespace) Unmarshal(args ...any) (any, error) {
return nil, fmt.Errorf("type %T not supported", data)
}
- if dataStr == "" {
- return nil, errors.New("no data to transform")
+ if strings.TrimSpace(dataStr) == "" {
+ return nil, nil
}
- key := helpers.MD5String(dataStr)
+ 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")
diff --git a/tpl/transform/unmarshal_test.go b/tpl/transform/unmarshal_test.go
index 1b976c449..9b34e1daa 100644
--- a/tpl/transform/unmarshal_test.go
+++ b/tpl/transform/unmarshal_test.go
@@ -139,6 +139,8 @@ func TestUnmarshal(t *testing.T) {
a;b;c`, mime: media.Builtin.CSVType}, map[string]any{"DElimiter": ";", "Comment": "%"}, func(r [][]string) {
b.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r)
}},
+ {``, nil, nil},
+ {` `, nil, nil},
// errors
{"thisisnotavaliddataformat", nil, false},
{testContentResource{key: "r1", content: `invalid&toml"`, mime: media.Builtin.TOMLType}, nil, false},
@@ -190,7 +192,7 @@ func BenchmarkUnmarshalString(b *testing.B) {
const numJsons = 100
var jsons [numJsons]string
- for i := 0; i < numJsons; i++ {
+ for i := range numJsons {
jsons[i] = strings.Replace(testJSON, "ROOT_KEY", fmt.Sprintf("root%d", i), 1)
}
@@ -218,7 +220,7 @@ func BenchmarkUnmarshalResource(b *testing.B) {
const numJsons = 100
var jsons [numJsons]testContentResource
- for i := 0; i < numJsons; i++ {
+ for i := range numJsons {
key := fmt.Sprintf("root%d", i)
jsons[i] = testContentResource{key: key, content: strings.Replace(testJSON, "ROOT_KEY", key, 1), mime: media.Builtin.JSONType}
}
diff --git a/transform/livereloadinject/livereloadinject.go b/transform/livereloadinject/livereloadinject.go
index e88e3895b..425d268b3 100644
--- a/transform/livereloadinject/livereloadinject.go
+++ b/transform/livereloadinject/livereloadinject.go
@@ -56,7 +56,7 @@ func New(baseURL *url.URL) transform.Transformer {
src += "&port=" + baseURL.Port()
src += "&path=" + strings.TrimPrefix(path+"/livereload", "/")
- script := []byte(fmt.Sprintf(``, html.EscapeString(src)))
+ script := fmt.Appendf(nil, ``, html.EscapeString(src))
c := make([]byte, len(b)+len(script))
copy(c, b[:idx])
diff --git a/transform/metainject/hugogenerator.go b/transform/metainject/hugogenerator.go
index 43a477354..b3dda9b15 100644
--- a/transform/metainject/hugogenerator.go
+++ b/transform/metainject/hugogenerator.go
@@ -39,12 +39,12 @@ func HugoGenerator(ft transform.FromTo) error {
}
head := ""
- replace := []byte(fmt.Sprintf("%s\n\t%s", head, hugoGeneratorTag))
+ replace := fmt.Appendf(nil, "%s\n\t%s", head, hugoGeneratorTag)
newcontent := bytes.Replace(b, []byte(head), replace, 1)
if len(newcontent) == len(b) {
head := ""
- replace := []byte(fmt.Sprintf("%s\n\t%s", head, hugoGeneratorTag))
+ replace := fmt.Appendf(nil, "%s\n\t%s", head, hugoGeneratorTag)
newcontent = bytes.Replace(b, []byte(head), replace, 1)
}
diff --git a/transform/urlreplacers/absurl.go b/transform/urlreplacers/absurl.go
index 029d94da2..17fe15327 100644
--- a/transform/urlreplacers/absurl.go
+++ b/transform/urlreplacers/absurl.go
@@ -13,7 +13,9 @@
package urlreplacers
-import "github.com/gohugoio/hugo/transform"
+import (
+ "github.com/gohugoio/hugo/transform"
+)
var ar = newAbsURLReplacer()
diff --git a/transform/urlreplacers/absurlreplacer.go b/transform/urlreplacers/absurlreplacer.go
index a875e6fa8..601fd9a1f 100644
--- a/transform/urlreplacers/absurlreplacer.go
+++ b/transform/urlreplacers/absurlreplacer.go
@@ -16,9 +16,11 @@ package urlreplacers
import (
"bytes"
"io"
+ "net/url"
"unicode"
"unicode/utf8"
+ "github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/transform"
)
@@ -31,6 +33,9 @@ type absurllexer struct {
// path may be set to a "." relative path
path []byte
+ // The root path, without leading slash.
+ root []byte
+
pos int // input position
start int // item start position
@@ -119,6 +124,9 @@ func checkCandidateBase(l *absurllexer) {
}
l.pos += relURLPrefixLen
l.w.Write(l.path)
+ if len(l.root) > 0 && bytes.HasPrefix(l.content[l.pos:], l.root) {
+ l.pos += len(l.root)
+ }
l.start = l.pos
}
@@ -174,7 +182,11 @@ func checkCandidateSrcset(l *absurllexer) {
for i, f := range fields {
if f[0] == '/' {
l.w.Write(l.path)
- l.w.Write(f[1:])
+ n := 1
+ if len(l.root) > 0 && bytes.HasPrefix(f[n:], l.root) {
+ n += len(l.root)
+ }
+ l.w.Write(f[n:])
} else {
l.w.Write(f)
@@ -229,10 +241,15 @@ func (l *absurllexer) replace() {
}
func doReplace(path string, ct transform.FromTo, quotes [][]byte) {
+ var root string
+ if u, err := url.Parse(path); err == nil {
+ root = paths.TrimLeading(u.Path)
+ }
lexer := &absurllexer{
content: ct.From().Bytes(),
w: ct.To(),
path: []byte(path),
+ root: []byte(root),
quotes: quotes,
}
diff --git a/watcher/filenotify/poller_test.go b/watcher/filenotify/poller_test.go
index 9b52b9780..77feb459d 100644
--- a/watcher/filenotify/poller_test.go
+++ b/watcher/filenotify/poller_test.go
@@ -220,11 +220,11 @@ func prepareTestDirWithSomeFiles(c *qt.C, id string) string {
c.Assert(os.MkdirAll(filepath.Join(dir, subdir1), 0o777), qt.IsNil)
c.Assert(os.MkdirAll(filepath.Join(dir, subdir2), 0o777), qt.IsNil)
- for i := 0; i < 3; i++ {
+ for i := range 3 {
c.Assert(os.WriteFile(filepath.Join(dir, subdir1, fmt.Sprintf("file%d", i)), []byte("hello1"), 0o600), qt.IsNil)
}
- for i := 0; i < 3; i++ {
+ for i := range 3 {
c.Assert(os.WriteFile(filepath.Join(dir, subdir2, fmt.Sprintf("file%d", i)), []byte("hello2"), 0o600), qt.IsNil)
}
diff --git a/watchtestscripts.sh b/watchtestscripts.sh
index bf61d0cc3..5c6f90009 100755
--- a/watchtestscripts.sh
+++ b/watchtestscripts.sh
@@ -3,5 +3,5 @@
trap exit SIGINT
# I use "run tests on save" in my editor.
-# Unfortunately, changes to text files does not trigger this. Hence this workaround.
-while true; do find testscripts -type f -name "*.txt" | entr -pd touch main_test.go; done
\ No newline at end of file
+# Unfortunately, changes to text files do not trigger this. Hence this workaround.
+while true; do find testscripts -type f -name "*.txt" | entr -pd touch main_test.go; done