Construct public interface to blog plugin
The blog plugin now only exposes the blog artifact constructor method and the configuration parameters.
This commit is contained in:
parent
6d6fccf2f7
commit
7cf34ca065
8 changed files with 185 additions and 137 deletions
6
TODO.rst
6
TODO.rst
|
@ -17,13 +17,11 @@ Features
|
||||||
========
|
========
|
||||||
|
|
||||||
- Template: docstrings in template
|
- Template: docstrings in template
|
||||||
|
- Localization: Fall back to English if a particular key does not exist for the
|
||||||
|
target language
|
||||||
|
|
||||||
Cleanup
|
Cleanup
|
||||||
=======
|
=======
|
||||||
|
|
||||||
- How much of each artifact's internals need to be exposed? Make accessor
|
- How much of each artifact's internals need to be exposed? Make accessor
|
||||||
functions? Constructor functions over `MAKE-INSTANCE`?
|
functions? Constructor functions over `MAKE-INSTANCE`?
|
||||||
- A proper blog plugin interface; we probably only want a constructor function
|
|
||||||
which will then instantiate a blog artifact which in turn will instantiate
|
|
||||||
all its subordinate artifacts. Thus the blog artifact acts as a façade to the
|
|
||||||
entire blog plugin.
|
|
||||||
|
|
|
@ -53,4 +53,5 @@
|
||||||
(:file "category-tag")
|
(:file "category-tag")
|
||||||
(:file "index")
|
(:file "index")
|
||||||
(:file "post")))
|
(:file "post")))
|
||||||
(:file "templates")))))))
|
(:file "templates")
|
||||||
|
(:file "facade")))))))
|
||||||
|
|
|
@ -59,126 +59,3 @@
|
||||||
(write-artifact tags)
|
(write-artifact tags)
|
||||||
(write-artifact (make-instance 'hssg.blog.artifacts:archive-page-artifact :periods periods :blog blog))
|
(write-artifact (make-instance 'hssg.blog.artifacts:archive-page-artifact :periods periods :blog blog))
|
||||||
(write-artifact (make-instance 'hssg.blog.artifacts:rss-feed-artifact :posts posts :blog blog))))
|
(write-artifact (make-instance 'hssg.blog.artifacts:rss-feed-artifact :posts posts :blog blog))))
|
||||||
|
|
||||||
(defun directory-contents (path &key (test #'fad:directory-pathname-p))
|
|
||||||
"Contents of a directory, sorted alphabetically in ascending order."
|
|
||||||
(declare (type pathname path)
|
|
||||||
(type (function (pathname) boolean) test))
|
|
||||||
(sort (remove-if-not test (fad:list-directory path))
|
|
||||||
#'string<= :key #'namestring))
|
|
||||||
|
|
||||||
(defun file-pathname-p (pathname)
|
|
||||||
"Whether PATHNAME names a file."
|
|
||||||
(not (fad:directory-pathname-p pathname)))
|
|
||||||
|
|
||||||
(defun read-blog (title path url output template &key (description "") (initial '()) (top ""))
|
|
||||||
"Read the entire blog at file path PATH and write it to OUTPUT. All pages are
|
|
||||||
processed through the TEMPLATE."
|
|
||||||
(declare (type pathname path output)
|
|
||||||
(type string title top)
|
|
||||||
(type list initial url))
|
|
||||||
(let ((result (make-instance 'hssg.blog.artifacts:blog-artifact
|
|
||||||
:title title :description description
|
|
||||||
:url url :output output
|
|
||||||
:template template :initial initial :top top))
|
|
||||||
(last-post nil))
|
|
||||||
(setf (slot-value (slot-value result 'hssg.blog.artifacts:categories) 'hssg.blog.artifacts:blog) result)
|
|
||||||
(setf (slot-value (slot-value result 'hssg.blog.artifacts:tags) 'hssg.blog.artifacts:blog) result)
|
|
||||||
(dolist (year (directory-contents path))
|
|
||||||
(dolist (month (directory-contents year))
|
|
||||||
(dolist (day (directory-contents month))
|
|
||||||
(dolist (entry (directory-contents day :test #'file-pathname-p))
|
|
||||||
(let ((post (read-post entry result)))
|
|
||||||
(when last-post
|
|
||||||
(setf (slot-value post 'hssg.blog.artifacts:previous) last-post)
|
|
||||||
(setf (slot-value last-post 'hssg.blog.artifacts:next) post))
|
|
||||||
(add-blog-post result post)
|
|
||||||
(setf last-post post)))
|
|
||||||
(dolist (directory (directory-contents day :test #'fad:directory-pathname-p))
|
|
||||||
(add-static
|
|
||||||
result
|
|
||||||
;; The path needs to be relative, so we perform some PATHNAME trickery
|
|
||||||
(enough-namestring directory (fad:pathname-parent-directory year))
|
|
||||||
path)))))
|
|
||||||
result))
|
|
||||||
|
|
||||||
(defun read-post (file-path blog)
|
|
||||||
"Reads one single blog post from file FILE-PATH, where the path of FILE-PATH
|
|
||||||
determines the date of the post and file name the slug of the post."
|
|
||||||
(declare (type pathname file-path)
|
|
||||||
(type blog-artifact blog))
|
|
||||||
(let ((date-components (mapcar #'parse-integer
|
|
||||||
(last (pathname-directory file-path) 3)))
|
|
||||||
(post (funcall (gethash (pathname-type file-path) hssg.blog:*BLOG-POST-READERS*)
|
|
||||||
file-path)))
|
|
||||||
(hssg:let-metadata post
|
|
||||||
((category :category hssg.blog.i18n:*default-category*)
|
|
||||||
(slug :slug (pathname-name file-path))
|
|
||||||
(title :title)
|
|
||||||
(author :author)
|
|
||||||
(tags :tags)
|
|
||||||
(published :published (apply #'date-from-numbers date-components))
|
|
||||||
(modified :modified)
|
|
||||||
(content :content))
|
|
||||||
(make-instance 'hssg.blog.artifacts:post-artifact :blog blog :slug slug :title title :content content
|
|
||||||
:category category :tags tags :author author
|
|
||||||
:published published
|
|
||||||
:modified modified
|
|
||||||
:metadata post))))
|
|
||||||
|
|
||||||
(defun add-blog-post (blog post)
|
|
||||||
"Add a single blog post to the entire blog. The blog's author, category and
|
|
||||||
tags will also be registered."
|
|
||||||
(with-slots (posts tags authors categories) blog
|
|
||||||
(push post posts)
|
|
||||||
(with-slots ((category-name category)
|
|
||||||
(post-tags tags)
|
|
||||||
author)
|
|
||||||
post
|
|
||||||
(when category-name
|
|
||||||
(hssg.blog.artifact.category:add-post categories category-name post))
|
|
||||||
(when post-tags
|
|
||||||
(dolist (tag post-tags)
|
|
||||||
(hssg.blog.artifact.tag:add-post tags tag post)))
|
|
||||||
(when author
|
|
||||||
(push post (gethash author authors))))
|
|
||||||
;; This assumes that incoming posts are ordered from oldest to newest!
|
|
||||||
(with-slots (periods) blog
|
|
||||||
(let ((year (local-time:timestamp-year (slot-value post 'hssg.blog.artifacts:published)))
|
|
||||||
(top-period (car periods)))
|
|
||||||
(when (or (not top-period) (not (= year (slot-value top-period 'hssg.blog.period:year))))
|
|
||||||
(push (make-instance 'hssg.blog.period:year-period :year year) periods)))
|
|
||||||
(push-post (car periods) post))))
|
|
||||||
|
|
||||||
(defun add-static (blog pathname input-dir)
|
|
||||||
(with-slots (static (output-dir output)) blog
|
|
||||||
(with-slots ((artifacts hssg:artifacts)) static
|
|
||||||
(let ((artifact (hssg:make-directory-artifact
|
|
||||||
pathname
|
|
||||||
input-dir
|
|
||||||
output-dir)))
|
|
||||||
(push artifact artifacts)))))
|
|
||||||
|
|
||||||
(defmethod push-post ((period hssg.blog.period:day-period) post)
|
|
||||||
(with-slots ((posts hssg.blog.period:posts)) period
|
|
||||||
(push post posts)))
|
|
||||||
|
|
||||||
(defmethod push-post ((period hssg.blog.period:month-period) post)
|
|
||||||
(with-slots ((days hssg.blog.period:days)) period
|
|
||||||
(let ((day (local-time:timestamp-day (slot-value post 'hssg.blog.artifacts:published))))
|
|
||||||
(let ((topmost (car days)))
|
|
||||||
(if (or (null topmost) (not (= day (slot-value topmost 'hssg.blog.period:day))))
|
|
||||||
(let ((day-period (make-instance 'hssg.blog.period:day-period :day day)))
|
|
||||||
(push day-period days)
|
|
||||||
(push-post day-period post))
|
|
||||||
(push-post topmost post))))))
|
|
||||||
|
|
||||||
(defmethod push-post ((period hssg.blog.period:year-period) post)
|
|
||||||
(with-slots ((months hssg.blog.period:months)) period
|
|
||||||
(let ((month (local-time:timestamp-month (slot-value post 'hssg.blog.artifacts:published))))
|
|
||||||
(let ((topmost (car months)))
|
|
||||||
(if (or (null topmost) (not (= month (slot-value topmost 'hssg.blog.period:month))))
|
|
||||||
(let ((month-period (make-instance 'hssg.blog.period:month-period :month month)))
|
|
||||||
(push month-period months)
|
|
||||||
(push-post month-period post))
|
|
||||||
(push-post topmost post))))))
|
|
||||||
|
|
151
src/blog/facade.lisp
Normal file
151
src/blog/facade.lisp
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
;;;; SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
;;;; facade.lisp Implementation of the public interface to the blog plugin
|
||||||
|
;;;;
|
||||||
|
;;;; Copyright (C) 2022 Alejandro "HiPhish" Sanchez
|
||||||
|
;;;;
|
||||||
|
;;;; This file is part of CL-HSSG.
|
||||||
|
;;;;
|
||||||
|
;;;; CL-HSSG is free software: you can redistribute it and/or modify it under
|
||||||
|
;;;; the terms of the GNU Affero General Public License as published by the
|
||||||
|
;;;; Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
;;;; option) any later version.
|
||||||
|
;;;;
|
||||||
|
;;;; CL-HSSG is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
;;;; WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
;;;; FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||||
|
;;;; more details.
|
||||||
|
;;;;
|
||||||
|
;;;; You should have received a copy of the GNU Affero General Public License
|
||||||
|
;;;; along with CL-HSSG If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
(in-package #:hssg.blog.facade)
|
||||||
|
|
||||||
|
(defun make-blog (input output &key title url description template top)
|
||||||
|
"Constructor function which produces a blog artifact. Parameters:
|
||||||
|
- INPUT: Pathname to the directory from which to read the blog
|
||||||
|
- OUTPUT: Pathname to the directory where to write the blog artifact to
|
||||||
|
- TITLE: Title of the blog
|
||||||
|
- URL: URL path components to the blog, relative to the site root
|
||||||
|
- TOP: Identifier to use as the top of the breadcrumbs
|
||||||
|
- DESCRIPTION: Human-readable description, will be used for RSS feed
|
||||||
|
- TEMPLATE: Template to use for HTML pages"
|
||||||
|
(declare (type pathname input output)
|
||||||
|
(type string title description top)
|
||||||
|
(type list url)
|
||||||
|
(type hssg:template template))
|
||||||
|
(let ((result (make-instance 'hssg.blog.artifacts:blog-artifact
|
||||||
|
:title title :description description
|
||||||
|
:url url :output output
|
||||||
|
:template template :top top))
|
||||||
|
last-post)
|
||||||
|
(setf (slot-value (slot-value result 'hssg.blog.artifacts:categories) 'hssg.blog.artifacts:blog) result)
|
||||||
|
(setf (slot-value (slot-value result 'hssg.blog.artifacts:tags) 'hssg.blog.artifacts:blog) result)
|
||||||
|
(dolist (year (hssg.blog.util:directory-contents input))
|
||||||
|
(dolist (month (hssg.blog.util:directory-contents year))
|
||||||
|
(dolist (day (hssg.blog.util:directory-contents month))
|
||||||
|
(dolist (entry (hssg.blog.util:directory-contents day :test #'file-pathname-p))
|
||||||
|
(let ((post (read-post entry result)))
|
||||||
|
(when last-post
|
||||||
|
(setf (slot-value post 'hssg.blog.artifacts:previous) last-post)
|
||||||
|
(setf (slot-value last-post 'hssg.blog.artifacts:next) post))
|
||||||
|
(add-blog-post result post)
|
||||||
|
(setf last-post post)))
|
||||||
|
(dolist (directory (hssg.blog.util:directory-contents day :test #'fad:directory-pathname-p))
|
||||||
|
(add-static
|
||||||
|
result
|
||||||
|
;; The path needs to be relative, so we perform some PATHNAME trickery
|
||||||
|
(enough-namestring directory (fad:pathname-parent-directory year))
|
||||||
|
input)))))
|
||||||
|
result))
|
||||||
|
|
||||||
|
(defun file-pathname-p (pathname)
|
||||||
|
"Whether PATHNAME names a file."
|
||||||
|
(not (fad:directory-pathname-p pathname)))
|
||||||
|
|
||||||
|
(defun read-post (file-path blog)
|
||||||
|
"Reads one single blog post from file FILE-PATH, where the path of FILE-PATH
|
||||||
|
determines the date of the post and file name the slug of the post."
|
||||||
|
(declare (type pathname file-path)
|
||||||
|
(type hssg.blog.artifacts:blog-artifact blog))
|
||||||
|
(let ((date-components (mapcar #'parse-integer
|
||||||
|
(last (pathname-directory file-path) 3)))
|
||||||
|
(post (funcall (gethash (pathname-type file-path) hssg.blog:*BLOG-POST-READERS*)
|
||||||
|
file-path)))
|
||||||
|
(hssg:let-metadata post
|
||||||
|
((category :category hssg.blog.i18n:*default-category*)
|
||||||
|
(slug :slug (pathname-name file-path))
|
||||||
|
(title :title)
|
||||||
|
(author :author)
|
||||||
|
(tags :tags)
|
||||||
|
(published :published (apply #'hssg.blog.util:date-from-numbers date-components))
|
||||||
|
(modified :modified)
|
||||||
|
(content :content))
|
||||||
|
(make-instance 'hssg.blog.artifacts:post-artifact :blog blog :slug slug :title title :content content
|
||||||
|
:category category :tags tags :author author
|
||||||
|
:published published
|
||||||
|
:modified modified
|
||||||
|
:metadata post))))
|
||||||
|
|
||||||
|
(defun add-blog-post (blog post)
|
||||||
|
"Add a single blog post to the entire blog. The blog's author, category and
|
||||||
|
tags will also be registered."
|
||||||
|
(with-slots ((posts hssg.blog.artifacts:posts)
|
||||||
|
(tags hssg.blog.artifacts:tags)
|
||||||
|
(authors hssg.blog.artifacts:authors)
|
||||||
|
(categories hssg.blog.artifacts:categories))
|
||||||
|
blog
|
||||||
|
(push post posts)
|
||||||
|
(with-slots ((category-name hssg.blog.artifacts:category)
|
||||||
|
(post-tags hssg.blog.artifacts:tags)
|
||||||
|
(author hssg.blog.artifacts:author))
|
||||||
|
post
|
||||||
|
(when category-name
|
||||||
|
(hssg.blog.artifact.category:add-post categories category-name post))
|
||||||
|
(when post-tags
|
||||||
|
(dolist (tag post-tags)
|
||||||
|
(hssg.blog.artifact.tag:add-post tags tag post)))
|
||||||
|
(when author
|
||||||
|
(push post (gethash author authors))))
|
||||||
|
;; This assumes that incoming posts are ordered from oldest to newest!
|
||||||
|
(with-slots ((periods hssg.blog.artifacts:periods)) blog
|
||||||
|
(let ((year (local-time:timestamp-year (slot-value post 'hssg.blog.artifacts:published)))
|
||||||
|
(top-period (car periods)))
|
||||||
|
(when (or (not top-period) (not (= year (slot-value top-period 'hssg.blog.period:year))))
|
||||||
|
(push (make-instance 'hssg.blog.period:year-period :year year) periods)))
|
||||||
|
(push-post (car periods) post))))
|
||||||
|
|
||||||
|
(defun add-static (blog pathname input-dir)
|
||||||
|
(with-slots ((static hssg.blog.artifacts:static)
|
||||||
|
(output-dir hssg.blog.artifacts:output))
|
||||||
|
blog
|
||||||
|
(with-slots ((artifacts hssg:artifacts))
|
||||||
|
static
|
||||||
|
(let ((artifact (hssg:make-directory-artifact
|
||||||
|
pathname
|
||||||
|
input-dir
|
||||||
|
output-dir)))
|
||||||
|
(push artifact artifacts)))))
|
||||||
|
|
||||||
|
(defmethod push-post ((period hssg.blog.period:day-period) post)
|
||||||
|
(with-slots ((posts hssg.blog.period:posts)) period
|
||||||
|
(push post posts)))
|
||||||
|
|
||||||
|
(defmethod push-post ((period hssg.blog.period:month-period) post)
|
||||||
|
(with-slots ((days hssg.blog.period:days)) period
|
||||||
|
(let ((day (local-time:timestamp-day (slot-value post 'hssg.blog.artifacts:published))))
|
||||||
|
(let ((topmost (car days)))
|
||||||
|
(if (or (null topmost) (not (= day (slot-value topmost 'hssg.blog.period:day))))
|
||||||
|
(let ((day-period (make-instance 'hssg.blog.period:day-period :day day)))
|
||||||
|
(push day-period days)
|
||||||
|
(push-post day-period post))
|
||||||
|
(push-post topmost post))))))
|
||||||
|
|
||||||
|
(defmethod push-post ((period hssg.blog.period:year-period) post)
|
||||||
|
(with-slots ((months hssg.blog.period:months)) period
|
||||||
|
(let ((month (local-time:timestamp-month (slot-value post 'hssg.blog.artifacts:published))))
|
||||||
|
(let ((topmost (car months)))
|
||||||
|
(if (or (null topmost) (not (= month (slot-value topmost 'hssg.blog.period:month))))
|
||||||
|
(let ((month-period (make-instance 'hssg.blog.period:month-period :month month)))
|
||||||
|
(push month-period months)
|
||||||
|
(push-post month-period post))
|
||||||
|
(push-post topmost post))))))
|
|
@ -23,7 +23,7 @@
|
||||||
(:use :cl)
|
(:use :cl)
|
||||||
(:export range date->year break-list
|
(:export range date->year break-list
|
||||||
date-from-numbers date->month date->day date->string date->url date->pathname
|
date-from-numbers date->month date->day date->string date->url date->pathname
|
||||||
intersperse))
|
intersperse directory-contents))
|
||||||
|
|
||||||
(defpackage #:hssg.blog.i18n
|
(defpackage #:hssg.blog.i18n
|
||||||
(:documentation "Internationalisation support")
|
(:documentation "Internationalisation support")
|
||||||
|
@ -72,8 +72,7 @@
|
||||||
categories-artifact items
|
categories-artifact items
|
||||||
category-artifact
|
category-artifact
|
||||||
tags-artifact tag-artifact
|
tags-artifact tag-artifact
|
||||||
post-artifact blog slug content category tags author published modified)
|
post-artifact blog slug content category tags author published modified))
|
||||||
(:export read-blog))
|
|
||||||
|
|
||||||
(defpackage #:hssg.blog.artifact.category
|
(defpackage #:hssg.blog.artifact.category
|
||||||
(:use :cl)
|
(:use :cl)
|
||||||
|
@ -127,11 +126,17 @@
|
||||||
(:use :cl)
|
(:use :cl)
|
||||||
(:export post-tag->sxml pager->sxml))
|
(:export post-tag->sxml pager->sxml))
|
||||||
|
|
||||||
|
(defpackage #:hssg.blog.facade
|
||||||
|
(:use :cl)
|
||||||
|
(:export make-blog))
|
||||||
|
|
||||||
(defpackage #:hssg.blog
|
(defpackage #:hssg.blog
|
||||||
(:documentation "CommonMark parser extension for HSSG; parses CommonMark
|
(:documentation "CommonMark parser extension for HSSG; parses CommonMark
|
||||||
files to SXML trees")
|
files to SXML trees")
|
||||||
(:use #:cl)
|
(:use #:cl)
|
||||||
(:import-from #:hssg.blog.artifacts.blog read-blog)
|
(:import-from #:hssg.blog.i18n *month-names* *default-category* *localisations*)
|
||||||
(:export read-blog
|
(:import-from #:hssg.blog.facade make-blog)
|
||||||
|
(:export make-blog
|
||||||
|
*month-names* *default-category* *localisations*
|
||||||
*posts-per-page*
|
*posts-per-page*
|
||||||
*blog-post-readers*))
|
*blog-post-readers*))
|
||||||
|
|
|
@ -87,3 +87,11 @@
|
||||||
(dolist (current list (nreverse (cdr result)))
|
(dolist (current list (nreverse (cdr result)))
|
||||||
(push current result)
|
(push current result)
|
||||||
(push item result))))
|
(push item result))))
|
||||||
|
|
||||||
|
(defun directory-contents (path &key (test #'fad:directory-pathname-p))
|
||||||
|
"Contents of a directory, sorted alphabetically in ascending order."
|
||||||
|
(declare (type pathname path)
|
||||||
|
(type (function (pathname) boolean) test))
|
||||||
|
(sort (remove-if-not test (fad:list-directory path))
|
||||||
|
#'string<= :key #'namestring))
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,8 @@
|
||||||
(defpackage #:hssg.template
|
(defpackage #:hssg.template
|
||||||
(:documentation "HTML template macros; templates transform SHTML aslists.")
|
(:documentation "HTML template macros; templates transform SHTML aslists.")
|
||||||
(:use #:cl)
|
(:use #:cl)
|
||||||
(:export deftemplate template let-metadata apply-template identity-template chain-templates))
|
(:export deftemplate template let-metadata apply-template identity-template
|
||||||
|
chain-templates template-with-data))
|
||||||
|
|
||||||
(defpackage #:hssg
|
(defpackage #:hssg
|
||||||
(:documentation "The hackable static site generator")
|
(:documentation "The hackable static site generator")
|
||||||
|
@ -61,11 +62,11 @@
|
||||||
(:import-from #:hssg.artifact.verbatim make-verbatim-artifact)
|
(:import-from #:hssg.artifact.verbatim make-verbatim-artifact)
|
||||||
(:import-from #:hssg.artifact.directory make-directory-artifact)
|
(:import-from #:hssg.artifact.directory make-directory-artifact)
|
||||||
(:import-from #:hssg.artifact.html static-page read-html-lisp)
|
(:import-from #:hssg.artifact.html static-page read-html-lisp)
|
||||||
(:import-from #:hssg.template deftemplate template let-metadata apply-template identity-template chain-templates)
|
(:import-from #:hssg.template deftemplate template let-metadata apply-template identity-template
|
||||||
|
chain-templates template-with-data)
|
||||||
(:export
|
(:export
|
||||||
*site-url* *site-language*
|
*site-url* *site-language*
|
||||||
write-artifact compound-artifact artifacts
|
write-artifact compound-artifact artifacts
|
||||||
html-artifact xml-artifact static-page read-html-lisp
|
html-artifact xml-artifact static-page read-html-lisp
|
||||||
make-verbatim-artifact make-directory-artifact
|
make-verbatim-artifact make-directory-artifact
|
||||||
deftemplate template let-metadata apply-template identity-template chain-templates))
|
deftemplate template let-metadata apply-template identity-template chain-templates template-with-data))
|
||||||
(unintern 'hssg:xml-artifact)
|
|
||||||
|
|
|
@ -116,3 +116,10 @@
|
||||||
(dolist (tmp templates result)
|
(dolist (tmp templates result)
|
||||||
(declare (type template tmp))
|
(declare (type template tmp))
|
||||||
(setf result (apply-template tmp result))))))
|
(setf result (apply-template tmp result))))))
|
||||||
|
|
||||||
|
(defun template-with-data (template initial-data)
|
||||||
|
"Returns a new template which calls TEMPLATE with INITIAL-DATA and the data
|
||||||
|
passed to the template."
|
||||||
|
(declare (type template template)
|
||||||
|
(type list initial-data))
|
||||||
|
(lambda (data) (funcall template (concatenate 'list data initial-data))))
|
||||||
|
|
Loading…
Add table
Reference in a new issue