domma-cms 1.6.32 → 1.6.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/js/lib/effect-defs.js +1 -1
- package/admin/js/lib/effects-builder.js +2 -2
- package/admin/js/lib/favicon-generator.js +1 -0
- package/admin/js/lib/markdown-toolbar.js +9 -9
- package/admin/js/lib/page-comments.js +9 -0
- package/admin/js/lib/page-editor-reference.js +1 -1
- package/admin/js/lib/scribe-composer.js +1 -1
- package/admin/js/lib/scribe-model.js +1 -1
- package/admin/js/lib/shortcode-preview.js +1 -1
- package/admin/js/lib/shortcode-registry.js +2 -2
- package/admin/js/lib/site-settings-arrange.js +1 -1
- package/admin/js/lib/syntax-highlight.js +1 -1
- package/admin/js/templates/settings.html +25 -7
- package/admin/js/views/settings.js +4 -4
- package/package.json +1 -1
- package/plugins/free-tier.lock.json +13 -13
- package/plugins/shopping-cart/CLAUDE.md +19 -0
- package/plugins/shopping-cart/admin/views/orders.js +2 -2
- package/plugins/shopping-cart/admin/views/shop.js +1 -1
- package/plugins/shopping-cart/lib/pricing.js +65 -13
- package/plugins/shopping-cart/lib/render.js +4 -3
- package/plugins/shopping-cart/lib/shop.js +11 -3
- package/plugins/shopping-cart/lib/summary.js +4 -2
- package/plugins/shopping-cart/plugin.js +46 -21
- package/plugins/shopping-cart/plugin.json +2 -2
- package/plugins/shopping-cart/tests/api.test.js +52 -0
- package/plugins/shopping-cart/tests/pure.test.js +30 -1
- package/public/css/site.css +1 -1
- package/public/js/effects.js +1 -1
- package/public/js/scribe-style.mjs +1 -0
- package/public/js/shortcode-expiry.mjs +1 -0
- package/public/js/site.js +1 -1
- package/scripts/run-tests.mjs +4 -0
- package/server/routes/api/pages.js +3 -3
- package/server/routes/api/settings.js +20 -1
- package/server/routes/api/versions.js +4 -3
- package/server/routes/public.js +24 -1
- package/server/services/content.js +7 -7
- package/server/services/favicon.js +33 -0
- package/server/services/markdown.js +24 -7
- package/server/services/shortcodeExpiry.js +146 -0
- package/server/services/versions.js +10 -0
- package/server/templates/page.html +4 -4
package/public/js/site.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{decodeBase64Json as P}from"/public/js/utf8-base64.mjs";import{makeBadgeEl as Y,applyColour as J,applyPill as te,applyFloat as oe,applyHighlight as R,applyItemHighlight as ne,itemHighlightAttrs as ae,isCurrentItem as X,menuAlign as se,applyClasses as O,cssClasses as W}from"/public/js/menu-decor.mjs?v=20261009-hover-darken";import{initFloatingMenus as ie}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as U,markInvalidFields as G,showValidationPopup as K}from"/public/js/forms.js?v=20261009-form-classes";import{buildWizardSteps as re}from"/public/js/form-steps.mjs";import{applyFormClasses as Q,applySubmitButtons as ce,applyWizardButtons as le}from"/public/js/form-submit.mjs";document.addEventListener("click",p=>{const c=p.target?.closest?.(".dm-banner__dismiss");c&&(p.preventDefault(),c.closest(".dm-banner")?.remove())}),$(()=>{const p=window.__CMS_NAV__||{},c=window.__CMS_SITE__||{};if(c.autoTheme?.enabled){let t=function(e){const a=(e||"07:00").split(":");return+a[0]*60+(+a[1]||0)},s=function(){const e=new Date,a=e.getHours()*60+e.getMinutes();return a>=t(n.dayStart)&&a<t(n.nightStart)?n.dayTheme:n.nightTheme},o=function(e){const a=n.apply&&n.apply[e]||{theme:e,custom:""};Domma.theme.set(a.theme);for(const f of[...document.body.classList])f.startsWith("dm-custom-")&&document.body.classList.remove(f);a.custom&&document.body.classList.add(a.custom)};const n=c.autoTheme;o(s()),setInterval(()=>o(s()),6e4)}const x=$("#site-navbar");if(x.length&&p.brand){let s=function(l,d=0){return Array.isArray(l)?l.filter(m=>!(m&&(m.type==="spacer"||m.type==="theme-switcher"))).filter(m=>!(m&&m.type==="separator"&&d===0)).map(m=>m&&m.type==="separator"?{divider:!0}:Array.isArray(m?.items)?{...m,items:s(m.items,d+1)}:m):l},o=function(l){return Array.isArray(l)?l.filter(d=>!(d&&(d.type==="separator"||d.type==="spacer"||d.type==="theme-switcher"))):[]};const n={...p.brand},t=n.size&&n.size!=="md"?` navbar-brand-${n.size}`:"";if(n.logo||n.icon||n.tagline){let l="";n.logo?l+=`<img src="${n.logo}" class="navbar-brand-logo" alt="${n.text||""}">`:n.icon&&(l+=`<span data-icon="${n.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),n.text&&(l+=`<span class="navbar-brand-text${t}">${n.text}</span>`),n.tagline&&(l+=`<small class="navbar-brand-tagline">${n.tagline}</small>`),n.html=l}else t&&n.text&&(n.html=`<span class="navbar-brand-text${t}">${n.text}</span>`);const e=p.position==="floating",a=p.orientation==="vertical";Domma.elements.navbar("#site-navbar",{brand:n,items:s(p.items||[]),variant:p.variant||"dark",position:e?"static":p.position||"sticky",collapsible:!0,appearOnHover:!!p.appearOnHover}),Domma.icons.scan("#site-navbar");const f=x.get(0);f.classList.add(`dm-nav-align-${se(p)||"centre"}`),a&&x.get(0).classList.add("dm-nav-vertical",p.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left");const v=p.dock||{};a&&!e&&de(f,p.side==="right"),!a&&v.hideOnScroll&&me(f),R(f,p),O(f,p.class),e&&(oe(x.get(0),p),ie(x.get(0).parentNode||document));const i=f.querySelector(".navbar-toggle"),u=f.querySelector(".navbar-collapse");if(i&&u){let l=null;const d=()=>{u.classList.contains("show")&&i.click()},m=()=>{l&&(l.remove(),l=null),document.body.classList.remove("dm-drawer-open")},g=()=>{l||(l=document.createElement("div"),l.className="dm-navbar-backdrop",l.addEventListener("click",d),document.body.appendChild(l),document.body.classList.add("dm-drawer-open"))};i.addEventListener("click",()=>{requestAnimationFrame(()=>{u.classList.contains("show")?g():m()})}),u.addEventListener("click",C=>{C.target.closest("a")&&requestAnimationFrame(m)}),document.addEventListener("keydown",C=>{C.key==="Escape"&&u.classList.contains("show")&&d()})}const h=f.querySelector(".navbar-nav");if(h){const l=Array.from(h.children);let d=0;for(const m of p.items||[])if(!(m&&m.type==="separator")){if(m&&m.type==="spacer"){const g=document.createElement("li"),C=["xs","sm","md","lg","xl","flex"].includes(m.size)?m.size:"md";g.className=`dm-menu-spacer dm-menu-spacer--${C}`,g.setAttribute("aria-hidden","true"),h.insertBefore(g,l[d]||null);continue}if(m&&m.type==="theme-switcher"){const g=document.createElement("li");g.className="dm-menu-theme-switcher";const C=document.createElement("span");C.dataset.themeSwitcher="menu",C.dataset.position="inline",g.appendChild(C),h.insertBefore(g,l[d]||null);continue}d++}window.CMSThemeSwitcher?.scan(h)}const b=o(p.items||[]);f.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(l=>{const d=b[parseInt(l.dataset.index,10)];if(!d)return;const m=l.dataset.subindex!=null,g=m&&Array.isArray(d.items)?d.items[parseInt(l.dataset.subindex,10)]:d;if(!g)return;g.colour&&J(l,g.colour),g.pill&&!m&&te(l,g.pill),ne(l,g),O(l.closest("li"),g.class);const C=Y(g.badge);C&&l.appendChild(C),X(g.url,location.pathname)&&(l.classList.add("active"),l.setAttribute("aria-current","page"))})}const y=$("#site-footer");if(y.length){let e=function(r){return String(r??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},a=function(r){return e(r).replace(/'/g,"'")},f=function(r){if(r&&r.type==="separator")return'<li class="footer-sep"><hr></li>';if(r&&r.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(r.size)?r.size:"md"}" aria-hidden="true"></li>`;if(r&&r.type==="theme-switcher")return'<li class="dm-menu-theme-switcher"><span data-theme-switcher="menu" data-position="inline"></span></li>';const A=a(r.url||"#"),k=e(r.text||""),q=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(f).join("")}</ul>`:"",N=ae(r),Z=[N.hover&&` data-menu-item-hover="${a(N.hover)}"`,N.active&&` data-menu-item-active="${a(N.active)}"`].filter(Boolean).join(""),ee=N.accent?` style="--dm-menu-highlight:${a(N.accent)}"`:"",V=W(r.class).join(" ");return`<li${V?` class="${a(V)}"`:""}><a href="${A}"${Z}${ee}>${k}</a>${q}</li>`},v=function(){if(!c.footer)return"";const r=Object.keys(t).filter(k=>n[k]);if(!r.length)return"";const A=W(c.footer.socialClass).join(" ");return`<div class="footer-social">${r.map(k=>{const{label:q,svg:N}=t[k];return`<a href="${n[k]}" target="_blank" rel="noopener noreferrer" aria-label="${q}" class="footer-social-link${A?` ${a(A)}`:""}">${N}</a>`}).join("")}</div>`},b=function(r){if(!r||r.type==="separator"||r.type==="spacer")return"";if(r.type==="theme-switcher")return`<li class="cms-footer-col">${f(r).replace(/^<li[^>]*>|<\/li>$/g,"")}</li>`;const A=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(f).join("")}</ul>`:"",k=h(r)?`<span class="cms-footer-col-title">${e(r.text||"")}</span>`:`<a class="cms-footer-col-title" href="${a(r.url||"#")}">${e(r.text||"")}</a>`,q=["cms-footer-col",...W(r.class)].join(" ");return`<li class="${a(q)}">${k}${A}</li>`},d=function(r,A,k=!1){return(r||[]).forEach(q=>{q&&(q.type==="separator"||q.type==="spacer"||q.type==="theme-switcher")||(k&&h(q)||A.push(q),Array.isArray(q.items)&&q.items.length&&d(q.items,A))}),A};const n=c.social||{},t={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let s='<div class="footer-inner container">';const o=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""},i=["columns","row"].includes(c.footer?.layout)?c.footer.layout:"auto",u=i==="columns"||i==="auto"&&o.primary.some(r=>Array.isArray(r?.items)&&r.items.length),h=r=>u&&!r.url&&Array.isArray(r.items)&&r.items.length>0;if(u){const r=p.brand||{},A=r.text||c.title||"",k=r.logo?`<img src="${a(r.logo)}" class="cms-footer-brand-logo" alt="">`:r.icon?'<span class="cms-footer-brand-icon"></span>':"",q=String(c.footer?.blurb||"").trim();s='<div class="footer-inner footer-inner--columns container"><div class="cms-footer-top">',s+=`<div class="cms-footer-brand">${A||k?`<a class="cms-footer-brand-name" href="${a(r.url||"/")}">${k}<span>${e(A)}</span></a>`:""}${q?`<p class="cms-footer-blurb">${e(q)}</p>`:""}${v()}</div>`,o.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary footer-nav--columns" aria-label="Footer"><ul>${o.primary.map(b).join("")}</ul></nav>`),s+='</div><div class="cms-footer-bottom">',o.copyright&&(s+=`<p class="cms-footer-copyright">${e(o.copyright)}</p>`),o.legal.length&&(s+=`<nav class="footer-nav footer-nav--legal" aria-label="Legal"><ul>${o.legal.map(f).join("")}</ul></nav>`),s+="</div>"}else o.copyright&&(s+=`<p>${e(o.copyright)}</p>`),o.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary"><ul>${o.primary.map(f).join("")}</ul></nav>`),o.legal.length&&(s+=`<nav class="footer-nav footer-nav--legal"><ul>${o.legal.map(f).join("")}</ul></nav>`),s+=v();s+="</div>",y.html(s),window.CMSThemeSwitcher?.scan(y.get(0));const l=y.get(0).querySelector(".cms-footer-brand-icon");l&&p.brand?.icon&&l.setAttribute("data-icon",p.brand.icon),O(y.get(0),c.footer?.class);const m=[...d(o.primary,[],!0),...d(o.legal,[])];y.get(0).querySelectorAll(".footer-nav a").forEach((r,A)=>{const k=m[A];if(!k)return;k.colour&&J(r,k.colour);const q=Y(k.badge);q&&r.appendChild(q),X(k.url,location.pathname)&&(r.classList.add("is-active"),r.setAttribute("aria-current","page"))}),R(y.get(0).querySelector(".footer-nav--primary"),{highlight:o.primaryHighlight}),R(y.get(0).querySelector(".footer-nav--legal"),{highlight:o.legalHighlight}),O(y.get(0).querySelector(".footer-nav--primary"),o.primaryClass),O(y.get(0).querySelector(".footer-nav--legal"),o.legalClass);const C=c.footer?.showMotionToggle!==!1,M=S.get("reduced_motion"),D=c.footer?.motionDefault,z=M!=null?!!M:D==="on"?!0:D==="off"?!1:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),B=c.footer?.motionControl==="checkbox",I=y.get(0).querySelector(".cms-footer-bottom")||y.get(0).querySelector(".footer-inner");if(C&&I){const r=document.createElement("input");r.type="checkbox",r.className=B?"footer-motion-checkbox":"form-switch-input",r.id="dm-motion-switch",r.checked=z,r.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const A=document.createElement("span");A.className=B?"footer-motion-label":"form-switch-label",A.textContent="Reduce motion";const k=document.createElement("label");k.className=B?"footer-motion-check":"form-switch footer-motion-switch",k.title="Reduce motion",k.appendChild(r),k.appendChild(A),I.appendChild(k)}}const L=document.getElementById("site-sidebar");L&&ue(L),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const w=$(".page-body");if(w.length){w.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),w.find(".tabs").each(function(){Domma.elements.tabs(this)}),w.find(".carousel").each(function(){const n={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const t=parseInt(this.dataset.animationDuration,10);Number.isFinite(t)&&(n.animationDuration=t)}this.dataset.animationEasing&&(n.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(t=>t.cloneNode(!0)),this._dmCarouselOpts=n),this._dmCarousel=Domma.elements.carousel(this,n)}),w.find(".dm-countdown").each(function(){const n={autoStart:!0},t=String(this.dataset.format||"").trim(),s=String(this.dataset.to||"").trim(),o=/^(\d{4})-(\d{2})-(\d{2})$/.exec(s),e=o?new Date(+o[1],+o[2]-1,+o[3]).getTime():s?new Date(s).getTime():NaN;Number.isFinite(e)?n.duration=Math.max(0,e-Date.now()):this.dataset.duration&&(n.duration=(parseInt(this.dataset.duration,10)||0)*1e3);const a=/^dd:/i.test(t);n.format=a?"hh:mm:ss":t||(n.duration>=36e5?"hh:mm:ss":"mm:ss");const f=this,v=u=>String(u).padStart(2,"0"),i=u=>{const h=Math.max(0,Math.floor(u/1e3)),b=f.querySelector(".dm-timer-time");b&&(b.textContent=`${v(Math.floor(h/86400))}:${v(Math.floor(h%86400/3600))}:${v(Math.floor(h%3600/60))}:${v(h%60)}`)};a&&(n.onTick=i),Domma.elements.timer(this,n),a&&i(n.duration||0)}),w.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),w.find(".dm-progression").each(function(){const n=Array.from(this.querySelectorAll(".dm-progression-item")).map(o=>{const e={title:o.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:o.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return o.dataset.date&&(e.date=o.dataset.date),o.dataset.status&&(e.status=o.dataset.status),o.dataset.entryId&&(e._entryId=o.dataset.entryId),e}),t=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",s=Domma.elements.progression(this,{layout:t,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...n.length?{items:n}:{}});n.length&&(this._dmCtx={instance:s,items:n})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(n=>{const t=n.dataset.revealMode||"stagger",s=n.dataset.revealAnimation||"slide-up",o=parseInt(n.dataset.revealDuration,10)||400,e=parseInt(n.dataset.revealStagger,10)||60,a=parseInt(n.dataset.revealDelay,10)||0,f=n.dataset.revealDirection==="rtl",v=Array.from(n.children),i={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};v.forEach((u,h)=>{u.style.opacity="0",u.style.transform=i[s]||"",u.style.transition=`opacity ${o}ms ease, transform ${o}ms ease`;const b=f?v.length-1-h:h;u.style.transitionDelay=t==="stagger"?`${a+b*e}ms`:`${a}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const u=new IntersectionObserver(h=>{h.forEach(b=>{b.isIntersecting&&(b.target.offsetWidth,b.target.style.opacity="1",b.target.style.transform="none",u.unobserve(b.target))})},{threshold:.1});v.forEach(h=>u.observe(h))}))}),w.find(".card[data-collapsible]").each(function(){const n=this.querySelector(".card-header");n&&n.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),w.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const n=this.dataset.soTarget,t=document.getElementById(n);if(!t)return;const s=E.slideover({title:t.dataset.soTitle||"",size:t.dataset.soSize||"md",position:t.dataset.soPosition||"right"});t.style.display="",s.setContent(t),s.open()})})}if(typeof $.setup=="function"){const n=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(t=>{try{const s=P(t.dataset.config);Object.assign(n,s)}catch{}}),Object.keys(n).length>0){const t={};for(const[s,o]of Object.entries(n)){const e=o?.events?.click,{confirm:a,toast:f,alert:v,prompt:i,...u}=e||{};a||f||v||i?($(s).on("click",async function(b){if(b.preventDefault(),a&&!await E.confirm(a))return;let l=null;if(!(i&&(l=await E.prompt(i,{inputPlaceholder:u.promptPlaceholder||"",inputValue:u.promptDefault||""}),l===null))){if(u.target){const d=$(u.target);u.toggleClass&&d.toggleClass(u.toggleClass),u.addClass&&d.addClass(u.addClass),u.removeClass&&d.removeClass(u.removeClass),l!==null&&(u.setText&&d.text(l),u.setVal&&d.val(l),u.setAttr&&d.attr(u.setAttr,l))}u.href&&(window.location.href=u.href),f&&E.toast(f,{type:u.toastType||"success"}),v&&E.alert(v)}}),Object.keys(u).length&&(t[s]={...o,events:{...o.events,click:u}})):t[s]=o}$.setup(t)}}w.length&&j(w.get(0))}),window.wireCTAButtons=j;function j(p){p.querySelectorAll(".dm-cta-trigger").forEach(c=>{c.__ctaWired||(c.__ctaWired=!0,c.addEventListener("click",async()=>{const x=c.dataset.action,y=c.dataset.entry,L=c.dataset.confirm;let w=S.get("auth_token");if(!w){E.toast("Please log in to perform this action.",{type:"warning"});return}if(L&&!await E.confirm(L))return;const n=Array.from(c.childNodes).map(s=>s.cloneNode(!0));c.disabled=!0,c.textContent="Running\u2026";const t=s=>fetch(`/api/actions/${x}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify(c.dataset.scope?{entryId:y,scope:c.dataset.scope}:{entryId:y})});try{let s=await t(w);if(s.status===401){const e=S.get("auth_refresh_token");if(e){const a=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(a.ok){const{token:f}=await a.json();S.set("auth_token",f),w=f,s=await t(w)}}}const o=await s.json().catch(()=>({}));if(!s.ok||o.success===!1)throw new Error(o.error||o.message||`Error ${s.status}`);E.toast(o.message||"Action completed.",{type:"success"})}catch(s){E.toast(s.message||"Action failed.",{type:"error"})}finally{c.disabled=!1,c.textContent="",n.forEach(s=>c.appendChild(s)),Domma.icons.scan(c)}}))})}(function(){const c=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!c.length)return;const x=S.get("auth_token");function y(w,n){const t=document.createElement("div");t.className="dm-collection-display dm-collection-empty";const s=document.createElement("p");if(n){s.appendChild(document.createTextNode("Please "));const o=document.createElement("a");o.href="/login",o.textContent="sign in",s.appendChild(o),s.appendChild(document.createTextNode(" to view your entries."))}else s.textContent=w;return t.appendChild(s),t}function L(w,n){const t=document.createElement("template");t.innerHTML=n||"",w.replaceWith(t.content)}c.forEach(async w=>{const n=w.dataset.collectionAttrs;if(!n)return;if(!x){w.replaceChildren(y("",!0));return}const t=s=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({attrs:n})});try{let s=x,o=await t(s);if(o.status===401){const a=S.get("auth_refresh_token");if(a){const f=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:a})});if(f.ok){const{token:v}=await f.json();S.set("auth_token",v),s=v,o=await t(s)}}}if(!o.ok){const{error:a}=await o.json().catch(()=>({}));w.replaceChildren(y(a||"Unable to load entries.",!1));return}const{html:e}=await o.json();L(w,e),window.DommaCollectionBrowser?.hydrate&&window.DommaCollectionBrowser.hydrate(document)}catch{w.replaceChildren(y("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),j(document.body)},100)})(),(function(){const c=document.querySelectorAll("[data-collection-table]");!c.length||typeof T>"u"||!T.create||c.forEach(x=>{let y;try{y=P(x.dataset.payload)}catch{return}const{columns:L,rows:w,search:n,sortable:t,exportable:s,pageSize:o,empty:e,ctaConfig:a,slug:f,exportCfg:v}=y;if(!L?.length)return;if(a){const h=L.findIndex(b=>b.key==="_cta");h!==-1&&(L[h]={key:"_cta",title:"",render:(b,l)=>{const d=document.createElement("button");if(d.className=`btn btn-${a.style||"primary"} dm-cta-trigger`,d.dataset.action=a.action||"",a.scope&&(d.dataset.scope=a.scope),d.dataset.entry=l._entryId||"",a.confirm&&(d.dataset.confirm=a.confirm),a.icon){const m=document.createElement("span");m.dataset.icon=a.icon,d.appendChild(m),d.appendChild(document.createTextNode(" "))}return d.appendChild(document.createTextNode(a.label||"Run")),d}})}const i="col-table-"+Math.random().toString(36).slice(2,7),u=document.createElement("div");u.id=i,x.replaceChildren(u),T.create("#"+i,{data:w,columns:L,search:n,sortable:t,exportable:s,pageSize:o,emptyMessage:e}),a&&j(u),x._dmTable={columns:L,rows:w,slug:f,exportCfg:v,render(h){T.create("#"+i,{data:h,columns:L,search:n,sortable:t,exportable:s,pageSize:o,emptyMessage:e}),a&&j(u)}}})})(),(function(){const c=document.querySelectorAll("[data-form-inline]");if(!c.length||typeof F>"u")return;function x(t){const s=t&&t.logic;return!!(s&&(s.visibility||s.requirement))}function y(t,s){const o={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const a=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),f=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":a?"chooser":e.type,v={...e.formConfig||{}};v.span==="full"&&s&&(v.span=s);const i={type:f,required:x(e)?!1:e.required,label:e.label,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...e.tooltip&&{tooltip:e.tooltip},...v}};e.minLength!==void 0&&(i.minLength=e.minLength),e.maxLength!==void 0&&(i.maxLength=e.maxLength),e.min!==void 0&&(i.min=e.min),e.max!==void 0&&(i.max=e.max),a&&(e.variant&&(i.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(i.multiple=!0),e.density&&(i.density=e.density),e.columns&&(i.columns=e.columns),e.accent&&(i.accent=e.accent),e.accentStyle&&(i.accentStyle=e.accentStyle),e.glow&&(i.glow=!0),e.glowColour&&(i.glowColour=e.glowColour),e.shadow&&(i.shadow=e.shadow),e.shadowColour&&(i.shadowColour=e.shadowColour)),o[e.name]=i}),o}function L(t){const s={};return(t||[]).forEach(o=>{if(!(!o.name||o.type==="page-break"||o.type==="spacer")&&(o.type==="select"||o.type==="multiselect")&&o.required){const e=(o.options||[])[0];e&&(s[o.name]=typeof e=="object"?e.value:e)}}),s}function w(t,s){(s||[]).forEach(o=>{if(o.type!=="date"||!o.name)return;const e=t.querySelector(`[name="${o.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function n(t,s,o){let e=t.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",t.appendChild(e)),e.textContent=s,e.style.cssText=o?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}c.forEach(t=>{let s;try{s=P(t.dataset.formInline)}catch{return}const o=s.fields||[],e=s.settings||{},a=Number(e.columns)===3?3:2,f=e.layout||"grid",v=o.some(d=>d.type==="page-break"),i=async d=>{if(t.dataset.triggerBlocked==="true"){const m=t._formLogicRuntime,g=m&&m.submitBlock?m.submitBlock().reasons:[];n(t,g.join(" ")||"This form cannot be submitted yet.",!0);const C=new Error("Blocked by a form trigger");throw C.formSubmitFailed=!0,C}try{const m=t.querySelector(".fb-form-honeypot"),g=m?.querySelector('input[type="text"]')||null,C=m?.querySelector('[name="_t"]')||null,M=Object.assign({},d);delete M._t,m?.dataset.hpName&&delete M[m.dataset.hpName],g!==null&&(M._hp=g.value),C!==null&&(M._t=C.value);const D=await H.post(`/api/forms/submit/${s.slug}`,M);if(D&&D.error){n(t,D.error,!0);const z=new Error(D.error);throw z.formSubmitFailed=!0,z}if(D?.redirect){window.location.href=D.redirect;return}for(;t.firstChild;)t.removeChild(t.firstChild);n(t,D?.message||e.successMessage||"Thank you for your submission.",!1)}catch(m){throw m.formSubmitFailed||n(t,m.message||"Submission failed. Please try again.",!0),m}},u=Date.now();function h(d){const m=d.querySelector("form");if(!m||m.querySelector(".fb-form-honeypot"))return;const g=document.createElement("div");g.className="fb-form-honeypot",g.setAttribute("aria-hidden","true");const C=document.createElement("input");C.name=m.querySelector('[name="website"]')?"_website":"website",g.dataset.hpName=C.name,C.type="text",C.tabIndex=-1,C.autocomplete="url",C.placeholder="https://",g.appendChild(C);const M=document.createElement("input");M.name="_t",M.type="hidden",M.value=u,g.appendChild(M),m.appendChild(g)}function b(d){if(!window.FormLogicEngine||!o.some(g=>g.logic||Array.isArray(g.triggers)&&g.triggers.length))return null;const m=new window.FormLogicEngine.FormLogicRuntime(s,t,d?{baseValues:d}:void 0);if(m.init(),t._formLogicRuntime=m,t._formLogicObserved)return m;if(t._formLogicObserved=!0,t.parentNode&&typeof MutationObserver<"u"){const g=new MutationObserver(function(C){for(const M of C)for(const D of M.removedNodes)if(D===t||D.nodeType===1&&D.contains&&D.contains(t)){t._formLogicRuntime&&t._formLogicRuntime.destroy(),g.disconnect();return}});g.observe(t.parentNode,{childList:!0,subtree:!1}),t._formLogicObserver=g}return m}let l=!1;if(t.addEventListener("dm-form-end",d=>{const m=d.detail?.validation;if(m&&!m.valid){G(t,m.missing),K(m);return}l||(l=!0,i(d.detail?.values||{}).catch(()=>{}).finally(()=>{l=!1}))}),v&&F.wizard){const d=re(o,s.title),m=d.map(I=>({title:I.title,description:I.description,fields:y(I.fields,a)})),g=d.map(I=>I.fields);let C=0,M=null,D=null;const z=I=>{const r=window.FormLogicEngine;if(!r||!r.validateFields)return;const A=r.validateFields(t,g[C]||[]);A.valid||(I.preventDefault(),I.stopImmediatePropagation(),G(t,A.missing),K(A))},B=()=>{const I=g[C]||o;le(t,e),Q(t,e,I),w(t,I),U(t,I),e.honeypot!==!1&&h(t),M&&M.destroy(),M=b(D?.getStepData?D.getStepData():void 0),t.querySelectorAll(".wizard-next, .wizard-submit").forEach(r=>r.addEventListener("click",z,!0))};D=F.wizard(t,{schema:{steps:m},layout:f,columns:a,finishText:e.submitText||"Finish",onSubmit:i,onStepChange:I=>{C=I,setTimeout(B,0)}}),t.addEventListener("dm-form-trigger",I=>{if(I.detail?.type!=="goto-step"||!D?.goToStep)return;const r=Number(I.detail.action.step);!Number.isFinite(r)||r<0||r>=m.length||r!==C&&D.goToStep(r)}),Promise.resolve(D).then(B)}else if(F.render){const d=F.render(t,y(o,a),L(o),{submitText:e.submitText||"Submit",layout:f,columns:a,onSubmit:i});Promise.resolve(d).then(function(){ce(t,e,f),Q(t,e,o),w(t,o),U(t,o),e.honeypot!==!1&&h(t),b()})}})})();function de(p,c){const x=document.body,y=x.classList,L=new Set((x.dataset.railSite||"").split(" ").filter(Boolean)),w=()=>window.matchMedia("(min-width: 993px)").matches;p.querySelectorAll(".navbar-link, .navbar-dropdown-toggle").forEach(a=>{const f=a.textContent.trim();f&&(a.setAttribute("title",f),a.querySelector("svg, [data-icon]")||(a.dataset.initial=f.charAt(0).toUpperCase()))});const n=document.createElement("button");n.type="button",n.className="dm-nav-collapse-toggle";const t=()=>{const a=y.contains("dm-vnav-collapsed");n.setAttribute("aria-pressed",String(a)),n.setAttribute("aria-label",a?"Expand menu":"Collapse menu"),n.title=n.getAttribute("aria-label");const f=c?"chevrons-left":"chevrons-right",v=c?"chevrons-right":"chevrons-left";n.innerHTML=`<span data-icon="${a?f:v}"></span>`,window.Domma&&Domma.icons&&Domma.icons.scan(n)},s=a=>{y.toggle("dm-vnav-collapsed",a),S.set("nav_collapsed",a),t()};n.addEventListener("click",()=>s(!y.contains("dm-vnav-collapsed"))),t();const o=p.querySelector(".navbar-container")||p;o.insertBefore(n,o.firstChild?.nextSibling||null);const e=a=>{y.toggle("dm-vnav-autohide",a),y.toggle("dm-vnav-collapsible",!a&&L.has("collapsible")),a?y.remove("dm-vnav-collapsed"):L.has("collapsible")&&S.get("nav_collapsed")===!0&&y.add("dm-vnav-collapsed"),p.classList.remove("dm-nav-revealed"),t()};p.addEventListener("pointerdown",a=>{y.contains("dm-vnav-autohide")&&a.pointerType!=="mouse"&&!p.classList.contains("dm-nav-revealed")&&(p.classList.add("dm-nav-revealed"),a.preventDefault())}),document.addEventListener("pointerdown",a=>{p.contains(a.target)||p.classList.remove("dm-nav-revealed")}),window.E&&typeof E.contextMenu=="function"&&E.contextMenu(p,{items:()=>{const a=y.contains("dm-vnav-autohide"),f=y.contains("dm-vnav-collapsed"),v=S.has("nav_autohide")||S.has("nav_collapsed");return[{header:"Menu"},{label:f?"Expand menu":"Collapse to icons",icon:f?"maximize":"minimize",disabled:!w()||a||!L.has("collapsible"),action:()=>s(!f)},{label:a?"Keep on screen":"Hide when not in use",icon:a?"eye":"eye-off",disabled:!w(),action:()=>{S.set("nav_autohide",!a),e(!a)}},{type:"divider"},{label:"Reset to the site's setting",icon:"refresh",disabled:!v,action:()=>{S.remove("nav_autohide"),S.remove("nav_collapsed"),e(L.has("autohide")),y.toggle("dm-vnav-collapsed",!L.has("autohide")&&L.has("start-collapsed")),t()}}]}})}function me(p){p.classList.add("dm-nav-scroll-hide");let c=window.scrollY,x=!1;window.addEventListener("scroll",()=>{x||(x=!0,requestAnimationFrame(()=>{x=!1;const y=window.scrollY,L=p.matches(":focus-within")||p.querySelector(".navbar-dropdown.open, .navbar-collapse.show");y>c&&y>p.offsetHeight&&!L?p.classList.add("dm-nav-scrolled-away"):y<c&&p.classList.remove("dm-nav-scrolled-away"),c=y}))},{passive:!0})}function ue(p){let c={};try{c=JSON.parse(p.dataset.contents||"{}")}catch{}const x=p.closest(".site-main"),y=document.querySelector(".site-content"),L=new Set(String(c.exclude||"").split(",").map(i=>i.trim().toLowerCase()).filter(Boolean)),w=i=>!i.closest(".hero, .card, .accordion, .tabs, figure, #site-sidebar"),n=i=>(y?[...y.querySelectorAll(i)]:[]).filter(w).filter(u=>!L.has(u.textContent.trim().toLowerCase())),t=c.headings==="h2"?n("h2"):c.headings==="h3"?n("h3"):c.headings==="h2h3"?n("h2, h3"):n("h2").length?n("h2"):n("h3");if(!t.length){x?.classList.add("no-sidebar-items");return}const s=new Set([...document.querySelectorAll("[id]")].map(i=>i.id)),o=document.createElement("nav");o.className="dm-contents",o.setAttribute("aria-label",c.title||"Page contents"),c.hover&&(o.dataset.menuHover=c.hover),c.selected&&(o.dataset.menuActive=c.selected),c.accent&&o.style.setProperty("--dm-menu-highlight",c.accent.startsWith("#")?c.accent:`var(--dm-${c.accent})`),c.font&&(o.style.fontFamily=c.font),c.fontSize&&(o.style.fontSize=c.fontSize),c.fontWeight&&(o.style.fontWeight=c.fontWeight);const e=document.createElement("div");e.className="dm-contents-head";const a=document.createElement("p");a.className="dm-contents-title",a.textContent=c.title||"",e.appendChild(a),o.appendChild(e);const f=document.createElement("ol");f.className="dm-contents-list"+(c.numbered?" is-numbered":"");const v=[];for(const i of t){if(!i.id){const b=(i.textContent||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"section";let l=b;for(let d=2;s.has(l);d++)l=`${b}-${d}`;s.add(l),i.id=l}const u=document.createElement("li");u.className=`dm-contents-item dm-contents-${i.tagName.toLowerCase()}`;const h=document.createElement("a");h.href=`#${i.id}`,h.textContent=i.textContent.trim(),u.appendChild(h),f.appendChild(u),v.push([i,h])}if(o.appendChild(f),p.replaceChildren(o),c.scrollSpy!==!1&&"IntersectionObserver"in window){const i=new Map,u=()=>{const b=v.find(([l])=>i.get(l))||null;v.forEach(([,l])=>{const d=b&&l===b[1];l.classList.toggle("is-active",!!d),d?l.setAttribute("aria-current","location"):l.removeAttribute("aria-current")})},h=new IntersectionObserver(b=>{b.forEach(l=>i.set(l.target,l.isIntersecting)),u()},{rootMargin:"-80px 0px -60% 0px"});v.forEach(([b])=>h.observe(b))}if(x?.classList.contains("contents-collapsible")){const i=document.createElement("button");i.type="button",i.className="dm-contents-toggle";const u=()=>{const h=x.classList.contains("contents-collapsed");i.setAttribute("aria-expanded",String(!h)),i.setAttribute("aria-label",h?"Show the contents":"Hide the contents"),i.title=i.getAttribute("aria-label"),i.innerHTML=`<span data-icon="${h?"list":"x"}"></span>`,window.Domma?.icons&&Domma.icons.scan(i)};i.addEventListener("click",()=>{S.set("contents_collapsed",x.classList.toggle("contents-collapsed")),u()}),u(),e.insertBefore(i,e.firstChild)}if(x?.classList.contains("contents-floating")&&c.movable){o.classList.add("is-movable");const i=S.get("contents_position");i&&Number.isFinite(i.x)&&Object.assign(p.style,{left:`${i.x}px`,top:`${i.y}px`,right:"auto",bottom:"auto"}),e.addEventListener("pointerdown",u=>{if(u.target.closest("button"))return;const h=p.getBoundingClientRect(),b=u.clientX-h.left,l=u.clientY-h.top,d=g=>{const C=Math.max(0,Math.min(window.innerWidth-h.width,g.clientX-b)),M=Math.max(0,Math.min(window.innerHeight-40,g.clientY-l));Object.assign(p.style,{left:`${C}px`,top:`${M}px`,right:"auto",bottom:"auto"})},m=()=>{document.removeEventListener("pointermove",d),S.set("contents_position",{x:parseInt(p.style.left,10),y:parseInt(p.style.top,10)})};document.addEventListener("pointermove",d),document.addEventListener("pointerup",m,{once:!0})})}window.E&&typeof E.contextMenu=="function"&&E.contextMenu(p,{items:()=>{const i=x?.classList.contains("contents-collapsible"),u=x?.classList.contains("contents-collapsed"),h=v.find(([,b])=>b.classList.contains("is-active"));return[{header:"Contents"},...i?[{label:u?"Show the contents":"Collapse to a button",icon:u?"list":"minimize",action:()=>p.querySelector(".dm-contents-toggle")?.click()}]:[],{label:"Copy a link to this section",icon:"link",disabled:!h,action:async()=>{const b=`${location.origin}${location.pathname}#${h[0].id}`;try{await _.copyToClipboard(b),E.toast("Link copied.",{type:"success"})}catch{E.toast("Could not copy the link.",{type:"error"})}}},{label:"Back to the top",icon:"arrow-up",action:()=>window.scrollTo({top:0,behavior:"smooth"})}]}}),x?.classList.contains("contents-autohide")&&(p.addEventListener("pointerdown",i=>{i.pointerType!=="mouse"&&!p.classList.contains("is-revealed")&&(p.classList.add("is-revealed"),i.preventDefault())}),document.addEventListener("pointerdown",i=>{p.contains(i.target)||p.classList.remove("is-revealed")}))}
|
|
1
|
+
import{decodeBase64Json as P}from"/public/js/utf8-base64.mjs";import{makeBadgeEl as Y,applyColour as J,applyPill as te,applyFloat as ne,applyHighlight as R,applyItemHighlight as oe,itemHighlightAttrs as ae,isCurrentItem as X,menuAlign as se,applyClasses as O,cssClasses as W}from"/public/js/menu-decor.mjs?v=20261009-hover-darken";import{initFloatingMenus as ie}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as U,markInvalidFields as G,showValidationPopup as K}from"/public/js/forms.js?v=20261009-form-classes";import{buildWizardSteps as re}from"/public/js/form-steps.mjs";import{applyFormClasses as Q,applySubmitButtons as ce,applyWizardButtons as le}from"/public/js/form-submit.mjs";document.addEventListener("click",p=>{const l=p.target?.closest?.(".dm-banner__dismiss");l&&(p.preventDefault(),l.closest(".dm-banner")?.remove())}),$(()=>{const p=window.__CMS_NAV__||{},l=window.__CMS_SITE__||{};if(l.autoTheme?.enabled){let n=function(e){const a=(e||"07:00").split(":");return+a[0]*60+(+a[1]||0)},s=function(){const e=new Date,a=e.getHours()*60+e.getMinutes();return a>=n(o.dayStart)&&a<n(o.nightStart)?o.dayTheme:o.nightTheme},t=function(e){const a=o.apply&&o.apply[e]||{theme:e,custom:""};Domma.theme.set(a.theme);for(const f of[...document.body.classList])f.startsWith("dm-custom-")&&document.body.classList.remove(f);a.custom&&document.body.classList.add(a.custom)};const o=l.autoTheme;t(s()),setInterval(()=>t(s()),6e4)}const x=$("#site-navbar");if(x.length&&p.brand){let s=function(d,m=0){return Array.isArray(d)?d.filter(u=>!(u&&(u.type==="spacer"||u.type==="theme-switcher"))).filter(u=>!(u&&u.type==="separator"&&m===0)).map(u=>u&&u.type==="separator"?{divider:!0}:Array.isArray(u?.items)?{...u,items:s(u.items,m+1)}:u):d},t=function(d){return Array.isArray(d)?d.filter(m=>!(m&&(m.type==="separator"||m.type==="spacer"||m.type==="theme-switcher"))):[]};const o={...p.brand},n=o.size&&o.size!=="md"?` navbar-brand-${o.size}`:"";if(o.logo||o.icon||o.tagline){let d="";o.logo?d+=`<img src="${o.logo}" class="navbar-brand-logo" alt="${o.text||""}">`:o.icon&&(d+=`<span data-icon="${o.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),o.text&&(d+=`<span class="navbar-brand-text${n}">${o.text}</span>`),o.tagline&&(d+=`<small class="navbar-brand-tagline">${o.tagline}</small>`),o.html=d}else n&&o.text&&(o.html=`<span class="navbar-brand-text${n}">${o.text}</span>`);const e=p.position==="floating",a=p.orientation==="vertical";Domma.elements.navbar("#site-navbar",{brand:o,items:s(p.items||[]),variant:p.variant||"dark",position:e?"static":p.position||"sticky",collapsible:!0,appearOnHover:!!p.appearOnHover}),Domma.icons.scan("#site-navbar");const f=x.get(0);f.classList.add(`dm-nav-align-${se(p)||"centre"}`),a&&x.get(0).classList.add("dm-nav-vertical",p.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left");const v=p.dock||{};a&&!e&&de(f,p.side==="right"),!a&&v.hideOnScroll&&me(f),R(f,p),O(f,p.class),e&&(ne(x.get(0),p),ie(x.get(0).parentNode||document));const i=f.querySelector(".navbar-toggle"),c=f.querySelector(".navbar-collapse");if(i&&c){let d=null;const m=()=>{c.classList.contains("show")&&i.click()},u=()=>{d&&(d.remove(),d=null),document.body.classList.remove("dm-drawer-open")},g=()=>{d||(d=document.createElement("div"),d.className="dm-navbar-backdrop",d.addEventListener("click",m),document.body.appendChild(d),document.body.classList.add("dm-drawer-open"))};i.addEventListener("click",()=>{requestAnimationFrame(()=>{c.classList.contains("show")?g():u()})}),c.addEventListener("click",C=>{C.target.closest("a")&&requestAnimationFrame(u)}),document.addEventListener("keydown",C=>{C.key==="Escape"&&c.classList.contains("show")&&m()})}const h=f.querySelector(".navbar-nav");if(h){const d=Array.from(h.children);let m=0;for(const u of p.items||[])if(!(u&&u.type==="separator")){if(u&&u.type==="spacer"){const g=document.createElement("li"),C=["xs","sm","md","lg","xl","flex"].includes(u.size)?u.size:"md";g.className=`dm-menu-spacer dm-menu-spacer--${C}`,g.setAttribute("aria-hidden","true"),h.insertBefore(g,d[m]||null);continue}if(u&&u.type==="theme-switcher"){const g=document.createElement("li");g.className="dm-menu-theme-switcher";const C=document.createElement("span");C.dataset.themeSwitcher="menu",C.dataset.position="inline",g.appendChild(C),h.insertBefore(g,d[m]||null);continue}m++}window.CMSThemeSwitcher?.scan(h)}const b=t(p.items||[]);f.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(d=>{const m=b[parseInt(d.dataset.index,10)];if(!m)return;const u=d.dataset.subindex!=null,g=u&&Array.isArray(m.items)?m.items[parseInt(d.dataset.subindex,10)]:m;if(!g)return;g.colour&&J(d,g.colour),g.pill&&!u&&te(d,g.pill),oe(d,g),O(d.closest("li"),g.class);const C=Y(g.badge);C&&d.appendChild(C),X(g.url,location.pathname)&&(d.classList.add("active"),d.setAttribute("aria-current","page"))})}const y=$("#site-footer");if(y.length){let e=function(r){return String(r??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},a=function(r){return e(r).replace(/'/g,"'")},f=function(r){if(r&&r.type==="separator")return'<li class="footer-sep"><hr></li>';if(r&&r.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(r.size)?r.size:"md"}" aria-hidden="true"></li>`;if(r&&r.type==="theme-switcher")return'<li class="dm-menu-theme-switcher"><span data-theme-switcher="menu" data-position="inline"></span></li>';const A=a(r.url||"#"),k=e(r.text||""),q=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(f).join("")}</ul>`:"",N=ae(r),Z=[N.hover&&` data-menu-item-hover="${a(N.hover)}"`,N.active&&` data-menu-item-active="${a(N.active)}"`].filter(Boolean).join(""),ee=N.accent?` style="--dm-menu-highlight:${a(N.accent)}"`:"",V=W(r.class).join(" ");return`<li${V?` class="${a(V)}"`:""}><a href="${A}"${Z}${ee}>${k}</a>${q}</li>`},v=function(){if(!l.footer)return"";const r=Object.keys(n).filter(k=>o[k]);if(!r.length)return"";const A=W(l.footer.socialClass).join(" ");return`<div class="footer-social">${r.map(k=>{const{label:q,svg:N}=n[k];return`<a href="${o[k]}" target="_blank" rel="noopener noreferrer" aria-label="${q}" class="footer-social-link${A?` ${a(A)}`:""}">${N}</a>`}).join("")}</div>`},b=function(r){if(!r||r.type==="separator"||r.type==="spacer")return"";if(r.type==="theme-switcher")return`<li class="cms-footer-col">${f(r).replace(/^<li[^>]*>|<\/li>$/g,"")}</li>`;const A=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(f).join("")}</ul>`:"",k=h(r)?`<span class="cms-footer-col-title">${e(r.text||"")}</span>`:`<a class="cms-footer-col-title" href="${a(r.url||"#")}">${e(r.text||"")}</a>`,q=["cms-footer-col",...W(r.class)].join(" ");return`<li class="${a(q)}">${k}${A}</li>`},m=function(r,A,k=!1){return(r||[]).forEach(q=>{q&&(q.type==="separator"||q.type==="spacer"||q.type==="theme-switcher")||(k&&h(q)||A.push(q),Array.isArray(q.items)&&q.items.length&&m(q.items,A))}),A};const o=l.social||{},n={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let s='<div class="footer-inner container">';const t=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""},i=["columns","row"].includes(l.footer?.layout)?l.footer.layout:"auto",c=i==="columns"||i==="auto"&&t.primary.some(r=>Array.isArray(r?.items)&&r.items.length),h=r=>c&&!r.url&&Array.isArray(r.items)&&r.items.length>0;if(c){const r=p.brand||{},A=r.text||l.title||"",k=r.logo?`<img src="${a(r.logo)}" class="cms-footer-brand-logo" alt="">`:r.icon?'<span class="cms-footer-brand-icon"></span>':"",q=String(l.footer?.blurb||"").trim();s='<div class="footer-inner footer-inner--columns container"><div class="cms-footer-top">',s+=`<div class="cms-footer-brand">${A||k?`<a class="cms-footer-brand-name" href="${a(r.url||"/")}">${k}<span>${e(A)}</span></a>`:""}${q?`<p class="cms-footer-blurb">${e(q)}</p>`:""}${v()}</div>`,t.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary footer-nav--columns" aria-label="Footer"><ul>${t.primary.map(b).join("")}</ul></nav>`),s+='</div><div class="cms-footer-bottom">',t.copyright&&(s+=`<p class="cms-footer-copyright">${e(t.copyright)}</p>`),t.legal.length&&(s+=`<nav class="footer-nav footer-nav--legal" aria-label="Legal"><ul>${t.legal.map(f).join("")}</ul></nav>`),s+="</div>"}else t.copyright&&(s+=`<p>${e(t.copyright)}</p>`),t.primary.length&&(s+=`<nav class="footer-nav footer-nav--primary"><ul>${t.primary.map(f).join("")}</ul></nav>`),t.legal.length&&(s+=`<nav class="footer-nav footer-nav--legal"><ul>${t.legal.map(f).join("")}</ul></nav>`),s+=v();s+="</div>",y.html(s),window.CMSThemeSwitcher?.scan(y.get(0));const d=y.get(0).querySelector(".cms-footer-brand-icon");d&&p.brand?.icon&&d.setAttribute("data-icon",p.brand.icon),O(y.get(0),l.footer?.class);const u=[...m(t.primary,[],!0),...m(t.legal,[])];y.get(0).querySelectorAll(".footer-nav a").forEach((r,A)=>{const k=u[A];if(!k)return;k.colour&&J(r,k.colour);const q=Y(k.badge);q&&r.appendChild(q),X(k.url,location.pathname)&&(r.classList.add("is-active"),r.setAttribute("aria-current","page"))}),R(y.get(0).querySelector(".footer-nav--primary"),{highlight:t.primaryHighlight}),R(y.get(0).querySelector(".footer-nav--legal"),{highlight:t.legalHighlight}),O(y.get(0).querySelector(".footer-nav--primary"),t.primaryClass),O(y.get(0).querySelector(".footer-nav--legal"),t.legalClass);const C=l.footer?.showMotionToggle!==!1,M=S.get("reduced_motion"),D=l.footer?.motionDefault,z=M!=null?!!M:D==="on"?!0:D==="off"?!1:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),B=l.footer?.motionControl==="checkbox",I=y.get(0).querySelector(".cms-footer-bottom")||y.get(0).querySelector(".footer-inner");if(C&&I){const r=document.createElement("input");r.type="checkbox",r.className=B?"footer-motion-checkbox":"form-switch-input",r.id="dm-motion-switch",r.checked=z,r.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const A=document.createElement("span");A.className=B?"footer-motion-label":"form-switch-label",A.textContent="Reduce motion";const k=document.createElement("label");k.className=B?"footer-motion-check":"form-switch footer-motion-switch",k.title="Reduce motion",k.appendChild(r),k.appendChild(A),I.appendChild(k)}}const L=document.getElementById("site-sidebar");L&&ue(L),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const w=$(".page-body");if(w.length){w.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),w.find(".tabs").each(function(){Domma.elements.tabs(this)}),w.find(".carousel").each(function(){const o={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const n=parseInt(this.dataset.animationDuration,10);Number.isFinite(n)&&(o.animationDuration=n)}this.dataset.animationEasing&&(o.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(n=>n.cloneNode(!0)),this._dmCarouselOpts=o),this._dmCarousel=Domma.elements.carousel(this,o)}),w.find(".dm-countdown").each(function(){const o={autoStart:!0},n=String(this.dataset.format||"").trim(),s=String(this.dataset.to||"").trim(),t=/^(\d{4})-(\d{2})-(\d{2})$/.exec(s),e=t?new Date(+t[1],+t[2]-1,+t[3]).getTime():s?new Date(s).getTime():NaN;Number.isFinite(e)?o.duration=Math.max(0,e-Date.now()):this.dataset.duration&&(o.duration=(parseInt(this.dataset.duration,10)||0)*1e3);const a=/^dd:/i.test(n);o.format=a?"hh:mm:ss":n||(o.duration>=36e5?"hh:mm:ss":"mm:ss");const f=this,v=c=>String(c).padStart(2,"0"),i=c=>{const h=Math.max(0,Math.floor(c/1e3)),b=f.querySelector(".dm-timer-time");b&&(b.textContent=`${v(Math.floor(h/86400))}:${v(Math.floor(h%86400/3600))}:${v(Math.floor(h%3600/60))}:${v(h%60)}`)};a&&(o.onTick=i),Domma.elements.timer(this,o),a&&i(o.duration||0)}),w.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),w.find(".dm-progression").each(function(){const o=Array.from(this.querySelectorAll(".dm-progression-item")).map(t=>{const e={title:t.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:t.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return t.dataset.date&&(e.date=t.dataset.date),t.dataset.status&&(e.status=t.dataset.status),t.dataset.eventIcon&&(e.icon=t.dataset.eventIcon),t.dataset.entryId&&(e._entryId=t.dataset.entryId),e}),n=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",s=Domma.elements.progression(this,{layout:n,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...o.length?{items:o}:{}});if((this.dataset.mode||"timeline")==="timeline"){const t={completed:{label:"Completed",icon:"check-circle",tone:"success"},planned:{label:"Planned",icon:"circle",tone:"secondary"},"in-progress":{label:"In progress",icon:"clock",tone:"primary"},blocked:{label:"Blocked",icon:"alert-circle",tone:"danger"}};this.querySelectorAll(".dm-progression-item").forEach((e,a)=>{const f=o[a];if(!f)return;const v=e.querySelector(".dm-progression-title");if(f.icon&&v){const c=document.createElement("span");c.dataset.icon=f.icon,c.style.cssText="display:inline-flex;vertical-align:middle;margin-right:.4em",c.setAttribute("aria-hidden","true"),v.prepend(c)}const i=t[f.status];if(i&&v){const c=document.createElement("span");c.className=`badge badge-${i.tone} cms-timeline-status`,c.style.cssText="display:inline-flex;align-items:center;gap:.35em;margin:.4rem 0";const h=document.createElement("span");h.dataset.icon=i.icon,h.dataset.iconSize="14",h.setAttribute("aria-hidden","true"),c.append(h,document.createTextNode(i.label)),v.after(c)}})}Domma.icons.scan(this),o.length&&(this._dmCtx={instance:s,items:o})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(o=>{const n=o.dataset.revealMode||"stagger",s=o.dataset.revealAnimation||"slide-up",t=parseInt(o.dataset.revealDuration,10)||400,e=parseInt(o.dataset.revealStagger,10)||60,a=parseInt(o.dataset.revealDelay,10)||0,f=o.dataset.revealDirection==="rtl",v=Array.from(o.children),i={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};v.forEach((c,h)=>{c.style.opacity="0",c.style.transform=i[s]||"",c.style.transition=`opacity ${t}ms ease, transform ${t}ms ease`;const b=f?v.length-1-h:h;c.style.transitionDelay=n==="stagger"?`${a+b*e}ms`:`${a}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const c=new IntersectionObserver(h=>{h.forEach(b=>{b.isIntersecting&&(b.target.offsetWidth,b.target.style.opacity="1",b.target.style.transform="none",c.unobserve(b.target))})},{threshold:.1});v.forEach(h=>c.observe(h))}))}),w.find(".card[data-collapsible]").each(function(){const o=this.querySelector(".card-header");o&&o.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),w.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const o=this.dataset.soTarget,n=document.getElementById(o);if(!n)return;const s=E.slideover({title:n.dataset.soTitle||"",size:n.dataset.soSize||"md",position:n.dataset.soPosition||"right"});n.style.display="",s.setContent(n),s.open()})})}if(typeof $.setup=="function"){const o=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(n=>{try{const s=P(n.dataset.config);Object.assign(o,s)}catch{}}),Object.keys(o).length>0){const n={};for(const[s,t]of Object.entries(o)){const e=t?.events?.click,{confirm:a,toast:f,alert:v,prompt:i,...c}=e||{};a||f||v||i?($(s).on("click",async function(b){if(b.preventDefault(),a&&!await E.confirm(a))return;let d=null;if(!(i&&(d=await E.prompt(i,{inputPlaceholder:c.promptPlaceholder||"",inputValue:c.promptDefault||""}),d===null))){if(c.target){const m=$(c.target);c.toggleClass&&m.toggleClass(c.toggleClass),c.addClass&&m.addClass(c.addClass),c.removeClass&&m.removeClass(c.removeClass),d!==null&&(c.setText&&m.text(d),c.setVal&&m.val(d),c.setAttr&&m.attr(c.setAttr,d))}c.href&&(window.location.href=c.href),f&&E.toast(f,{type:c.toastType||"success"}),v&&E.alert(v)}}),Object.keys(c).length&&(n[s]={...t,events:{...t.events,click:c}})):n[s]=t}$.setup(n)}}w.length&&j(w.get(0))}),window.wireCTAButtons=j;function j(p){p.querySelectorAll(".dm-cta-trigger").forEach(l=>{l.__ctaWired||(l.__ctaWired=!0,l.addEventListener("click",async()=>{const x=l.dataset.action,y=l.dataset.entry,L=l.dataset.confirm;let w=S.get("auth_token");if(!w){E.toast("Please log in to perform this action.",{type:"warning"});return}if(L&&!await E.confirm(L))return;const o=Array.from(l.childNodes).map(s=>s.cloneNode(!0));l.disabled=!0,l.textContent="Running\u2026";const n=s=>fetch(`/api/actions/${x}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify(l.dataset.scope?{entryId:y,scope:l.dataset.scope}:{entryId:y})});try{let s=await n(w);if(s.status===401){const e=S.get("auth_refresh_token");if(e){const a=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(a.ok){const{token:f}=await a.json();S.set("auth_token",f),w=f,s=await n(w)}}}const t=await s.json().catch(()=>({}));if(!s.ok||t.success===!1)throw new Error(t.error||t.message||`Error ${s.status}`);E.toast(t.message||"Action completed.",{type:"success"})}catch(s){E.toast(s.message||"Action failed.",{type:"error"})}finally{l.disabled=!1,l.textContent="",o.forEach(s=>l.appendChild(s)),Domma.icons.scan(l)}}))})}(function(){const l=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!l.length)return;const x=S.get("auth_token");function y(w,o){const n=document.createElement("div");n.className="dm-collection-display dm-collection-empty";const s=document.createElement("p");if(o){s.appendChild(document.createTextNode("Please "));const t=document.createElement("a");t.href="/login",t.textContent="sign in",s.appendChild(t),s.appendChild(document.createTextNode(" to view your entries."))}else s.textContent=w;return n.appendChild(s),n}function L(w,o){const n=document.createElement("template");n.innerHTML=o||"",w.replaceWith(n.content)}l.forEach(async w=>{const o=w.dataset.collectionAttrs;if(!o)return;if(!x){w.replaceChildren(y("",!0));return}const n=s=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({attrs:o})});try{let s=x,t=await n(s);if(t.status===401){const a=S.get("auth_refresh_token");if(a){const f=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:a})});if(f.ok){const{token:v}=await f.json();S.set("auth_token",v),s=v,t=await n(s)}}}if(!t.ok){const{error:a}=await t.json().catch(()=>({}));w.replaceChildren(y(a||"Unable to load entries.",!1));return}const{html:e}=await t.json();L(w,e),window.DommaCollectionBrowser?.hydrate&&window.DommaCollectionBrowser.hydrate(document)}catch{w.replaceChildren(y("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),j(document.body)},100)})(),(function(){const l=document.querySelectorAll("[data-collection-table]");!l.length||typeof T>"u"||!T.create||l.forEach(x=>{let y;try{y=P(x.dataset.payload)}catch{return}const{columns:L,rows:w,search:o,sortable:n,exportable:s,pageSize:t,empty:e,ctaConfig:a,slug:f,exportCfg:v}=y;if(!L?.length)return;if(a){const h=L.findIndex(b=>b.key==="_cta");h!==-1&&(L[h]={key:"_cta",title:"",render:(b,d)=>{const m=document.createElement("button");if(m.className=`btn btn-${a.style||"primary"} dm-cta-trigger`,m.dataset.action=a.action||"",a.scope&&(m.dataset.scope=a.scope),m.dataset.entry=d._entryId||"",a.confirm&&(m.dataset.confirm=a.confirm),a.icon){const u=document.createElement("span");u.dataset.icon=a.icon,m.appendChild(u),m.appendChild(document.createTextNode(" "))}return m.appendChild(document.createTextNode(a.label||"Run")),m}})}const i="col-table-"+Math.random().toString(36).slice(2,7),c=document.createElement("div");c.id=i,x.replaceChildren(c),T.create("#"+i,{data:w,columns:L,search:o,sortable:n,exportable:s,pageSize:t,emptyMessage:e}),a&&j(c),x._dmTable={columns:L,rows:w,slug:f,exportCfg:v,render(h){T.create("#"+i,{data:h,columns:L,search:o,sortable:n,exportable:s,pageSize:t,emptyMessage:e}),a&&j(c)}}})})(),(function(){const l=document.querySelectorAll("[data-form-inline]");if(!l.length||typeof F>"u")return;function x(n){const s=n&&n.logic;return!!(s&&(s.visibility||s.requirement))}function y(n,s){const t={};return(n||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const a=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),f=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":a?"chooser":e.type,v={...e.formConfig||{}};v.span==="full"&&s&&(v.span=s);const i={type:f,required:x(e)?!1:e.required,label:e.label,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...e.tooltip&&{tooltip:e.tooltip},...v}};e.minLength!==void 0&&(i.minLength=e.minLength),e.maxLength!==void 0&&(i.maxLength=e.maxLength),e.min!==void 0&&(i.min=e.min),e.max!==void 0&&(i.max=e.max),a&&(e.variant&&(i.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(i.multiple=!0),e.density&&(i.density=e.density),e.columns&&(i.columns=e.columns),e.accent&&(i.accent=e.accent),e.accentStyle&&(i.accentStyle=e.accentStyle),e.glow&&(i.glow=!0),e.glowColour&&(i.glowColour=e.glowColour),e.shadow&&(i.shadow=e.shadow),e.shadowColour&&(i.shadowColour=e.shadowColour)),t[e.name]=i}),t}function L(n){const s={};return(n||[]).forEach(t=>{if(!(!t.name||t.type==="page-break"||t.type==="spacer")&&(t.type==="select"||t.type==="multiselect")&&t.required){const e=(t.options||[])[0];e&&(s[t.name]=typeof e=="object"?e.value:e)}}),s}function w(n,s){(s||[]).forEach(t=>{if(t.type!=="date"||!t.name)return;const e=n.querySelector(`[name="${t.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function o(n,s,t){let e=n.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",n.appendChild(e)),e.textContent=s,e.style.cssText=t?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}l.forEach(n=>{let s;try{s=P(n.dataset.formInline)}catch{return}const t=s.fields||[],e=s.settings||{},a=Number(e.columns)===3?3:2,f=e.layout||"grid",v=t.some(m=>m.type==="page-break"),i=async m=>{if(n.dataset.triggerBlocked==="true"){const u=n._formLogicRuntime,g=u&&u.submitBlock?u.submitBlock().reasons:[];o(n,g.join(" ")||"This form cannot be submitted yet.",!0);const C=new Error("Blocked by a form trigger");throw C.formSubmitFailed=!0,C}try{const u=n.querySelector(".fb-form-honeypot"),g=u?.querySelector('input[type="text"]')||null,C=u?.querySelector('[name="_t"]')||null,M=Object.assign({},m);delete M._t,u?.dataset.hpName&&delete M[u.dataset.hpName],g!==null&&(M._hp=g.value),C!==null&&(M._t=C.value);const D=await H.post(`/api/forms/submit/${s.slug}`,M);if(D&&D.error){o(n,D.error,!0);const z=new Error(D.error);throw z.formSubmitFailed=!0,z}if(D?.redirect){window.location.href=D.redirect;return}for(;n.firstChild;)n.removeChild(n.firstChild);o(n,D?.message||e.successMessage||"Thank you for your submission.",!1)}catch(u){throw u.formSubmitFailed||o(n,u.message||"Submission failed. Please try again.",!0),u}},c=Date.now();function h(m){const u=m.querySelector("form");if(!u||u.querySelector(".fb-form-honeypot"))return;const g=document.createElement("div");g.className="fb-form-honeypot",g.setAttribute("aria-hidden","true");const C=document.createElement("input");C.name=u.querySelector('[name="website"]')?"_website":"website",g.dataset.hpName=C.name,C.type="text",C.tabIndex=-1,C.autocomplete="url",C.placeholder="https://",g.appendChild(C);const M=document.createElement("input");M.name="_t",M.type="hidden",M.value=c,g.appendChild(M),u.appendChild(g)}function b(m){if(!window.FormLogicEngine||!t.some(g=>g.logic||Array.isArray(g.triggers)&&g.triggers.length))return null;const u=new window.FormLogicEngine.FormLogicRuntime(s,n,m?{baseValues:m}:void 0);if(u.init(),n._formLogicRuntime=u,n._formLogicObserved)return u;if(n._formLogicObserved=!0,n.parentNode&&typeof MutationObserver<"u"){const g=new MutationObserver(function(C){for(const M of C)for(const D of M.removedNodes)if(D===n||D.nodeType===1&&D.contains&&D.contains(n)){n._formLogicRuntime&&n._formLogicRuntime.destroy(),g.disconnect();return}});g.observe(n.parentNode,{childList:!0,subtree:!1}),n._formLogicObserver=g}return u}let d=!1;if(n.addEventListener("dm-form-end",m=>{const u=m.detail?.validation;if(u&&!u.valid){G(n,u.missing),K(u);return}d||(d=!0,i(m.detail?.values||{}).catch(()=>{}).finally(()=>{d=!1}))}),v&&F.wizard){const m=re(t,s.title),u=m.map(I=>({title:I.title,description:I.description,fields:y(I.fields,a)})),g=m.map(I=>I.fields);let C=0,M=null,D=null;const z=I=>{const r=window.FormLogicEngine;if(!r||!r.validateFields)return;const A=r.validateFields(n,g[C]||[]);A.valid||(I.preventDefault(),I.stopImmediatePropagation(),G(n,A.missing),K(A))},B=()=>{const I=g[C]||t;le(n,e),Q(n,e,I),w(n,I),U(n,I),e.honeypot!==!1&&h(n),M&&M.destroy(),M=b(D?.getStepData?D.getStepData():void 0),n.querySelectorAll(".wizard-next, .wizard-submit").forEach(r=>r.addEventListener("click",z,!0))};D=F.wizard(n,{schema:{steps:u},layout:f,columns:a,finishText:e.submitText||"Finish",onSubmit:i,onStepChange:I=>{C=I,setTimeout(B,0)}}),n.addEventListener("dm-form-trigger",I=>{if(I.detail?.type!=="goto-step"||!D?.goToStep)return;const r=Number(I.detail.action.step);!Number.isFinite(r)||r<0||r>=u.length||r!==C&&D.goToStep(r)}),Promise.resolve(D).then(B)}else if(F.render){const m=F.render(n,y(t,a),L(t),{submitText:e.submitText||"Submit",layout:f,columns:a,onSubmit:i});Promise.resolve(m).then(function(){ce(n,e,f),Q(n,e,t),w(n,t),U(n,t),e.honeypot!==!1&&h(n),b()})}})})();function de(p,l){const x=document.body,y=x.classList,L=new Set((x.dataset.railSite||"").split(" ").filter(Boolean)),w=()=>window.matchMedia("(min-width: 993px)").matches;p.querySelectorAll(".navbar-link, .navbar-dropdown-toggle").forEach(a=>{const f=a.textContent.trim();f&&(a.setAttribute("title",f),a.querySelector("svg, [data-icon]")||(a.dataset.initial=f.charAt(0).toUpperCase()))});const o=document.createElement("button");o.type="button",o.className="dm-nav-collapse-toggle";const n=()=>{const a=y.contains("dm-vnav-collapsed");o.setAttribute("aria-pressed",String(a)),o.setAttribute("aria-label",a?"Expand menu":"Collapse menu"),o.title=o.getAttribute("aria-label");const f=l?"chevrons-left":"chevrons-right",v=l?"chevrons-right":"chevrons-left";o.innerHTML=`<span data-icon="${a?f:v}"></span>`,window.Domma&&Domma.icons&&Domma.icons.scan(o)},s=a=>{y.toggle("dm-vnav-collapsed",a),S.set("nav_collapsed",a),n()};o.addEventListener("click",()=>s(!y.contains("dm-vnav-collapsed"))),n();const t=p.querySelector(".navbar-container")||p;t.insertBefore(o,t.firstChild?.nextSibling||null);const e=a=>{y.toggle("dm-vnav-autohide",a),y.toggle("dm-vnav-collapsible",!a&&L.has("collapsible")),a?y.remove("dm-vnav-collapsed"):L.has("collapsible")&&S.get("nav_collapsed")===!0&&y.add("dm-vnav-collapsed"),p.classList.remove("dm-nav-revealed"),n()};p.addEventListener("pointerdown",a=>{y.contains("dm-vnav-autohide")&&a.pointerType!=="mouse"&&!p.classList.contains("dm-nav-revealed")&&(p.classList.add("dm-nav-revealed"),a.preventDefault())}),document.addEventListener("pointerdown",a=>{p.contains(a.target)||p.classList.remove("dm-nav-revealed")}),window.E&&typeof E.contextMenu=="function"&&E.contextMenu(p,{items:()=>{const a=y.contains("dm-vnav-autohide"),f=y.contains("dm-vnav-collapsed"),v=S.has("nav_autohide")||S.has("nav_collapsed");return[{header:"Menu"},{label:f?"Expand menu":"Collapse to icons",icon:f?"maximize":"minimize",disabled:!w()||a||!L.has("collapsible"),action:()=>s(!f)},{label:a?"Keep on screen":"Hide when not in use",icon:a?"eye":"eye-off",disabled:!w(),action:()=>{S.set("nav_autohide",!a),e(!a)}},{type:"divider"},{label:"Reset to the site's setting",icon:"refresh",disabled:!v,action:()=>{S.remove("nav_autohide"),S.remove("nav_collapsed"),e(L.has("autohide")),y.toggle("dm-vnav-collapsed",!L.has("autohide")&&L.has("start-collapsed")),n()}}]}})}function me(p){p.classList.add("dm-nav-scroll-hide");let l=window.scrollY,x=!1;window.addEventListener("scroll",()=>{x||(x=!0,requestAnimationFrame(()=>{x=!1;const y=window.scrollY,L=p.matches(":focus-within")||p.querySelector(".navbar-dropdown.open, .navbar-collapse.show");y>l&&y>p.offsetHeight&&!L?p.classList.add("dm-nav-scrolled-away"):y<l&&p.classList.remove("dm-nav-scrolled-away"),l=y}))},{passive:!0})}function ue(p){let l={};try{l=JSON.parse(p.dataset.contents||"{}")}catch{}const x=p.closest(".site-main"),y=document.querySelector(".site-content"),L=new Set(String(l.exclude||"").split(",").map(i=>i.trim().toLowerCase()).filter(Boolean)),w=i=>!i.closest(".hero, .card, .accordion, .tabs, figure, #site-sidebar"),o=i=>(y?[...y.querySelectorAll(i)]:[]).filter(w).filter(c=>!L.has(c.textContent.trim().toLowerCase())),n=l.headings==="h2"?o("h2"):l.headings==="h3"?o("h3"):l.headings==="h2h3"?o("h2, h3"):o("h2").length?o("h2"):o("h3");if(!n.length){x?.classList.add("no-sidebar-items");return}const s=new Set([...document.querySelectorAll("[id]")].map(i=>i.id)),t=document.createElement("nav");t.className="dm-contents",t.setAttribute("aria-label",l.title||"Page contents"),l.hover&&(t.dataset.menuHover=l.hover),l.selected&&(t.dataset.menuActive=l.selected),l.accent&&t.style.setProperty("--dm-menu-highlight",l.accent.startsWith("#")?l.accent:`var(--dm-${l.accent})`),l.font&&(t.style.fontFamily=l.font),l.fontSize&&(t.style.fontSize=l.fontSize),l.fontWeight&&(t.style.fontWeight=l.fontWeight);const e=document.createElement("div");e.className="dm-contents-head";const a=document.createElement("p");a.className="dm-contents-title",a.textContent=l.title||"",e.appendChild(a),t.appendChild(e);const f=document.createElement("ol");f.className="dm-contents-list"+(l.numbered?" is-numbered":"");const v=[];for(const i of n){if(!i.id){const b=(i.textContent||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"section";let d=b;for(let m=2;s.has(d);m++)d=`${b}-${m}`;s.add(d),i.id=d}const c=document.createElement("li");c.className=`dm-contents-item dm-contents-${i.tagName.toLowerCase()}`;const h=document.createElement("a");h.href=`#${i.id}`,h.textContent=i.textContent.trim(),c.appendChild(h),f.appendChild(c),v.push([i,h])}if(t.appendChild(f),p.replaceChildren(t),l.scrollSpy!==!1&&"IntersectionObserver"in window){const i=new Map,c=()=>{const b=v.find(([d])=>i.get(d))||null;v.forEach(([,d])=>{const m=b&&d===b[1];d.classList.toggle("is-active",!!m),m?d.setAttribute("aria-current","location"):d.removeAttribute("aria-current")})},h=new IntersectionObserver(b=>{b.forEach(d=>i.set(d.target,d.isIntersecting)),c()},{rootMargin:"-80px 0px -60% 0px"});v.forEach(([b])=>h.observe(b))}if(x?.classList.contains("contents-collapsible")){const i=document.createElement("button");i.type="button",i.className="dm-contents-toggle";const c=()=>{const h=x.classList.contains("contents-collapsed");i.setAttribute("aria-expanded",String(!h)),i.setAttribute("aria-label",h?"Show the contents":"Hide the contents"),i.title=i.getAttribute("aria-label"),i.innerHTML=`<span data-icon="${h?"list":"x"}"></span>`,window.Domma?.icons&&Domma.icons.scan(i)};i.addEventListener("click",()=>{S.set("contents_collapsed",x.classList.toggle("contents-collapsed")),c()}),c(),e.insertBefore(i,e.firstChild)}if(x?.classList.contains("contents-floating")&&l.movable){t.classList.add("is-movable");const i=S.get("contents_position");i&&Number.isFinite(i.x)&&Object.assign(p.style,{left:`${i.x}px`,top:`${i.y}px`,right:"auto",bottom:"auto"}),e.addEventListener("pointerdown",c=>{if(c.target.closest("button"))return;const h=p.getBoundingClientRect(),b=c.clientX-h.left,d=c.clientY-h.top,m=g=>{const C=Math.max(0,Math.min(window.innerWidth-h.width,g.clientX-b)),M=Math.max(0,Math.min(window.innerHeight-40,g.clientY-d));Object.assign(p.style,{left:`${C}px`,top:`${M}px`,right:"auto",bottom:"auto"})},u=()=>{document.removeEventListener("pointermove",m),S.set("contents_position",{x:parseInt(p.style.left,10),y:parseInt(p.style.top,10)})};document.addEventListener("pointermove",m),document.addEventListener("pointerup",u,{once:!0})})}window.E&&typeof E.contextMenu=="function"&&E.contextMenu(p,{items:()=>{const i=x?.classList.contains("contents-collapsible"),c=x?.classList.contains("contents-collapsed"),h=v.find(([,b])=>b.classList.contains("is-active"));return[{header:"Contents"},...i?[{label:c?"Show the contents":"Collapse to a button",icon:c?"list":"minimize",action:()=>p.querySelector(".dm-contents-toggle")?.click()}]:[],{label:"Copy a link to this section",icon:"link",disabled:!h,action:async()=>{const b=`${location.origin}${location.pathname}#${h[0].id}`;try{await _.copyToClipboard(b),E.toast("Link copied.",{type:"success"})}catch{E.toast("Could not copy the link.",{type:"error"})}}},{label:"Back to the top",icon:"arrow-up",action:()=>window.scrollTo({top:0,behavior:"smooth"})}]}}),x?.classList.contains("contents-autohide")&&(p.addEventListener("pointerdown",i=>{i.pointerType!=="mouse"&&!p.classList.contains("is-revealed")&&(p.classList.add("is-revealed"),i.preventDefault())}),document.addEventListener("pointerdown",i=>{p.contains(i.target)||p.classList.remove("is-revealed")}))}
|
package/scripts/run-tests.mjs
CHANGED
|
@@ -61,6 +61,10 @@ try {
|
|
|
61
61
|
verbatimSymlinks: true,
|
|
62
62
|
filter: (src) => {
|
|
63
63
|
const rel = path.relative(ROOT, src);
|
|
64
|
+
// The dashboard tests expect empty traffic; live counters belong
|
|
65
|
+
// to the running site and must not enter the isolated test copy.
|
|
66
|
+
if ([path.join('content', 'analytics'), path.join('plugins', 'analytics', 'data'),
|
|
67
|
+
path.join('plugins', 'analytics', 'daily.json'), path.join('plugins', 'analytics', 'journeys.json')].includes(rel)) return false;
|
|
64
68
|
return !rel || !SKIP.has(rel.split(path.sep)[0]);
|
|
65
69
|
}
|
|
66
70
|
});
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
listPreviewLinks,
|
|
22
22
|
revokePreviewLink
|
|
23
23
|
} from '../../services/previewLinks.js';
|
|
24
|
-
import {getVersionCount} from '../../services/versions.js';
|
|
24
|
+
import {getVersionCount, versionAuthor} from '../../services/versions.js';
|
|
25
25
|
import {authenticate, requirePermission} from '../../middleware/auth.js';
|
|
26
26
|
import {canSeeArtefact, canSeePage} from '../../services/projects.js';
|
|
27
27
|
import {getConfig, saveConfig} from '../../config.js';
|
|
@@ -249,11 +249,11 @@ export async function pagesRoutes(fastify) {
|
|
|
249
249
|
// follow the move - drop them rather than leave them 404ing.
|
|
250
250
|
await deletePreviewLinksForPage(urlPath);
|
|
251
251
|
|
|
252
|
-
const page = await updatePage(newUrlPath, frontmatter || {}, body, {author: request.user
|
|
252
|
+
const page = await updatePage(newUrlPath, frontmatter || {}, body, {author: await versionAuthor(request.user)});
|
|
253
253
|
return page;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
const page = await updatePage(urlPath, frontmatter || {}, body, {author: request.user
|
|
256
|
+
const page = await updatePage(urlPath, frontmatter || {}, body, {author: await versionAuthor(request.user)});
|
|
257
257
|
return page;
|
|
258
258
|
});
|
|
259
259
|
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* GET /api/settings/_count - the sidebar badge: what is wrong with the settings
|
|
7
7
|
*/
|
|
8
8
|
import {normaliseOrigin} from '../../services/passwordReset.js';
|
|
9
|
-
import {getConfig, saveConfig} from '../../config.js';
|
|
9
|
+
import {config, getConfig, saveConfig} from '../../config.js';
|
|
10
|
+
import {faviconSource, generateFavicon} from '../../services/favicon.js';
|
|
11
|
+
import {saveMedia} from '../../services/content.js';
|
|
12
|
+
import {randomUUID} from 'node:crypto';
|
|
10
13
|
import {authenticate, requirePermission} from '../../middleware/auth.js';
|
|
11
14
|
import {createTransport} from '../../services/email.js';
|
|
12
15
|
import fs from 'fs/promises';
|
|
@@ -69,6 +72,22 @@ export async function settingsRoutes(fastify) {
|
|
|
69
72
|
const canRead = {preHandler: [authenticate, requirePermission('settings', 'read')]};
|
|
70
73
|
const canUpdate = {preHandler: [authenticate, requirePermission('settings', 'update')]};
|
|
71
74
|
|
|
75
|
+
fastify.post('/settings/favicon', canUpdate, async (request, reply) => {
|
|
76
|
+
let source;
|
|
77
|
+
try { source = faviconSource(request.body?.source, config.content.mediaDir); }
|
|
78
|
+
catch (err) { return reply.code(400).send({error: err.message}); }
|
|
79
|
+
try {
|
|
80
|
+
const input = await fs.readFile(source);
|
|
81
|
+
const icon = await generateFavicon(input);
|
|
82
|
+
const result = await saveMedia(`favicon-${randomUUID()}.ico`, icon);
|
|
83
|
+
return {...result, sizes: [16, 32, 48, 256]};
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err.code === 'ENOENT') return reply.code(404).send({error: 'That Media image no longer exists.'});
|
|
86
|
+
if (err.code) throw err;
|
|
87
|
+
return reply.code(400).send({error: 'Could not generate an icon from this image. Choose another image.'});
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
72
91
|
fastify.get('/settings', canRead, async (request, reply) => {
|
|
73
92
|
const config = getConfig('site');
|
|
74
93
|
const safeConfig = { ...config };
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
getVersion,
|
|
16
16
|
listVersions,
|
|
17
17
|
pruneVersions,
|
|
18
|
-
restoreVersion
|
|
18
|
+
restoreVersion,
|
|
19
|
+
versionAuthor
|
|
19
20
|
} from '../../services/versions.js';
|
|
20
21
|
import {authenticate, requirePermission} from '../../middleware/auth.js';
|
|
21
22
|
import {getPage} from '../../services/content.js';
|
|
@@ -66,7 +67,7 @@ export async function versionsRoutes(fastify) {
|
|
|
66
67
|
const urlPath = '/' + request.params['*'];
|
|
67
68
|
const {label} = request.body || {};
|
|
68
69
|
const entry = await createVersion(urlPath, {
|
|
69
|
-
author: request.user
|
|
70
|
+
author: await versionAuthor(request.user),
|
|
70
71
|
type: 'manual',
|
|
71
72
|
label: label || null
|
|
72
73
|
});
|
|
@@ -79,7 +80,7 @@ export async function versionsRoutes(fastify) {
|
|
|
79
80
|
const urlPath = '/' + request.params['*'];
|
|
80
81
|
const filename = decodeURIComponent(request.params.filename);
|
|
81
82
|
try {
|
|
82
|
-
await restoreVersion(urlPath, filename, request.user
|
|
83
|
+
await restoreVersion(urlPath, filename, await versionAuthor(request.user));
|
|
83
84
|
return {success: true};
|
|
84
85
|
} catch (err) {
|
|
85
86
|
if (err.code === 'ENOENT') return reply.status(404).send({error: 'Version not found'});
|
package/server/routes/public.js
CHANGED
|
@@ -12,6 +12,7 @@ import {renderPage} from '../services/renderer.js';
|
|
|
12
12
|
import {resolvePublishedPage} from '../services/pagePublishing.js';
|
|
13
13
|
import {buildRobotsTxt, generate as generateSitemap} from '../services/sitemap.js';
|
|
14
14
|
import {resolveViewer} from '../services/viewerSession.js';
|
|
15
|
+
import {validSessionCookie, newSessionCookie} from '../services/shortcodeExpiry.js';
|
|
15
16
|
import {touchPreviewLink, verifyPreviewToken} from '../services/previewLinks.js';
|
|
16
17
|
import {getEffectiveRoles} from '../services/userRoles.js';
|
|
17
18
|
import {hooks} from '../services/hooks.js';
|
|
@@ -220,7 +221,21 @@ export async function publicRoutes(fastify) {
|
|
|
220
221
|
// visitor's role for visibility filtering. The first getPage() above
|
|
221
222
|
// was anonymous - it only needed the page's metadata.
|
|
222
223
|
const render = async () => {
|
|
223
|
-
|
|
224
|
+
let expiry = null;
|
|
225
|
+
if (!isDraft && page.hasExpiry) {
|
|
226
|
+
const rawCookies = Object.fromEntries(String(request.headers.cookie || '').split(';').map(part => {
|
|
227
|
+
const at = part.indexOf('=');
|
|
228
|
+
return at < 0 ? ['', ''] : [part.slice(0, at).trim(), part.slice(at + 1).trim()];
|
|
229
|
+
}));
|
|
230
|
+
let sessionId = validSessionCookie(rawCookies.dm_shortcode_session, process.env.JWT_SECRET);
|
|
231
|
+
if (!sessionId) {
|
|
232
|
+
const fresh = newSessionCookie(process.env.JWT_SECRET);
|
|
233
|
+
sessionId = fresh.id;
|
|
234
|
+
reply.header('set-cookie', `dm_shortcode_session=${fresh.value}; Path=/; HttpOnly; SameSite=Lax${request.protocol === 'https' ? '; Secure' : ''}`);
|
|
235
|
+
}
|
|
236
|
+
expiry = {ip:request.ip || '', sessionId, secret:process.env.JWT_SECRET};
|
|
237
|
+
}
|
|
238
|
+
const pageForRole = await getPage(page.urlPath, {user: userObj, ...(expiry && {expiry})}) || page;
|
|
224
239
|
return renderPage(pageForRole, {
|
|
225
240
|
baseUrl,
|
|
226
241
|
user: userObj,
|
|
@@ -238,6 +253,14 @@ export async function publicRoutes(fastify) {
|
|
|
238
253
|
return reply.type('text/html; charset=utf-8').send(await render());
|
|
239
254
|
}
|
|
240
255
|
|
|
256
|
+
// Shortcode expiry depends on the visitor and changes on each view,
|
|
257
|
+
// so these pages must render against the expiry ledger on every request.
|
|
258
|
+
if (page.hasExpiry) {
|
|
259
|
+
reply.header('cache-control', 'no-store, private');
|
|
260
|
+
hooks.emit('content:pageViewed', {urlPath, title: page.title || ''});
|
|
261
|
+
return reply.type('text/html; charset=utf-8').send(await render());
|
|
262
|
+
}
|
|
263
|
+
|
|
241
264
|
// A row-scoped [view] shows each signed-in user their OWN rows, so the
|
|
242
265
|
// render differs per user, not per role set - two users sharing a role
|
|
243
266
|
// must not share an entry. Anonymous viewers of such a view get no rows
|
|
@@ -70,7 +70,7 @@ export async function getPageMtime(urlPath) {
|
|
|
70
70
|
export async function getPage(urlPath, opts = {}) {
|
|
71
71
|
const filePath = await resolveExistingFilePath(urlPath);
|
|
72
72
|
try {
|
|
73
|
-
return await readPageFile(filePath, {user: opts.user || null});
|
|
73
|
+
return await readPageFile(filePath, {user: opts.user || null, expiry:opts.expiry});
|
|
74
74
|
} catch {
|
|
75
75
|
return null;
|
|
76
76
|
}
|
|
@@ -98,9 +98,9 @@ export async function getPage(urlPath, opts = {}) {
|
|
|
98
98
|
*/
|
|
99
99
|
export async function buildPreviewPage(urlPath, frontmatter, body, opts = {}) {
|
|
100
100
|
const raw = serialiseMarkdown({...(frontmatter || {})}, body || '');
|
|
101
|
-
const {data, content, html, usedComponents, tags, perViewer} =
|
|
102
|
-
await parseMarkdown(raw, {user: opts.user || null, urlPath});
|
|
103
|
-
return {...data, urlPath, content, html, usedComponents, cacheTags: tags, perViewer};
|
|
101
|
+
const {data, content, html, usedComponents, tags, perViewer, hasExpiry} =
|
|
102
|
+
await parseMarkdown(raw, {user: opts.user || null, urlPath, expiry:{preview:true}});
|
|
103
|
+
return {...data, urlPath, content, html, usedComponents, cacheTags: tags, perViewer, hasExpiry};
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/**
|
|
@@ -411,8 +411,8 @@ async function readPageFile(filePath, opts = {}) {
|
|
|
411
411
|
const urlPath = filePathToUrlPath(filePath);
|
|
412
412
|
// urlPath is passed through so `[menu location="…"]` in the body resolves
|
|
413
413
|
// against menus bound to this page or its project, exactly as the navbar does.
|
|
414
|
-
const { data, content, html, usedComponents, tags, perViewer } =
|
|
415
|
-
await parseMarkdown(raw, {user: opts.user || null, urlPath});
|
|
414
|
+
const { data, content, html, usedComponents, tags, perViewer, hasExpiry } =
|
|
415
|
+
await parseMarkdown(raw, {user: opts.user || null, urlPath, expiry:opts.expiry || (opts.user ? {preview:true} : {})});
|
|
416
416
|
// parseMarkdown's `tags` are CACHE tags (`block:x`, `collection:y`), not
|
|
417
417
|
// the page's editorial tags. Spreading them last used to shadow the
|
|
418
418
|
// frontmatter's `tags`, so the editor loaded `block:feedback-card` into
|
|
@@ -420,7 +420,7 @@ async function readPageFile(filePath, opts = {}) {
|
|
|
420
420
|
// cache internals in its Tags column. Keep them apart.
|
|
421
421
|
// perViewer: a row-scoped [view] renders per user - the public route keeps
|
|
422
422
|
// such a page out of the role-keyed response cache for signed-in viewers.
|
|
423
|
-
return { ...data, urlPath, content, html, usedComponents, cacheTags: tags, perViewer };
|
|
423
|
+
return { ...data, urlPath, content, html, usedComponents, cacheTags: tags, perViewer, hasExpiry };
|
|
424
424
|
}
|
|
425
425
|
|
|
426
426
|
/**
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** Only local Media images can be read by the generator. */
|
|
5
|
+
export function faviconSource(source, mediaDir) {
|
|
6
|
+
if (typeof source !== 'string' || !source.startsWith('/media/')) throw new Error('Choose an image from Media.');
|
|
7
|
+
let name;
|
|
8
|
+
try { name = decodeURIComponent(source.slice(7)); } catch { throw new Error('Invalid image address.'); }
|
|
9
|
+
if (!name || name.includes('/') || name.includes('\\') || name === '.' || name === '..'
|
|
10
|
+
|| !/\.(png|jpe?g|webp|gif|avif|svg|tiff?|bmp)$/i.test(name)) throw new Error('Choose a PNG, JPEG, WebP, GIF, AVIF or SVG image from Media.');
|
|
11
|
+
return path.resolve(mediaDir, name);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** ICO directory with PNG images at standard browser/desktop sizes. */
|
|
15
|
+
export async function generateFavicon(input) {
|
|
16
|
+
const sizes = [16, 32, 48, 256];
|
|
17
|
+
const images = await Promise.all(sizes.map(size => sharp(input, {limitInputPixels: 40_000_000})
|
|
18
|
+
.rotate().resize(size, size, {fit: 'contain', background: {r: 0, g: 0, b: 0, alpha: 0}}).png().toBuffer()));
|
|
19
|
+
const header = Buffer.alloc(6 + 16 * sizes.length);
|
|
20
|
+
header.writeUInt16LE(1, 2);
|
|
21
|
+
header.writeUInt16LE(sizes.length, 4);
|
|
22
|
+
let offset = header.length;
|
|
23
|
+
images.forEach((image, i) => {
|
|
24
|
+
const entry = 6 + 16 * i;
|
|
25
|
+
header[entry] = header[entry + 1] = sizes[i] === 256 ? 0 : sizes[i];
|
|
26
|
+
header.writeUInt16LE(1, entry + 4);
|
|
27
|
+
header.writeUInt16LE(32, entry + 6);
|
|
28
|
+
header.writeUInt32LE(image.length, entry + 8);
|
|
29
|
+
header.writeUInt32LE(offset, entry + 12);
|
|
30
|
+
offset += image.length;
|
|
31
|
+
});
|
|
32
|
+
return Buffer.concat([header, ...images]);
|
|
33
|
+
}
|
|
@@ -18,8 +18,10 @@ import {buildMenuNav} from './menuRender.js';
|
|
|
18
18
|
import {checkVisibility} from '../middleware/auth.js';
|
|
19
19
|
import {getRoleHierarchy, getRoleLevel} from './roles.js';
|
|
20
20
|
import {buildTextStyle} from '../../public/js/text-style.mjs';
|
|
21
|
+
import {buildScribeStyle} from '../../public/js/scribe-style.mjs';
|
|
21
22
|
import {CHARACTERS, resolveCharacterSize} from '../../public/js/celebrations/character-catalog.mjs';
|
|
22
23
|
import {publicButtonSettings} from '../../public/js/form-submit.mjs';
|
|
24
|
+
import {applyShortcodeExpiry} from './shortcodeExpiry.js';
|
|
23
25
|
|
|
24
26
|
const __dirname_md = path.dirname(fileURLToPath(import.meta.url));
|
|
25
27
|
const BLOCKS_DIR = path.resolve(__dirname_md, '../../content/blocks');
|
|
@@ -1680,7 +1682,7 @@ export function parseShortcodeAttrs(attrStr) {
|
|
|
1680
1682
|
*/
|
|
1681
1683
|
let scrubCallId = 0;
|
|
1682
1684
|
|
|
1683
|
-
export function scrubCodeRegions(markdown) {
|
|
1685
|
+
export function scrubCodeRegions(markdown, preserveIndented = false) {
|
|
1684
1686
|
const callId = ++scrubCallId;
|
|
1685
1687
|
const store = [];
|
|
1686
1688
|
const placeholder = (s) => {
|
|
@@ -1694,7 +1696,10 @@ export function scrubCodeRegions(markdown) {
|
|
|
1694
1696
|
// Collection bodies may already be HTML: inline examples remain literal too.
|
|
1695
1697
|
scrubbed = scrubbed.replace(/<code\b[^>]*>[\s\S]*?<\/code>/gi, placeholder);
|
|
1696
1698
|
// Fenced code blocks (``` or ~~~, with optional language tag)
|
|
1697
|
-
scrubbed =
|
|
1699
|
+
scrubbed = preserveIndented
|
|
1700
|
+
? scrubbed.replace(/^ {0,3}(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:\n {0,3}\1[ \t]*$|(?![\s\S]))/gm, placeholder)
|
|
1701
|
+
: scrubbed.replace(/^(`{3,}|~{3,})[^\n]*\n[\s\S]*?\n\1[ \t]*$/gm, placeholder);
|
|
1702
|
+
if (preserveIndented) scrubbed = scrubbed.replace(/^(?: {4}|\t)[^\r\n]*/gm, placeholder);
|
|
1698
1703
|
// Inline code spans (single or multiple backticks, non-greedy)
|
|
1699
1704
|
scrubbed = scrubbed.replace(/`+[^`\n]+`+/g, placeholder);
|
|
1700
1705
|
|
|
@@ -1847,12 +1852,14 @@ function processEffectsBlocks(markdown) {
|
|
|
1847
1852
|
if (typeof attrs[key] === 'string') attrs[key] = attrs[key].replace(/&(amp|quot|apos|lt|gt);/g, (_, name) => entities[name]);
|
|
1848
1853
|
}
|
|
1849
1854
|
const dataAttrs = Object.entries(attrs).map(([k, v]) => ` data-fx-${k}="${escapeAttr(v)}"`).join('');
|
|
1855
|
+
const scribeStyle = buildScribeStyle(attrs);
|
|
1856
|
+
const styleAttrs = scribeStyle ? ` style="${escapeAttr(scribeStyle)}"` : '';
|
|
1850
1857
|
const hasActions = /\[\s*(render|wait|undo|redraw)\b/i.test(body);
|
|
1851
1858
|
if (!hasActions) {
|
|
1852
1859
|
// Restore code placeholders before the nested parse (tabs-handler
|
|
1853
1860
|
// pattern) so code spans/fences render as <code>, not raw backticks.
|
|
1854
1861
|
const innerHtml = marked.parse(processCardBlocks(processGridBlocks(processInlineShortcodes(restore(body.trim())))));
|
|
1855
|
-
return `<div class="dm-fx-scribe"${dataAttrs}>${innerHtml}</div>\n`;
|
|
1862
|
+
return `<div class="dm-fx-scribe"${dataAttrs}${styleAttrs}>${innerHtml}</div>\n`;
|
|
1856
1863
|
}
|
|
1857
1864
|
const actions = [];
|
|
1858
1865
|
const actionRe = /\[render([^\]]*)\]([\s\S]*?)\[\/render\]|\[wait\]([\s\S]*?)\[\/wait\]|\[undo([^\]]*?)\/\]|\[redraw([^\]]*)\]([\s\S]*?)\[\/redraw\]/gi;
|
|
@@ -1881,7 +1888,7 @@ function processEffectsBlocks(markdown) {
|
|
|
1881
1888
|
}
|
|
1882
1889
|
}
|
|
1883
1890
|
if (!actions.length) return '';
|
|
1884
|
-
return `<span class="dm-fx-scribe"${dataAttrs} data-fx-actions="${escapeAttr(JSON.stringify(actions))}"></span>\n`;
|
|
1891
|
+
return `<span class="dm-fx-scribe"${dataAttrs}${styleAttrs} data-fx-actions="${escapeAttr(JSON.stringify(actions))}"></span>\n`;
|
|
1885
1892
|
});
|
|
1886
1893
|
|
|
1887
1894
|
for (const name of ['reveal', 'breathe', 'pulse', 'shake', 'scramble', 'ripple', 'twinkle']) {
|
|
@@ -4541,10 +4548,20 @@ export async function parseMarkdown(raw, opts = {}) {
|
|
|
4541
4548
|
// → listgroup → spacer → center → icon → demo → embed → form → hero → table
|
|
4542
4549
|
// → badge → text → button → link → cta → grid → card (+ box) → banner → slideover
|
|
4543
4550
|
// → marked → sanitize → afterParse
|
|
4544
|
-
|
|
4551
|
+
// Comments must be removed before shortcodes are expanded. Protect code
|
|
4552
|
+
// examples first; four-space-indented lines remain Markdown code too.
|
|
4553
|
+
const comments = scrubCodeRegions(content, true);
|
|
4554
|
+
const uncommented = comments.restore(comments.scrubbed
|
|
4555
|
+
.replace(/<!--[\s\S]*?(?:-->|$)/g, '')
|
|
4556
|
+
.replace(/^[ ]{0,3}\/\/[^\r\n]*/gm, ''));
|
|
4557
|
+
const expirySafe = scrubCodeRegions(uncommented, true);
|
|
4558
|
+
const expiry = await applyShortcodeExpiry(expirySafe.scrubbed, {
|
|
4559
|
+
...(opts.expiry || {}), urlPath: opts.urlPath || '/', parseAttrs: parseShortcodeAttrs
|
|
4560
|
+
});
|
|
4561
|
+
const preprocessed = applyTransforms('markdown:beforeParse', expirySafe.restore(expiry.markdown));
|
|
4545
4562
|
const {output: withComponents, used: usedComponents} = collectAndRewriteComponents(preprocessed);
|
|
4546
4563
|
const tagSet = new Set();
|
|
4547
|
-
const flags = {perViewer: false};
|
|
4564
|
+
const flags = {perViewer: false, hasExpiry:expiry.hasExpiry};
|
|
4548
4565
|
const withCollection = await processCollectionBlocks(withComponents, tagSet);
|
|
4549
4566
|
const withView = await processViewBlocks(withCollection, tagSet, opts.user || null, flags);
|
|
4550
4567
|
const withStaticBlock = await processStaticBlocks(withView, tagSet);
|
|
@@ -4658,7 +4675,7 @@ export async function parseMarkdown(raw, opts = {}) {
|
|
|
4658
4675
|
const allowed = new Set(_dmTagAllowlist.map(t => t.replace(/^dm-/, '')));
|
|
4659
4676
|
const filteredUsed = [...usedComponents].filter(name => allowed.has(name));
|
|
4660
4677
|
|
|
4661
|
-
return {data, content, html, usedComponents: filteredUsed, tags: [...tagSet], perViewer: flags.perViewer};
|
|
4678
|
+
return {data, content, html, usedComponents: filteredUsed, tags: [...tagSet], perViewer: flags.perViewer, hasExpiry:flags.hasExpiry};
|
|
4662
4679
|
}
|
|
4663
4680
|
|
|
4664
4681
|
/**
|