From 466334149740335f437524c792ab6b6b99cf912d Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Sun, 21 Apr 2024 19:09:58 +0900 Subject: [PATCH] Improved page navigation and import of on-demand assets --- .../vendor/alpine-ext/async-alpine@1.2.2.js | 1 + public/vendor/alpine-ext/morph@3.13.8.js | 1 + public/vendor/htmx-ext/alpine-morph@1.9.12.js | 16 ++ public/vendor/htmx-ext/head-support@1.9.12.js | 141 ++++++++++++++++++ src/routes/about.lisp | 10 +- src/routes/index.lisp | 11 +- src/scripts/pages/about.js | 9 ++ src/scripts/pages/index.js | 9 ++ src/styles/pages/about.css | 2 +- src/styles/pages/index.css | 2 +- src/view/asset.lisp | 31 ++++ src/view/components/document.lisp | 67 +++------ src/view/components/layout.lisp | 1 + src/view/renderer.lisp | 2 +- 14 files changed, 248 insertions(+), 55 deletions(-) create mode 100644 public/vendor/alpine-ext/async-alpine@1.2.2.js create mode 100644 public/vendor/alpine-ext/morph@3.13.8.js create mode 100644 public/vendor/htmx-ext/alpine-morph@1.9.12.js create mode 100644 public/vendor/htmx-ext/head-support@1.9.12.js create mode 100644 src/scripts/pages/about.js create mode 100644 src/scripts/pages/index.js create mode 100644 src/view/asset.lisp diff --git a/public/vendor/alpine-ext/async-alpine@1.2.2.js b/public/vendor/alpine-ext/async-alpine@1.2.2.js new file mode 100644 index 0000000..43a2b91 --- /dev/null +++ b/public/vendor/alpine-ext/async-alpine@1.2.2.js @@ -0,0 +1 @@ +(()=>{var d=Object.defineProperty;var w=e=>d(e,"__esModule",{value:!0});var A=(e,t)=>{w(e);for(var i in t)d(e,i,{get:t[i],enumerable:!0})};var o={};A(o,{eager:()=>h,event:()=>f,idle:()=>c,media:()=>_,visible:()=>m});var v=()=>!0,h=v;var b=({component:e,argument:t})=>new Promise(i=>{if(t)window.addEventListener(t,()=>i(),{once:!0});else{let s=n=>{n.detail.id===e.id&&(window.removeEventListener("async-alpine:load",s),i())};window.addEventListener("async-alpine:load",s)}}),f=b;var $=()=>new Promise(e=>{"requestIdleCallback"in window?window.requestIdleCallback(e):setTimeout(e,200)}),c=$;var E=({argument:e})=>new Promise(t=>{if(!e)return console.log("Async Alpine: media strategy requires a media query. Treating as 'eager'"),t();let i=window.matchMedia(`(${e})`);i.matches?t():i.addEventListener("change",t,{once:!0})}),_=E;var q=({component:e,argument:t})=>new Promise(i=>{let s=t||"0px 0px 0px 0px",n=new IntersectionObserver(a=>{a[0].isIntersecting&&(n.disconnect(),i())},{rootMargin:s});n.observe(e.el)}),m=q;function p(e){let t=P(e),i=x(t);return i.type==="method"?{type:"expression",operator:"&&",parameters:[i]}:i}function P(e){let t=/\s*([()])\s*|\s*(\|\||&&|\|)\s*|\s*((?:[^()&|]+\([^()]+\))|[^()&|]+)\s*/g,i=[],s;for(;(s=t.exec(e))!==null;){let[,n,a,r]=s;if(n!==void 0)i.push({type:"parenthesis",value:n});else if(a!==void 0)i.push({type:"operator",value:a==="|"?"&&":a});else{let u={type:"method",method:r.trim()};r.includes("(")&&(u.method=r.substring(0,r.indexOf("(")).trim(),u.argument=r.substring(r.indexOf("(")+1,r.indexOf(")"))),r.method==="immediate"&&(r.method="eager"),i.push(u)}}return i}function x(e){let t=g(e);for(;e.length>0&&(e[0].value==="&&"||e[0].value==="|"||e[0].value==="||");){let i=e.shift().value,s=g(e);t.type==="expression"&&t.operator===i?t.parameters.push(s):t={type:"expression",operator:i,parameters:[t,s]}}return t}function g(e){if(e[0].value==="("){e.shift();let t=x(e);return e[0].value===")"&&e.shift(),t}else return e.shift()}var y="__internal_",l={Alpine:null,_options:{prefix:"ax-",alpinePrefix:"x-",root:"load",inline:"load-src",defaultStrategy:"eager"},_alias:!1,_data:{},_realIndex:0,get _index(){return this._realIndex++},init(e,t={}){return this.Alpine=e,this._options={...this._options,...t},this},start(){return this._processInline(),this._setupComponents(),this._mutations(),this},data(e,t=!1){return this._data[e]={loaded:!1,download:t},this},url(e,t){!e||!t||(this._data[e]||this.data(e),this._data[e].download=()=>import(this._parseUrl(t)))},alias(e){this._alias=e},_processInline(){let e=document.querySelectorAll(`[${this._options.prefix}${this._options.inline}]`);for(let t of e)this._inlineElement(t)},_inlineElement(e){let t=e.getAttribute(`${this._options.alpinePrefix}data`),i=e.getAttribute(`${this._options.prefix}${this._options.inline}`);if(!t||!i)return;let s=this._parseName(t);this.url(s,i)},_setupComponents(){let e=document.querySelectorAll(`[${this._options.prefix}${this._options.root}]`);for(let t of e)this._setupComponent(t)},_setupComponent(e){let t=e.getAttribute(`${this._options.alpinePrefix}data`);e.setAttribute(`${this._options.alpinePrefix}ignore`,"");let i=this._parseName(t),s=e.getAttribute(`${this._options.prefix}${this._options.root}`)||this._options.defaultStrategy;this._componentStrategy({name:i,strategy:s,el:e,id:e.id||this._index})},async _componentStrategy(e){let t=p(e.strategy);await this._generateRequirements(e,t),await this._download(e.name),this._activate(e)},_generateRequirements(e,t){if(t.type==="expression"){if(t.operator==="&&")return Promise.all(t.parameters.map(i=>this._generateRequirements(e,i)));if(t.operator==="||")return Promise.any(t.parameters.map(i=>this._generateRequirements(e,i)))}return o[t.method]?o[t.method]({component:e,argument:t.argument}):!1},async _download(e){if(e.startsWith(y)||(this._handleAlias(e),!this._data[e]||this._data[e].loaded))return;let t=await this._getModule(e);this.Alpine.data(e,t),this._data[e].loaded=!0},async _getModule(e){if(!this._data[e])return;let t=await this._data[e].download(e);return typeof t=="function"?t:t[e]||t.default||Object.values(t)[0]||!1},_activate(e){this.Alpine.destroyTree(e.el);let t=`${this._options.alpinePrefix}ignore`;e.el.removeAttribute(t),e.el._x_ignore=!1,!this._anyParent(e.el,i=>i.hasAttribute(t))&&this.Alpine.initTree(e.el)},_mutations(){new MutationObserver(t=>{for(let i of t)if(!!i.addedNodes)for(let s of i.addedNodes){if(s.nodeType!==1)continue;s.hasAttribute(`${this._options.prefix}${this._options.root}`)&&this._mutationEl(s),s.querySelectorAll(`[${this._options.prefix}${this._options.root}]`).forEach(a=>this._mutationEl(a))}}).observe(document,{attributes:!0,childList:!0,subtree:!0})},_mutationEl(e){e.hasAttribute(`${this._options.prefix}${this._options.inline}`)&&this._inlineElement(e),this._setupComponent(e)},_handleAlias(e){if(!(!this._alias||this._data[e])){if(typeof this._alias=="function"){this.data(e,this._alias);return}this.url(e,this._alias.replaceAll("[name]",e))}},_parseName(e){return(e||"").split(/[({]/g)[0]||`${y}${this._index}`},_parseUrl(e){return new RegExp("^(?:[a-z+]+:)?//","i").test(e)?e:new URL(e,document.baseURI).href},_anyParent(e,t){return!e||e.nodeName==="HTML"?!1:t(e)?e:this._anyParent(e.parentElement,t)}};document.addEventListener("alpine:init",()=>{window.AsyncAlpine=l,l.init(Alpine,window.AsyncAlpineOptions||{}),document.dispatchEvent(new CustomEvent("async-alpine:init")),l.start()});})(); diff --git a/public/vendor/alpine-ext/morph@3.13.8.js b/public/vendor/alpine-ext/morph@3.13.8.js new file mode 100644 index 0000000..d8502c1 --- /dev/null +++ b/public/vendor/alpine-ext/morph@3.13.8.js @@ -0,0 +1 @@ +(()=>{function k(u,l,o){Y();let g,h,y,B,O,E,v,T,_,A;function V(e={}){let n=a=>a.getAttribute("key"),d=()=>{};O=e.updating||d,E=e.updated||d,v=e.removing||d,T=e.removed||d,_=e.adding||d,A=e.added||d,y=e.key||n,B=e.lookahead||!1}function D(e,n){if(W(e,n))return q(e,n);let d=!1;if(!b(O,e,n,()=>d=!0)){if(e.nodeType===1&&window.Alpine&&window.Alpine.cloneNode(e,n),X(n)){$(e,n),E(e,n);return}d||G(e,n),E(e,n),L(e,n)}}function W(e,n){return e.nodeType!=n.nodeType||e.nodeName!=n.nodeName||x(e)!=x(n)}function q(e,n){if(b(v,e))return;let d=n.cloneNode(!0);b(_,d)||(e.replaceWith(d),T(e),A(d))}function $(e,n){let d=n.nodeValue;e.nodeValue!==d&&(e.nodeValue=d)}function G(e,n){if(e._x_transitioning||e._x_isShown&&!n._x_isShown||!e._x_isShown&&n._x_isShown)return;let d=Array.from(e.attributes),a=Array.from(n.attributes);for(let i=d.length-1;i>=0;i--){let t=d[i].name;n.hasAttribute(t)||e.removeAttribute(t)}for(let i=a.length-1;i>=0;i--){let t=a[i].name,m=a[i].value;e.getAttribute(t)!==m&&e.setAttribute(t,m)}}function L(e,n){e._x_teleport&&(e=e._x_teleport),n._x_teleport&&(n=n._x_teleport);let d=H(e.children),a={},i=I(n),t=I(e);for(;i;){Z(i,t);let s=x(i),p=x(t);if(!t)if(s&&a[s]){let r=a[s];e.appendChild(r),t=r}else{if(!b(_,i)){let r=i.cloneNode(!0);e.appendChild(r),A(r)}i=c(n,i);continue}let C=r=>r&&r.nodeType===8&&r.textContent==="[if BLOCK]><![endif]",S=r=>r&&r.nodeType===8&&r.textContent==="[if ENDBLOCK]><![endif]";if(C(i)&&C(t)){let r=0,N=t;for(;t;){let f=c(e,t);if(C(f))r++;else if(S(f)&&r>0)r--;else if(S(f)&&r===0){t=f;break}t=f}let R=t;r=0;let j=i;for(;i;){let f=c(n,i);if(C(f))r++;else if(S(f)&&r>0)r--;else if(S(f)&&r===0){i=f;break}i=f}let z=i,J=new w(N,R),Q=new w(j,z);L(J,Q);continue}if(t.nodeType===1&&B&&!t.isEqualNode(i)){let r=c(n,i),N=!1;for(;!N&&r;)r.nodeType===1&&t.isEqualNode(r)&&(N=!0,t=K(e,i,t),p=x(t)),r=c(n,r)}if(s!==p){if(!s&&p){a[p]=t,t=K(e,i,t),a[p].remove(),t=c(e,t),i=c(n,i);continue}if(s&&!p&&d[s]&&(t.replaceWith(d[s]),t=d[s]),s&&p){let r=d[s];if(r)a[p]=t,t.replaceWith(r),t=r;else{a[p]=t,t=K(e,i,t),a[p].remove(),t=c(e,t),i=c(n,i);continue}}}let P=t&&c(e,t);D(t,i),i=i&&c(n,i),t=P}let m=[];for(;t;)b(v,t)||m.push(t),t=c(e,t);for(;m.length;){let s=m.shift();s.remove(),T(s)}}function x(e){return e&&e.nodeType===1&&y(e)}function H(e){let n={};for(let d of e){let a=x(d);a&&(n[a]=d)}return n}function K(e,n,d){if(!b(_,n)){let a=n.cloneNode(!0);return e.insertBefore(a,d),A(a),a}return n}return V(o),g=u,h=typeof l=="string"?U(l):l,window.Alpine&&window.Alpine.closestDataStack&&!u._x_dataStack&&(h._x_dataStack=window.Alpine.closestDataStack(u),h._x_dataStack&&window.Alpine.cloneNode(u,h)),D(u,h),g=void 0,h=void 0,u}k.step=()=>{};k.log=()=>{};function b(u,...l){let o=!1;return u(...l,()=>o=!0),o}var F=!1;function U(u){let l=document.createElement("template");return l.innerHTML=u,l.content.firstElementChild}function X(u){return u.nodeType===3||u.nodeType===8}var w=class{constructor(l,o){this.startComment=l,this.endComment=o}get children(){let l=[],o=this.startComment.nextSibling;for(;o&&o!==this.endComment;)l.push(o),o=o.nextSibling;return l}appendChild(l){this.endComment.before(l)}get firstChild(){let l=this.startComment.nextSibling;if(l!==this.endComment)return l}nextNode(l){let o=l.nextSibling;if(o!==this.endComment)return o}insertBefore(l,o){return o.before(l),l}};function I(u){return u.firstChild}function c(u,l){let o;return u instanceof w?o=u.nextNode(l):o=l.nextSibling,o}function Y(){if(F)return;F=!0;let u=Element.prototype.setAttribute,l=document.createElement("div");Element.prototype.setAttribute=function(g,h){if(!g.includes("@"))return u.call(this,g,h);l.innerHTML=`<span ${g}="${h}"></span>`;let y=l.firstElementChild.getAttributeNode(g);l.firstElementChild.removeAttributeNode(y),this.setAttributeNode(y)}}function Z(u,l){let o=l&&l._x_bindings&&l._x_bindings.id;o&&(u.setAttribute("id",o),u.id=o)}function M(u){u.morph=k}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(M)});})(); diff --git a/public/vendor/htmx-ext/alpine-morph@1.9.12.js b/public/vendor/htmx-ext/alpine-morph@1.9.12.js new file mode 100644 index 0000000..1872dae --- /dev/null +++ b/public/vendor/htmx-ext/alpine-morph@1.9.12.js @@ -0,0 +1,16 @@ +htmx.defineExtension('alpine-morph', { + isInlineSwap: function (swapStyle) { + return swapStyle === 'morph'; + }, + handleSwap: function (swapStyle, target, fragment) { + if (swapStyle === 'morph') { + if (fragment.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + Alpine.morph(target, fragment.firstElementChild); + return [target]; + } else { + Alpine.morph(target, fragment.outerHTML); + return [target]; + } + } + } +}); \ No newline at end of file diff --git a/public/vendor/htmx-ext/head-support@1.9.12.js b/public/vendor/htmx-ext/head-support@1.9.12.js new file mode 100644 index 0000000..67cfc69 --- /dev/null +++ b/public/vendor/htmx-ext/head-support@1.9.12.js @@ -0,0 +1,141 @@ +//========================================================== +// head-support.js +// +// An extension to htmx 1.0 to add head tag merging. +//========================================================== +(function(){ + + var api = null; + + function log() { + //console.log(arguments); + } + + function mergeHead(newContent, defaultMergeStrategy) { + + if (newContent && newContent.indexOf('<head') > -1) { + const htmlDoc = document.createElement("html"); + // remove svgs to avoid conflicts + var contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, ''); + // extract head tag + var headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im); + + // if the head tag exists... + if (headTag) { + + var added = [] + var removed = [] + var preserved = [] + var nodesToAppend = [] + + htmlDoc.innerHTML = headTag; + var newHeadTag = htmlDoc.querySelector("head"); + var currentHead = document.head; + + if (newHeadTag == null) { + return; + } else { + // put all new head elements into a Map, by their outerHTML + var srcToNewHeadNodes = new Map(); + for (const newHeadChild of newHeadTag.children) { + srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); + } + } + + + + // determine merge strategy + var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy; + + // get the current head + for (const currentHeadElt of currentHead.children) { + + // If the current head element is in the map + var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); + var isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval"; + var isPreserved = api.getAttributeValue(currentHeadElt, "hx-preserve") === "true"; + if (inNewContent || isPreserved) { + if (isReAppended) { + // remove the current version and let the new version replace it and re-execute + removed.push(currentHeadElt); + } else { + // this element already exists and should not be re-appended, so remove it from + // the new content map, preserving it in the DOM + srcToNewHeadNodes.delete(currentHeadElt.outerHTML); + preserved.push(currentHeadElt); + } + } else { + if (mergeStrategy === "append") { + // we are appending and this existing element is not new content + // so if and only if it is marked for re-append do we do anything + if (isReAppended) { + removed.push(currentHeadElt); + nodesToAppend.push(currentHeadElt); + } + } else { + // if this is a merge, we remove this content since it is not in the new head + if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: currentHeadElt}) !== false) { + removed.push(currentHeadElt); + } + } + } + } + + // Push the tremaining new head elements in the Map into the + // nodes to append to the head tag + nodesToAppend.push(...srcToNewHeadNodes.values()); + log("to append: ", nodesToAppend); + + for (const newNode of nodesToAppend) { + log("adding: ", newNode); + var newElt = document.createRange().createContextualFragment(newNode.outerHTML); + log(newElt); + if (api.triggerEvent(document.body, "htmx:addingHeadElement", {headElement: newElt}) !== false) { + currentHead.appendChild(newElt); + added.push(newElt); + } + } + + // remove all removed elements, after we have appended the new elements to avoid + // additional network requests for things like style sheets + for (const removedElement of removed) { + if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: removedElement}) !== false) { + currentHead.removeChild(removedElement); + } + } + + api.triggerEvent(document.body, "htmx:afterHeadMerge", {added: added, kept: preserved, removed: removed}); + } + } + } + + htmx.defineExtension("head-support", { + init: function(apiRef) { + // store a reference to the internal API. + api = apiRef; + + htmx.on('htmx:afterSwap', function(evt){ + var serverResponse = evt.detail.xhr.response; + if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) { + mergeHead(serverResponse, evt.detail.boosted ? "merge" : "append"); + } + }) + + htmx.on('htmx:historyRestore', function(evt){ + if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) { + if (evt.detail.cacheMiss) { + mergeHead(evt.detail.serverResponse, "merge"); + } else { + mergeHead(evt.detail.item.head, "merge"); + } + } + }) + + htmx.on('htmx:historyItemCreated', function(evt){ + var historyItem = evt.detail.item; + historyItem.head = document.head.outerHTML; + }) + } + }); + +})() \ No newline at end of file diff --git a/src/routes/about.lisp b/src/routes/about.lisp index 6edd7c9..0fbea6f 100644 --- a/src/routes/about.lisp +++ b/src/routes/about.lisp @@ -11,10 +11,14 @@ (pi:define-element page () (pi:h - (section :data-css "pages/about" + (section (view:asset-props :style "pages/about" + :script "pages/about" + :x-data "aboutPageState") (h1 "About") - (a :href "/" - "top")))) + (a :href "/" :hx-boost "true" + "top") + (button :@click "decrement()" + (span :x-text "count"))))) (defun handle-get (params) (declare (ignore params)) diff --git a/src/routes/index.lisp b/src/routes/index.lisp index 5311ce1..7592f8c 100644 --- a/src/routes/index.lisp +++ b/src/routes/index.lisp @@ -7,13 +7,18 @@ (pi:define-element page () (pi:h - (section :data-css "pages/index" + (section (view:asset-props :style "pages/index" + :script "pages/index" + :x-data "indexPageState") (h1 "Hello, World!") - (a :href "/about" + (a :href "/about" :hx-boost "true" "About") (button :x-data t :@click "$store.darkMode.toggle()" - "Toggle theme")))) + "Toggle theme") + (button + :@click "increment()" + (span :x-text "count"))))) (defun handle-get (params) (declare (ignore params)) diff --git a/src/scripts/pages/about.js b/src/scripts/pages/about.js new file mode 100644 index 0000000..7426ded --- /dev/null +++ b/src/scripts/pages/about.js @@ -0,0 +1,9 @@ +export function aboutPageState() { + return { + count: 0, + + decrement() { + this.count-- + } + } +} diff --git a/src/scripts/pages/index.js b/src/scripts/pages/index.js new file mode 100644 index 0000000..7ebac58 --- /dev/null +++ b/src/scripts/pages/index.js @@ -0,0 +1,9 @@ +export function indexPageState() { + return { + count: 0, + + increment() { + this.count++ + } + } +} diff --git a/src/styles/pages/about.css b/src/styles/pages/about.css index 1d792a6..24c6147 100644 --- a/src/styles/pages/about.css +++ b/src/styles/pages/about.css @@ -1,4 +1,4 @@ -@scope ([data-css='pages/about']) { +@scope ([data-style='pages/about']) { :scope { height: 100svh; display: grid; diff --git a/src/styles/pages/index.css b/src/styles/pages/index.css index 463170b..e743a0f 100644 --- a/src/styles/pages/index.css +++ b/src/styles/pages/index.css @@ -1,4 +1,4 @@ -@scope ([data-css='pages/index']) { +@scope ([data-style='pages/index']) { :scope { height: 100svh; display: grid; diff --git a/src/view/asset.lisp b/src/view/asset.lisp new file mode 100644 index 0000000..810531e --- /dev/null +++ b/src/view/asset.lisp @@ -0,0 +1,31 @@ +(defpackage #:hp/view/asset + (:use #:cl) + (:local-nicknames (#:re #:cl-ppcre)) + (:export #:get-css-links + #:asset-props)) +(in-package #:hp/view/asset) + +(defun detect-data-props (html-str data-prop-name) + (remove-duplicates (re:all-matches-as-strings (format nil + "(?<=~a=\")[^\"]*(?=\")" + data-prop-name) + html-str) + :test #'string=)) + +(defun data-props->asset-links (parent-path extension data-props) + (mapcar (lambda (data-prop) + (concatenate 'string parent-path data-prop extension)) + data-props)) + +(defun get-css-links (html-str) + (data-props->asset-links "/styles/" + ".css" + (detect-data-props html-str "data-style"))) + +(defun asset-props (&key style script x-data) + (append (and style `(:data-style ,style)) + (and script x-data + `(:ax-load t + :ax-load-src ,(format nil "/scripts/~a.js" script) + :x-ignore t + :x-data ,x-data)))) diff --git a/src/view/components/document.lisp b/src/view/components/document.lisp index 0707ef0..e88e6fd 100644 --- a/src/view/components/document.lisp +++ b/src/view/components/document.lisp @@ -1,55 +1,19 @@ (defpackage #:hp/view/components/document (:use #:cl) - (:local-nicknames (#:re #:cl-ppcre)) (:local-nicknames (#:pi #:piccolo)) - (:export #:document)) + (:local-nicknames (#:asset #:hp/view/asset)) + (:export #:document + #:partial-document)) (in-package #:hp/view/components/document) -(defun detect-data-props (html-str data-prop-name) - (remove-duplicates (re:all-matches-as-strings (format nil - "(?<=~a=\")[^\"]*(?=\")" - data-prop-name) - html-str) - :test #'string=)) - -(defun data-props->asset-links (parent-path extension data-props) - (mapcar (lambda (data-prop) - (concatenate 'string parent-path data-prop extension)) - data-props)) - -(defun get-css-links (html-str) - (data-props->asset-links "/styles/" - ".css" - (detect-data-props html-str "data-css"))) - -(defun get-js-links (html-str) - (data-props->asset-links "/scripts/" - ".js" - (detect-data-props html-str "data-js"))) - -(pi:define-element scripts (srcs) - (pi:h - (<> - (mapcar (lambda (src) - (script :src src :defer t)) - srcs)))) - -(pi:define-element stylesheets (hrefs) - (pi:h - (<> - (mapcar (lambda (href) - (link :rel "stylesheet" :type "text/css" :href href)) - hrefs)))) - -(pi:define-element on-demand-assets (component) - (let* ((pi:*escape-html* nil) - (html-str (pi:elem-str component)) - (css-links (get-css-links html-str)) - (js-links (get-js-links html-str))) +(pi:define-element on-demand-stylesheets () + (let* ((html-str (pi:elem-str pi:children)) + (css-links (asset:get-css-links html-str))) (pi:h (<> - (stylesheets :hrefs css-links) - (scripts :srcs js-links))))) + (mapcar (lambda (href) + (link :rel "stylesheet" :type "text/css" :href href)) + css-links))))) (pi:define-element document (title description) (pi:h @@ -58,18 +22,29 @@ (meta :charset "UTF-8") (link :rel "stylesheet" :type "text/css" :href "/vendor/ress.css") (link :rel "stylesheet" :type "text/css" :href "/styles/global.css") + (on-demand-stylesheets pi:children) (link :rel "preconnect" :href "https://fonts.googleapis.com") (link :rel "preconnect" :href "https://fonts.gstatic.com" :crossorigin t) (link :rel "stylesheet" :href "https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap") (script :src "/vendor/htmx@1.9.12.js") + (script :src "/vendor/htmx-ext/alpine-morph@1.9.12.js") + (script :src "/vendor/htmx-ext/head-support@1.9.12.js") + (script :src "/vendor/alpine-ext/async-alpine@1.2.2.js" :defer t) (script :src "/vendor/alpine-ext/persist@3.13.8.js" :defer t) + (script :src "/vendor/alpine-ext/morph@3.13.8.js" :defer t) (script :src "/scripts/global.js" :defer t) - (on-demand-assets :component pi:children) (script :src "/vendor/alpine@3.13.8.js" :defer t) (title (format nil "~@[~a - ~]skyizwhite.dev" title)) (meta :name "description" :content (or description "pakuの個人サイト"))) pi:children))) + +(pi:define-element partial-document () + (pi:h + (<> + (head :hx-head "append" + (on-demand-stylesheets pi:children)) + pi:children))) diff --git a/src/view/components/layout.lisp b/src/view/components/layout.lisp index 3a0e3e3..86f1047 100644 --- a/src/view/components/layout.lisp +++ b/src/view/components/layout.lisp @@ -7,6 +7,7 @@ (pi:define-element layout () (pi:h (body + :hx-ext "head-support,alpine-morph" :x-data t :|:data-dark| "$store.darkMode.on" ; header diff --git a/src/view/renderer.lisp b/src/view/renderer.lisp index 033023d..3c6fc52 100644 --- a/src/view/renderer.lisp +++ b/src/view/renderer.lisp @@ -17,4 +17,4 @@ (defun partial-render (component &key status) (jg:with-html-response (if status (jg:set-response-status status)) - (pi:elem-str component))) + (pi:elem-str (cmp:partial-document component))))