From c4d10b7c7c8ae4d88badf23b8b3a467a24237adc Mon Sep 17 00:00:00 2001 From: Alexander Artemenko Date: Mon, 1 Apr 2024 23:41:03 +0300 Subject: [PATCH] Copy static files, render tags and timestamps. --- docs/difference-from-coleslaw.lisp | 12 +++ example/blog/first.post | 12 +++ src/content.lisp | 123 +++++++++++++++++++++-------- src/content/defaults.lisp | 7 +- src/content/post.lisp | 22 ++++++ src/core.lisp | 8 +- src/site.lisp | 19 +++-- src/tag.lisp | 23 ++++++ src/theme.lisp | 26 +++++- src/themes/closure-template.lisp | 78 +++++++++++++++--- themes/hyde/base.tmpl | 26 +++--- themes/hyde/css/cc-by-sa.png | Bin 0 -> 672 bytes themes/hyde/css/logo_large.jpg | Bin 0 -> 52898 bytes themes/hyde/css/logo_medium.jpg | Bin 0 -> 26688 bytes themes/hyde/css/logo_small.jpg | Bin 0 -> 19931 bytes themes/hyde/css/style.css | 100 +++++++++++++++++++++++ themes/hyde/post.tmpl | 2 +- 17 files changed, 388 insertions(+), 70 deletions(-) create mode 100644 docs/difference-from-coleslaw.lisp create mode 100644 example/blog/first.post create mode 100644 src/content/post.lisp create mode 100644 src/tag.lisp create mode 100644 themes/hyde/css/cc-by-sa.png create mode 100644 themes/hyde/css/logo_large.jpg create mode 100644 themes/hyde/css/logo_medium.jpg create mode 100644 themes/hyde/css/logo_small.jpg create mode 100644 themes/hyde/css/style.css diff --git a/docs/difference-from-coleslaw.lisp b/docs/difference-from-coleslaw.lisp new file mode 100644 index 0000000..253b4bb --- /dev/null +++ b/docs/difference-from-coleslaw.lisp @@ -0,0 +1,12 @@ +(defvar *templates-difference* + "Variable `config` was renamed to `site`. + `post` renamed to `content` + `pubdate` renamed to `site.pubdate`. + +For templates base on Closure Template, StatiCL defines these filters: + +* date - formats the a timestamp in this as YYYY-MM-DD +* datetime - formats a timestamp as YYYY-MM-DD HH:MM + +To define additional filters, inherit your template class from CLOSURE-TEMPLATE and define a method for REGISTER-USER-FILTERS generic-function. +") diff --git a/example/blog/first.post b/example/blog/first.post new file mode 100644 index 0000000..89b526a --- /dev/null +++ b/example/blog/first.post @@ -0,0 +1,12 @@ +;;;;; +title: Second version of Ultralisp.org is available now! +description: An announce of the new version on Ultralisp - frequently updated Common Lisp libraries distribution. Ultralisp allows everyone to host list libraries, it is like PyPi hosting from Python world. +tags: release, my-projects, ultralisp +created-at: 2019-02-03 15:00 +format: md +;;;;; + +I believe, that software should evolve and evolve quickly. +One of the reasons why Common Lisp seems strange to newcomers is its +ecosystem. It takes a long time to add a new library and make it useful +to other common lispers. diff --git a/src/content.lisp b/src/content.lisp index b599e39..1f87261 100644 --- a/src/content.lisp +++ b/src/content.lisp @@ -20,6 +20,8 @@ (:import-from #:staticl/content/reader #:read-content-file) (:import-from #:local-time + #:+iso-8601-date-format+ + #:format-timestring #:universal-to-timestamp #:timestamp) (:import-from #:serapeum @@ -27,6 +29,13 @@ (:import-from #:utilities.print-items #:print-items-mixin #:print-items) + (:import-from #:closer-mop + #:class-slots + #:slot-definition-initargs) + (:import-from #:staticl/tag + #:tag) + (:import-from #:staticl/format + #:to-html) (:export #:supported-content-types #:content-type #:content @@ -63,7 +72,10 @@ (defclass content (print-items-mixin) - ()) + ((metadata :initform (make-hash-table :test 'equal) + :type hash-table + :reader content-metadata + :documentation "A hash with additional fields specified in the file's header."))) (defclass content-with-title-mixin () @@ -78,7 +90,7 @@ (defclass content-with-tags-mixin () ((tags :initarg :tags - :type (soft-list-of string) + :type (soft-list-of tag) :reader content-tags))) @@ -106,36 +118,40 @@ (defmethod print-items append ((obj content-from-file)) - (list (list :file (list :after :title) "= ~S" (content-file obj)))) - - -;; (defmethod print-object ((obj content) stream) -;; (print-unreadable-object (obj stream :type t) -;; (when (and (slot-boundp obj 'title) -;; (slot-boundp obj 'file)) -;; (format stream "~S :file ~S" -;; (content-title obj) -;; (content-file obj))))) - - -(defmethod initialize-instance ((obj content) &rest initargs) - (apply #'call-next-method - obj - (normalize-plist initargs - :created-at (lambda (value) - (etypecase value - (null value) - (string - (universal-to-timestamp - (org.shirakumo.fuzzy-dates:parse value))) - (local-time:timestamp - value))) - :tags (lambda (value) - (etypecase value - (string - (mapcar #'str:trim - (str:split "," value - :omit-nulls t)))))))) + `(((:file (:after :title)) " file = ~S" ,(content-file obj)))) + + +(defmethod initialize-instance ((obj content) &rest initargs &key &allow-other-keys) + (let* ((normalized-args + (normalize-plist initargs + :created-at (lambda (value) + (etypecase value + (null value) + (string + (universal-to-timestamp + (org.shirakumo.fuzzy-dates:parse value))) + (local-time:timestamp + value))) + :tags (lambda (value) + (etypecase value + (string + (loop for tag-name in (str:split "," value + :omit-nulls t) + collect (make-instance 'tag + :name (str:trim tag-name)))))))) + (result (apply #'call-next-method obj normalized-args)) + (all-initargs + (loop for slot in (class-slots (class-of obj)) + appending (slot-definition-initargs slot)))) + + ;; Write unknown initargs into the metadata slot + (loop for (key value) on initargs by #'cddr + unless (member key all-initargs) + do (setf (gethash (string-downcase key) + (content-metadata result)) + value)) + + (values result))) (defgeneric supported-content-types (site) @@ -217,7 +233,7 @@ (vars (dict "site" site-vars "content" content-vars)) (template-name (content-template content))) - + (staticl/theme:render theme template-name vars stream)))) @@ -225,12 +241,49 @@ (:documentation "Returns an additional list content objects such as RSS feeds or sitemaps.")) +(defmethod template-vars ((content content) &key (hash (dict))) + (setf (gethash "metadata" hash) + (content-metadata content)) + + (if (next-method-p) + (call-next-method content :hash hash) + (values hash))) + + (defmethod template-vars ((content content-from-file) &key (hash (dict))) (setf (gethash "title" hash) (content-title content) (gethash "html" hash) - (staticl/format:to-html (content-text content) - (content-format content))) + (to-html (content-text content) + (content-format content)) + (gethash "created-at" hash) + (content-created-at content) + + ;; (gethash "created-at" hash) + ;; (format-timestring nil (content-created-at content) + ;; :format local-time:+iso-8601-format+) + ;; ;; Also we provide only a date, because it might be more convinitent + ;; ;; to render it in short format: + ;; (gethash "created-at-date" hash) + ;; (format-timestring nil (content-created-at content) + ;; :format +iso-8601-date-format+) + + ;; ;; And in case if user wants a complied to ISO format: + ;; (gethash "created-at-iso" hash) + ;; (format-timestring nil (content-created-at content) + ;; :format local-time:+iso-8601-format+) + ) + + (if (next-method-p) + (call-next-method content :hash hash) + (values hash))) + + +(defmethod template-vars ((content content-with-tags-mixin) &key (hash (dict))) + (setf (gethash "tags" hash) + (mapcar #'template-vars + (content-tags content))) + (if (next-method-p) (call-next-method content :hash hash) (values hash))) diff --git a/src/content/defaults.lisp b/src/content/defaults.lisp index 80c6f40..a83664b 100644 --- a/src/content/defaults.lisp +++ b/src/content/defaults.lisp @@ -5,9 +5,12 @@ (:import-from #:staticl/site #:site) (:import-from #:staticl/content/page - #:page-type)) + #:page-type) + (:import-from #:staticl/content/post + #:post-type)) (in-package #:staticl/content/defaults) (defmethod supported-content-types ((site site)) - (list (make-instance 'page-type))) + (list (make-instance 'page-type) + (make-instance 'post-type))) diff --git a/src/content/post.lisp b/src/content/post.lisp new file mode 100644 index 0000000..31a9467 --- /dev/null +++ b/src/content/post.lisp @@ -0,0 +1,22 @@ +(uiop:define-package #:staticl/content/post + (:use #:cl) + (:import-from #:staticl/content + #:content-from-file + #:content-type) + (:export #:post-type + #:post)) +(in-package #:staticl/content/post) + + +(defclass post (content-from-file) + () + (:default-initargs + ;; In coleslaw page and post share the same template + :template "post")) + + +(defclass post-type (content-type) + () + (:default-initargs + :type "post" + :content-class 'post)) diff --git a/src/core.lisp b/src/core.lisp index a813308..00fc475 100644 --- a/src/core.lisp +++ b/src/core.lisp @@ -1,6 +1,7 @@ (uiop:define-package #:staticl (:use #:cl) (:import-from #:staticl/site + #:site-theme #:site-plugins #:make-site) (:import-from #:staticl/content @@ -9,6 +10,8 @@ #:preprocess) (:import-from #:serapeum #:->) + (:import-from #:staticl/theme + #:copy-static) (:nicknames #:staticl/core) (:export #:generate #:stage)) @@ -24,7 +27,7 @@ (root-dir *default-pathname-defaults*) (stage-dir (merge-pathnames (make-pathname :directory '(:relative "stage")) (uiop:ensure-directory-pathname root-dir)))) - (let* ((site (make-site root-dir)) + (let* ((site (make-site root-dir)) (initial-content (read-contents site)) (plugins (site-plugins site)) (additional-content @@ -35,4 +38,7 @@ additional-content))) (loop for content in all-content do (write-content site content stage-dir)) + + (copy-static (site-theme site) + stage-dir) (values))) diff --git a/src/site.lisp b/src/site.lisp index ab02052..5b1199c 100644 --- a/src/site.lisp +++ b/src/site.lisp @@ -1,6 +1,7 @@ (uiop:define-package #:staticl/site (:use #:cl) (:import-from #:log) + (:import-from #:local-time) (:import-from #:serapeum #:dict #:soft-list-of @@ -17,13 +18,12 @@ #:template-vars #:load-theme #:theme) - (:export - #:site - #:site-content-root - #:site-title - #:make-site - #:site-plugins - #:site-theme)) + (:export #:site + #:site-content-root + #:site-title + #:make-site + #:site-plugins + #:site-theme)) (in-package #:staticl/site) @@ -105,4 +105,7 @@ (defmethod template-vars ((site site) &key (hash (dict))) (setf (gethash "title" hash) - (site-title site))) + (site-title site) + (gethash "pubdate" hash) + (local-time:now)) + (values hash)) diff --git a/src/tag.lisp b/src/tag.lisp new file mode 100644 index 0000000..f4d023e --- /dev/null +++ b/src/tag.lisp @@ -0,0 +1,23 @@ +(uiop:define-package #:staticl/tag + (:use #:cl) + (:import-from #:serapeum + #:dict) + (:import-from #:staticl/theme + #:template-vars) + (:export #:tag-name + #:tag)) +(in-package #:staticl/tag) + + +(defclass tag () + ((name :initarg :name + :type string + :reader tag-name)) + (:default-initargs + :name (error ":NAME is required argument for a tag."))) + + +(defmethod template-vars ((tag tag) &key (hash (dict))) + (setf (gethash "name" hash) + (tag-name tag)) + (values hash)) diff --git a/src/theme.lisp b/src/theme.lisp index 11f5452..cd48cdf 100644 --- a/src/theme.lisp +++ b/src/theme.lisp @@ -13,7 +13,9 @@ #:print-items) (:export #:theme #:template-vars - #:render)) + #:render + #:list-static + #:copy-static)) (in-package #:staticl/theme) @@ -85,3 +87,25 @@ (remove-if #'null (list site-root builtin-themes-dir)))))) + + +(defgeneric list-static (theme) + (:documentation "Returns a list of static files such as CSS, JS, images. + + Each list item should be a list of two items where first + item is an absolute pathname and second is a pathname relative + to the root of the site.")) + + +(defgeneric copy-static (theme stage-dir) + (:documentation "Copies static files such as CSS, JS, images into the STAGE-DIR. + + Usually it is enough to define a method for LIST-STATIC generic-function.") + (:method ((theme theme) (stage-dir pathname)) + (loop with target-dir = (uiop:ensure-directory-pathname stage-dir) + for (source-filename relative-filename) in (list-static theme) + for target-filename = (merge-pathnames relative-filename + target-dir) + do (ensure-directories-exist target-filename) + (uiop:copy-file source-filename + target-filename)))) diff --git a/src/themes/closure-template.lisp b/src/themes/closure-template.lisp index 07eed4d..acfcbe4 100644 --- a/src/themes/closure-template.lisp +++ b/src/themes/closure-template.lisp @@ -1,29 +1,89 @@ (uiop:define-package #:staticl/themes/closure-template (:use #:cl) (:import-from #:closure-template + #:define-print-syntax + #:register-print-handler #:compile-template) (:import-from #:staticl/utils #:transform-keys #:do-files) - (:import-from #:staticl/theme) + (:import-from #:staticl/theme + #:list-static) (:import-from #:str - #:replace-all)) + #:replace-all) + (:import-from #:local-time + #:format-timestring)) (in-package #:staticl/themes/closure-template) (defclass closure-template (staticl/theme:theme) ((namespace :initarg :namespace :type string - :reader template-namespace)) + :reader template-namespace) + (static-files :reader list-static + :type list) + (date-format :initarg :date-format + :type list + :reader date-format) + (datetime-format :initarg :datetime-format + :type list + :reader datetime-format)) (:default-initargs - :namespace (error ":NAMESPACE argument show be given and correspond to the namespace used in *.tmpl files."))) + :namespace (error ":NAMESPACE argument show be given and correspond to the namespace used in *.tmpl files.") + :date-format local-time:+iso-8601-date-format+ + :datetime-format (append local-time:+iso-8601-date-format+ + '(#\Space (:hour 2) #\: (:min 2))) + :static-files nil)) +(defgeneric register-user-filters (theme) + (:documentation "Registers some variable filters useful inside templates.") + (:method ((theme closure-template)) + + ;; This rule should go first, because it has the same prefix + ;; as "date" rule: + (define-print-syntax print-datetime "datetime" (:constant t)) + (define-print-syntax print-date "date" (:constant t)) + + (flet ((format-date (params end value) + (declare (ignore params end)) + (format-timestring nil value + :format (date-format theme))) + (format-datetime (params end value) + (declare (ignore params end)) + (format-timestring nil value + :format (datetime-format theme)))) + (register-print-handler :common-lisp-backend + 'print-date + :function #'format-date) + (register-print-handler :common-lisp-backend + 'print-datetime + :function #'format-datetime)))) + (defmethod initialize-instance :after ((obj closure-template) &rest initargs &key path &allow-other-keys) (declare (ignore initargs)) + + (register-user-filters obj) - (do-files (filename path :file-type "tmpl") - (compile-template :common-lisp-backend filename))) + (let ((static-files nil) + (root-path (uiop:ensure-directory-pathname path))) + + (do-files (filename root-path) + (let ((ext (pathname-type filename)) + (exts-to-ignore '("lisp" "fasl"))) + (cond + ((string-equal ext "tmpl") + (compile-template :common-lisp-backend filename)) + ((not (member ext exts-to-ignore :test #'string-equal)) + (push (list filename + (uiop:enough-pathname filename root-path)) + static-files))))) + + ;; Here we'll save collected static files to + ;; be able to copy them into the staging dir later. + (setf (slot-value obj 'static-files) + static-files) + (values))) (defun normalize-key (text) @@ -58,7 +118,7 @@ ;; Closure templates do not have ;; inheritance, so we just render ;; inner part of the page separately: - :raw (with-output-to-string (string-stream) - (render-helper template template-name vars - string-stream))) + "raw" (with-output-to-string (string-stream) + (render-helper template template-name vars + string-stream))) stream)))) diff --git a/themes/hyde/base.tmpl b/themes/hyde/base.tmpl index 24303cd..575fb87 100644 --- a/themes/hyde/base.tmpl +++ b/themes/hyde/base.tmpl @@ -2,15 +2,15 @@ {template base} {\n} - + - {$config.title} - + {$site.title} + - - + + {if $injections.head} {foreach $injection in $injections.head} {$injection |noAutoescape} @@ -19,10 +19,10 @@ {\n}
{\n} {if $content.created_at} - Written on {$content.created_at} + Written on {$content.created_at|datetime} {/if}
{\n} {\n}