This commit is contained in:
Akira Tempaku 2024-04-20 07:45:44 +09:00
commit 027077c97a
25 changed files with 123 additions and 217 deletions

View file

@ -3,25 +3,20 @@
(:use #:cl)
(:local-nicknames (#:jg #:jingle))
(:local-nicknames (#:fbr #:ningle-fbr))
(:local-nicknames (#:mw #:hp/middlewares/*))
(:local-nicknames (#:cfg #:hp/config))
(:local-nicknames (#:mw #:hp/middlewares/*))
(:export #:start
#:stop
#:update))
(in-package #:hp)
(defparameter *app* (jg:make-app :address "localhost"
:port 3000))
:port cfg:*port*))
(defun start ()
(uiop:run-program (if (cfg:dev-mode-p)
"make dev"
"make build"))
(jg:start *app*))
(defun stop ()
(when (cfg:dev-mode-p)
(uiop:run-program "make stop"))
(jg:stop *app*))
(defun setup ()
@ -30,7 +25,8 @@
(fbr:assign-routes *app*
:system "hp"
:directory "src/routes")
(jg:static-path *app* "/assets/" "src/assets/")
(jg:static-path *app* "/scripts/" "src/scripts/")
(jg:static-path *app* "/styles/" "src/styles/")
(jg:install-middleware *app* mw:*public-files*)
(jg:install-middleware *app* mw:*recovery*)
(jg:install-middleware *app* mw:*normalize-path*)

View file

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

File diff suppressed because one or more lines are too long

View file

@ -1,141 +0,0 @@
//==========================================================
// 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;
})
}
});
})()

File diff suppressed because one or more lines are too long

View file

@ -1,25 +0,0 @@
(defpackage #:hp/components/document
(:use #:cl)
(:local-nicknames (#:pi #:piccolo))
(:export #:document))
(in-package #:hp/components/document)
(pi:define-element document (title description)
(pi:h
(html :lang "ja"
(head
(meta :charset "UTF-8")
(script :src "/assets/js/htmx.js")
(script :src "/assets/js/htmx-ext/head-support.js")
(script :src "/assets/js/alpine.js" :defer t)
(link :rel "stylesheet" :type "text/css" :href "/assets/css/dist.css")
(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")
(title (format nil "~@[~a - ~]skyizwhite.dev" title))
(meta
:name "description"
:content (or description "pakuの個人サイト")))
pi:children)))

View file

@ -2,7 +2,8 @@
(:use #:cl)
(:import-from #:log4cl)
(:export #:dev-mode-p
#:prod-mode-p))
#:prod-mode-p
#:*port*))
(in-package #:hp/config)
(defparameter *env* (or (uiop:getenv "HP_ENV") "dev"))
@ -13,4 +14,6 @@
(defun prod-mode-p ()
(string= *env* "prod"))
(defparameter *port* (parse-integer (or (uiop:getenv "HP_PORT") "3000")))
(log:config :nofile)

View file

@ -1,7 +1,7 @@
(defpackage #:hp/routes/about
(:use #:cl)
(:local-nicknames (#:pi #:piccolo))
(:local-nicknames (#:view #:hp/view/*))
(:local-nicknames (#:view #:hp/view/**/*))
(:export #:handle-get))
(in-package #:hp/routes/about)
@ -11,9 +11,9 @@
(pi:define-element page ()
(pi:h
(section
(section :data-css "pages/about"
(h1 "About")
(a :href "/" :hx-boost "true"
(a :href "/"
"top"))))
(defun handle-get (params)

View file

@ -1,16 +1,16 @@
(defpackage #:hp/routes/index
(:use #:cl)
(:local-nicknames (#:pi #:piccolo))
(:local-nicknames (#:view #:hp/view/*))
(:local-nicknames (#:view #:hp/view/**/*))
(:export #:handle-get))
(in-package #:hp/routes/index)
(pi:define-element page ()
(pi:h
(section
(h1 :class "text-red-400"
(section :data-css "pages/index"
(h1
"Hello, World!")
(a :href "/about" :hx-boost "true"
(a :href "/about"
"About"))))
(defun handle-get (params)

View file

@ -2,7 +2,7 @@
(:use #:cl)
(:local-nicknames (#:jg #:jingle))
(:local-nicknames (#:pi #:piccolo))
(:local-nicknames (#:view #:hp/view/*))
(:local-nicknames (#:view #:hp/view/**/*))
(:export #:handle-not-found))
(in-package #:hp/routes/not-found)

View file

0
src/scripts/pages/.keep Normal file
View file

View file

8
src/styles/global.css Normal file
View file

@ -0,0 +1,8 @@
@charset "utf-8";
body {
font-family: "Noto Sans JP", sans-serif;
font-optical-sizing: auto;
font-weight: 400;
font-style: normal;
}

View file

@ -0,0 +1,8 @@
@scope ([data-css='pages/about']) {
:scope {
height: 100svh;
background-color: thistle;
display: grid;
place-content: center;
}
}

View file

@ -0,0 +1,8 @@
@scope ([data-css='pages/index']) {
:scope {
height: 100svh;
background-color: aliceblue;
display: grid;
place-content: center;
}
}

View file

@ -0,0 +1,73 @@
(defpackage #:hp/view/components/document
(:use #:cl)
(:local-nicknames (#:re #:cl-ppcre))
(:local-nicknames (#:pi #:piccolo))
(:export #: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))
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:h
(<>
(stylesheets :hrefs css-links)
(scripts :srcs js-links)))))
(pi:define-element document (title description)
(pi:h
(html :lang "ja"
(head
(meta :charset "UTF-8")
(link :rel "stylesheet" :type "text/css" :href "/vendor/ress.css")
(link :rel "stylesheet" :type "text/css" :href "/styles/global.css")
(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")
(on-demand-assets :component pi:children)
(script :src "/vendor/htmx@1.9.12.js")
(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)))

View file

@ -1,12 +1,12 @@
(defpackage #:hp/components/layout
(defpackage #:hp/view/components/layout
(:use #:cl)
(:local-nicknames (#:pi #:piccolo))
(:export #:layout))
(in-package #:hp/components/layout)
(in-package #:hp/view/components/layout)
(pi:define-element layout ()
(pi:h
(body :hx-ext "head-support"
(body
; header
(main pi:children)
; footer

View file

@ -3,18 +3,18 @@
(:local-nicknames (#:jg #:jingle))
(:local-nicknames (#:pi #:piccolo))
(:local-nicknames (#:cfg #:hp/config))
(:local-nicknames (#:cmp #:hp/components/*))
(:local-nicknames (#:cmp #:hp/view/components/*))
(:export #:render
#:partial-render))
(in-package #:hp/view/renderer)
(defun render (page &key status metadata)
(jg:with-html-response
(jg:set-response-status (or status :ok))
(if status (jg:set-response-status status))
(pi:elem-str (cmp:document metadata
(cmp:layout page)))))
(defun partial-render (component &key status)
(jg:with-html-response
(jg:set-response-status (or status :ok))
(if status (jg:set-response-status status))
(pi:elem-str component)))