From 53520ec0ff99f150820f7f067c4dde75f283dbd9 Mon Sep 17 00:00:00 2001 From: Alexander Artemenko Date: Thu, 9 May 2024 21:45:52 +0300 Subject: [PATCH] Added more documentation sections. --- docs/index.lisp | 6 + docs/making-a-static-site.lisp | 207 ++++++++++++++++++++++++++++++++- docs/pipeline.lisp | 68 +++++++++++ src/builder.lisp | 7 +- src/content-pipeline.lisp | 4 + src/content.lisp | 8 +- src/feeds/atom.lisp | 9 +- src/feeds/rss.lisp | 9 +- src/filter.lisp | 61 ++++++---- src/index/paginated.lisp | 39 ++++++- src/index/tags.lisp | 33 +++++- src/links/prev-next.lisp | 5 +- src/main.lisp | 7 +- src/plugins/disqus.lisp | 9 +- src/plugins/mathjax.lisp | 8 +- src/plugins/sitemap.lisp | 4 +- src/server.lisp | 39 ++++--- src/site.lisp | 8 +- src/skeleton.lisp | 4 +- src/user-package.lisp | 2 + src/utils.lisp | 28 +++++ 21 files changed, 494 insertions(+), 71 deletions(-) create mode 100644 docs/pipeline.lisp diff --git a/docs/index.lisp b/docs/index.lisp index bfc786f..374551b 100644 --- a/docs/index.lisp +++ b/docs/index.lisp @@ -21,6 +21,10 @@ #:eval-always) (:import-from #:40ants-doc/locatives/asdf-system #:asdf-system-documentation-title) + (:import-from #:staticl-docs/making-a-static-site + #:@making-a-site) + (:import-from #:staticl-docs/pipeline + #:@pipeline) (:export #:@index #:@readme #:@changelog)) @@ -73,6 +77,8 @@ (find-symbol "40ANTS-THEME" (find-package "40ANTS-DOC-THEME-40ANTS")) :root-sections '(@index + @making-a-site + @pipeline @api))) diff --git a/docs/making-a-static-site.lisp b/docs/making-a-static-site.lisp index 2e12368..a1bb80a 100644 --- a/docs/making-a-static-site.lisp +++ b/docs/making-a-static-site.lisp @@ -12,8 +12,213 @@ (in-readtable pythonic-string-syntax) -(defsection @introduction (:title "Making a Static Site") +(defsection @making-a-site (:title "Making a Static Site" + :ignore-words ("YAML" + "IDE" + "URL" + "CSS" + "HTML" + "REPL" + "JS" + "TODO" + "RSS")) """ +In this tutorial, we will create our first static website using Static. The site will have pages "Home", "About the site", "Services" and a blog with posts. The blog posts will be collected in an RSS feed, and all pages will be presented in a file sitemap.xml . At the end, we will connect the Disqus comment system to the posts for feedback from visitors. Thanks to these additional features, the site will be more user-friendly and attractive to users. +## Initializing static for the site + +For a quick start, `StatiCL` allows you to create the structure of a future site using a simple command. You can call it via Lisp REPL: + +```lisp +CL-USER> (staticl:new-site "/tmp/my-site" + "My Lisp Site" + "https://about-lisp.org") +#P"/tmp/my-site/" +``` + +Or you can call the same command from the command line if you installed `StatiCL` using `Roswell` - just open a terminal and enter this command to create the initial site config: + +```lisp +$ staticl --verbose new-site -o /tmp/my-site 'My Lisp Site' https://about-lisp.org +Site's content was written to: /tmp/my-site/ +``` + +Let's look at what happened as a result: + +```bash +$ tree -a /tmp/blah +/tmp/blah +├── .staticlrc +├── about.page +├── blog +│   ├── first.post +│   └── second.post +└── index.page +``` + +Here we see the site config `.staticlrc`, two regular pages: `index.page` and `about.page`, as well as two blog posts. And here are the settings generated for us: + +``` +(asdf:load-system "staticl/format/spinneret") + +(site "My Lisp Site" + :description "A site description." + :url "https://about-lisp.org" + :navigation (menu (item "Blog" "/blog/") + (item "About" "/about/")) + :pipeline (list (load-content) + (prev-next-links) + (paginated-index :target-path "blog/") + (rss :target-path "blog/rss.xml") + (atom :target-path "blog/atom.xml") + (tags-index :target-path "tags/") + (sitemap)) + :theme "readable") +``` + +To create a site, the STATICL/USER-PACKAGE:SITE function is used, which passes the site title, its description, the `URL` for publication, links for navigation and a description of the content processing pipeline. The pipeline plays a key role in the formation of the site, since the final result depends on it. To load the `staticl/format/spinneret` dependency, you need to call the `asdf:load-system` function. This module adds support for the `Spinneret` format, an example of which can be found in the `about.post` file. Now let's look at the contents of the pipeline. + +The first call in the pipeline is STATICL/USER-PACKAGE:LOAD-CONTENT function. It is responsible for downloading content from files with the `post` and `page` extensions. Next comes the STATICL/USER-PACKAGE:PREV-NEXT-LINKS block, it links the "post" type content together, due to which the `Previous` and `Next` links appear on the blog pages. The STATICL/USER-PACKAGE:PAGINATED-INDEX block is responsible for creating pages on which blog posts are grouped into `N` pieces. Here, in the example, the number of posts per page is not specified, but the STATICL/USER-PACKAGE:PAGINATED-INDEX function can accept the `:PAGE-SIZE` argument, as well as some other arguments. The great news is that when you edit such a config in an editor that supports working with Common Lisp, the IDE will tell you what the signature of each function is and what parameters it can have. Try to do this with a static site generator that uses the YAML format for configuration! + +Next we have two calls of STATICL/USER-PACKAGE:RSS function and STATICL/USER-PACKAGE:ATOM function. They are similar in that they give a feed output from the latest blog posts. Well, the STATICL/USER-PACKAGE:SITEMAP function generates a file `sitemap.xml ` and includes all the content that was created in the previous stages. + +## Generating a static website + +Now that we have some content, let's make an HTML website out of it. To do this, run the following command in REPL: + +```lisp +CL-USER> (staticl:generate :root-dir "/tmp/my-site" + :stage-dir "/tmp/result") +#P"/tmp/result/" +``` + +Or on the command line: + +```bash +$ staticl -v generate -s /tmp/my-site -o /tmp/result +Site was written to: /tmp/result/ +``` + +This command created several HTML files in the `/tmp/result/` directory, and also put the necessary CSS and JS files there: + +```bash +$ tree /tmp/result +/tmp/result +├── about +│   └── index.html +├── blog +│   ├── atom.xml +│   ├── first +│   │   └── index.html +│   ├── index.html +│   ├── rss.xml +│   └── second +│   └── index.html +├── css +│   ├── bootstrap.min.css +│   └── custom.css +├── img +│   ├── cc-by-sa.png +│   ├── glyphicons-halflings-white.png +│   ├── glyphicons-halflings.png +│   └── staticl-logo-small.webp +├── index.html +├── js +│   └── bootstrap.min.js +├── sitemap.xml +└── tags + ├── bar + │   └── index.html + ├── example + │   └── index.html + └── foo + └── index.html +``` + +Now we need to somehow open the site in the browser. If you just open the file in the browser `index.html `, then he will load it without styles, and other things may not work either, since the page opened in this way will not have a domain. To fully open the site, we need to launch a web server. Previously, I would have done this using `python`. This is how you can distribute static from a local directory using `python` and its `http.server` module: + +```bash +$ cd /tmp/result +$ python3 -m http.server +Serving HTTP on :: port 8000 (http://[::]:8000/) ... +``` + +However, we can do better – a web server is already built into `StatiCL`. Moreover, it can track changes in your site's files and update the site pages opened in the browser. To start this local web server, use the following command in REPL: + +```lisp +CL-USER> (staticl:serve :root-dir #P"/tmp/my-site" + :stage-dir #P"/tmp/result") + [21:35:55] staticl/utils utils.lisp (top level form find-class-by-name) - + Searching class by name STATICL/UTILS::NAME: "closure-template" + [21:35:55] staticl/server server.lisp (top level form serve) - + Starting Clack server to serve site from /tmp/result/ +Hunchentoot server is started. +Listening on localhost:8001. +``` + +Or from the command line: + +```bash +$ staticl serve -s /tmp/my-site -o /tmp/result + [21:39:48] staticl/server server.lisp (top level form serve) - + Starting Clack server to serve site from /tmp/result/ +Hunchentoot server is started. +Listening on localhost:8001. +``` + +As you can see, the `serve` command is very similar to `generate`, and accepts similar parameters, because it needs not only to distribute static, but also to generate it. In addition, the `serve` command will try to open the site in the browser. Look at it: + +![Image](https://storage.yandexcloud.net/40ants-public/staticl/docs/tutorial/site-example.webp) + +If you click on the `Blog` link, a page with a list of posts will open. This part of the pipeline is responsible for its generation: `(paginated-index :target-path #P"blog/")'. The page looks like this: + +![Image](https://storage.yandexcloud.net/40ants-public/staticl/docs/tutorial/first-page.webp) + +On the index page, `StatiCL` displays only the introduction if the source file of the post separates it from the main part with the line ``. + +If you click on one of the posts, it will open in its entirety: + +![Image](https://storage.yandexcloud.net/40ants-public/staticl/docs/tutorial/second-post.webp) + +Pay attention to the "Next" link in the lower right corner. All blog posts are linked to each other and this is also done thanks to a separate pipeline block: `(prev-next-links)'. This pipeline block adds metadata to each post, which is then available in the template. If you remove it, the links will disappear from the pages. + +## How to add comments using Disqus + + +Like any content creator, you will definitely want to communicate with your readers. The easiest way to do this is to connect dynamic comments to our static blog. There are many services that provide such comments – for example, Disqus, Commento, Remark42, etc.. + +At the moment, `StatiCL` only supports Disqus, but it's easy to write a plugin for any other comment system. + +Let's add comments to our website! All you need to do is register with Disqus, get a short site name, and add another pipeline block to the config. Let's say I registered the site name `example` in Disqus, and then I need to add the `(disqus "example") block to our pipeline: + + +```lisp +:pipeline (list (load-content) + (prev-next-links) + (paginated-index :target-path #P"blog/") + (rss :target-path "blog/rss.xml") + (atom :target-path "blog/atom.xml") + (tags-index :target-path "tags/") + (sitemap) + (disqus "example")) +``` + +This is how the post page with comments will look like: + +![Image](https://storage.yandexcloud.net/40ants-public/staticl/docs/tutorial/post-with-comments.webp) + +## Content filtering + +Now let's assume that we don't want to connect comments to all posts, but only to those where there is no `no-comments` tag. To do this, we can filter the content using the STATICL/USER-PACKAGE:FILTER block. This block accepts a number of parameters, as well as other pipeline blocks, which will receive only those content elements that have passed the filter. This is how the filter will look like, which will apply STATICL/USER-PACKAGE:DISQUS function only to content that does not have the `no-comments` tag: + +```lisp +(filter (:tags "no-comments" + :invert t) + (disqus "example")) +``` + +Now add the `no-comments` tag to the `blog/first.post` file and make sure that comments are not displayed on the page of this post. They are on the page of the second post. + +In the following tutorials, we will figure out how to create themes for your static website and learn how to add the necessary functionality using plugins. """ ) diff --git a/docs/pipeline.lisp b/docs/pipeline.lisp new file mode 100644 index 0000000..9a35e35 --- /dev/null +++ b/docs/pipeline.lisp @@ -0,0 +1,68 @@ +(uiop:define-package #:staticl-docs/pipeline + (:use #:cl) + (:import-from #:named-readtables + #:in-readtable) + (:import-from #:40ants-doc + #:defsection) + (:import-from #:pythonic-string-reader + #:pythonic-string-syntax) + (:shadowing-import-from #:staticl/user-package + #:atom) + (:import-from #:staticl/user-package + #:tags-index + #:paginated-index + #:filter + #:atom + #:mathjax + #:rss + #:load-content + #:prev-next-links + #:disqus + #:sitemap)) +(in-package #:staticl-docs/pipeline) + + +(in-readtable pythonic-string-syntax) + +(defsection @pipeline (:title "Pipeline" + :ignore-words ("HTML" + "XML" + "MathJAX" + "STATICL-USER")) + (@idea section) + (@building-blocks section)) + + +(defsection @idea (:title "Idea") + """ +Pipeline is the basis of Static. All the content of the site passes through the pipeline, and is converted at the output to HTML, XML, and other formats. The pipeline principle is used here – each part of the pipeline processes or creates new content elements, and those, in turn, are processed by subsequent pipeline blocks. + +In the static site config, the pipeline is assembled using function calls. But these functions do not create any content at the time of loading the configuration file. They only return a description of the pipeline. + +For example, the pipeline of the simplest site will consist of only one element: load-content. He is responsible for reading pages and posts from files. + +If we want to do something with posts and pages, for example, create for them `sitemap.xml `, then we can add a call to another SITEMAP function after the LOAD-CONTENT function. Both of these functions return the "nodes" of the pipeline. Each node is an object that will then be used when calling generic-function STATICL/PIPELINE:PROCESS-ITEMS to process the content. +""") + + +(defsection @building-blocks (:title "Building Blocks") + "All symbols, listed below, are available in the STATICL-USER package and can be used in the `.staticlrc` file without a package prefix." + + "# Main Building Blocks" + (load-content function) + (filter macro) + + "# Content Organization" + (prev-next-links function) + (paginated-index function) + (tags-index function) + + "# Feeds" + + (atom function) + (rss function) + (sitemap function) + + "# Plugins" + (disqus function) + (mathjax function)) diff --git a/src/builder.lisp b/src/builder.lisp index eec6cc7..f090943 100644 --- a/src/builder.lisp +++ b/src/builder.lisp @@ -7,8 +7,7 @@ #:make-site) (:import-from #:staticl/content #:write-content - #:read-contents - #:preprocess) + #:read-contents) (:import-from #:serapeum #:->) (:import-from #:staticl/theme @@ -18,7 +17,9 @@ (:import-from #:staticl/current-root #:with-current-root) (:import-from #:staticl/url - #:with-base-url)) + #:with-base-url) + (:export + #:generate)) (in-package #:staticl/builder) diff --git a/src/content-pipeline.lisp b/src/content-pipeline.lisp index 81254fc..230f0ce 100644 --- a/src/content-pipeline.lisp +++ b/src/content-pipeline.lisp @@ -21,6 +21,10 @@ (values load-content &optional)) (defun load-content (&key (exclude (list ".qlot"))) + "Reads content from the disk. + + By default only `post` and `page` files are loaded. But this list could be extended if you'll define + a custom site class and a method for STATICL/CONTENT:SUPPORTED-CONTENT-TYPES generic-function." (make-instance 'load-content :exclude exclude)) diff --git a/src/content.lisp b/src/content.lisp index 948ba93..b2a27cf 100644 --- a/src/content.lisp +++ b/src/content.lisp @@ -16,6 +16,7 @@ #:with-output-to-file #:length=) (:import-from #:staticl/utils + #:comma-split #:normalize-plist #:do-files) (:import-from #:staticl/content/reader @@ -59,7 +60,7 @@ #:content-class #:write-content-to-stream #:write-content - #:preprocess + ;; #:preprocess #:get-target-filename #:content-with-title-mixin #:content-with-tags-mixin @@ -187,10 +188,9 @@ (etypecase value (list value) (string - (loop for tag-name in (str:split "," value - :omit-nulls t) + (loop for tag-name in (comma-split value) collect (make-instance 'tag - :name (str:trim tag-name)))))))) + :name tag-name))))))) (result (apply #'call-next-method obj normalized-args)) (all-initargs (loop for slot in (class-slots (class-of obj)) diff --git a/src/feeds/atom.lisp b/src/feeds/atom.lisp index 4780323..2dd0d86 100644 --- a/src/feeds/atom.lisp +++ b/src/feeds/atom.lisp @@ -14,6 +14,11 @@ :feed-type 'org.shirakumo.feeder:atom)) -(defun atom (&key (target-path #P"atom.xml")) +(defun atom (&key (target-path #P"atom.xml") + (limit 10)) + "Creates an XML feed in Atom format at TARGET-PATH. + + Only a LIMIT latest posts are included into the feed." (make-instance 'atom - :target-path (pathname target-path))) + :target-path (pathname target-path) + :length-limit limit)) diff --git a/src/feeds/rss.lisp b/src/feeds/rss.lisp index 6b2c7d1..e94b367 100644 --- a/src/feeds/rss.lisp +++ b/src/feeds/rss.lisp @@ -13,6 +13,11 @@ :feed-type 'org.shirakumo.feeder:rss)) -(defun rss (&key (target-path #P"rss.xml")) +(defun rss (&key (target-path #P"rss.xml") + (limit 10)) + "Creates an XML feed in Rss format at TARGET-PATH. + + Only a LIMIT latest posts are included into the feed." (make-instance 'rss - :target-path (pathname target-path))) + :target-path (pathname target-path) + :length-limit limit)) diff --git a/src/filter.lisp b/src/filter.lisp index 1a454ed..88aaa1f 100644 --- a/src/filter.lisp +++ b/src/filter.lisp @@ -7,9 +7,12 @@ (:import-from #:staticl/site #:site) (:import-from #:staticl/content + #:has-tag-p #:content) (:import-from #:staticl/current-root #:current-root) + (:import-from #:staticl/utils + #:comma-split) (:export #:filter #:filter-fn #:pipeline-items)) @@ -32,32 +35,44 @@ :pipeline nil)) -(defmacro filter ((&key path invert) &rest pipeline) - "Filters input content objects and processes them using steps given as a body. +(defmacro filter ((&key path tags invert) &body pipeline) + "Filters input content objects and processes them using pipeline items given as a body. Arguments: - - PATH: if given result will contain only items read from the given path. - - INVERT: inverts effect of the filter." - (alexandria:once-only (path) - (let (rules) - - (when path - (push `(path-matches-p item (merge-pathnames (uiop:ensure-directory-pathname ,path) - (current-root))) - rules)) - - (setf rules - (if invert - `(not (and ,@rules)) - `(and ,@rules))) - - - `(flet ((filter-fn (item) - ,rules)) - (make-instance 'filter - :filter-fn #'filter-fn - :pipeline (list ,@pipeline)))))) + - PATH: result will contain only items read from the given path. + - TAGS: result will contain only items having all given tags. + - INVERT: inverts effect of the filter. + - PIPELINE: any number of function calls returning pipeline nodes. + + **Note:** Right now, all new items generated by PIPELINE given to the FILTER macro + are added to the toplevel list and stay invisible to the sebsequent pipeline nodes. + The same applicable to content deletion." + + (let (rules) + (when path + (push `(path-matches-p item (merge-pathnames (uiop:ensure-directory-pathname ,path) + (current-root))) + rules)) + + (when tags + (loop for tag-name in (etypecase tags + (string (comma-split tags)) + (list tags)) + do (push `(has-tag-p item ,tag-name) + rules))) + + (setf rules + (if invert + `(not (and ,@rules)) + `(and ,@rules))) + + + `(flet ((filter-fn (item) + ,rules)) + (make-instance 'filter + :filter-fn #'filter-fn + :pipeline (list ,@pipeline))))) (-> filter-items ((soft-list-of content) filter) diff --git a/src/index/paginated.lisp b/src/index/paginated.lisp index b2fe8ca..bdfaab5 100644 --- a/src/index/paginated.lisp +++ b/src/index/paginated.lisp @@ -12,6 +12,8 @@ #:fmt) (:import-from #:staticl/content/post #:postp) + (:import-from #:staticl/utils + #:slot-documentation) (:export #:paginated-index #:page-filename-fn #:page-title-fn)) @@ -51,7 +53,7 @@ :documentation "A callback to change page titles. Accepts single argument - a page number and should return a pathname relative to the site's root. - By default, it returns index.html for the first page and page-2.html, page-3.html for others. + By default, it returns `index.html` for the first page and `page-2.html`, `page-3.html` for others. If site has \"clean urls\" setting enabled, then additional transformation to the pathname will be applied automatically." @@ -82,6 +84,31 @@ initargs)) +(let ((docs + (fmt " +Creates additional HTML files with post's excerpts grouped by PAGE-SIZE items. + +By default `index.html`, `page-2.html`, `page-3.html`, etc. filenames are used, but this +can be overriden by PAGE-FILENAME-FN argument. + +The same way page title may be overriden by providing a function as PAGE-TITLE-FN argument. + +# Arguments: + +**PAGE-FILENAME-FN**: + +~A + +**PAGE-TITLE-FN**: + +~A +" + (slot-documentation 'paginated-index 'page-filename-fn) + (slot-documentation 'paginated-index 'page-title-fn)))) + (setf (documentation 'paginated-index 'function) + docs)) + + (defmethod staticl/pipeline:process-items ((site site) (index paginated-index) content-items) (loop with only-posts = (remove-if-not #'postp content-items) with sorted-posts = (sort only-posts @@ -101,9 +128,9 @@ :items batch) into pages finally (loop for (prev page next) on (list* nil pages) when page - do (setf (staticl/index/base:prev-page page) - prev - (staticl/index/base:prev-page page) - next) - (staticl/pipeline:produce-item page))) + do (setf (staticl/index/base:prev-page page) + prev + (staticl/index/base:prev-page page) + next) + (staticl/pipeline:produce-item page))) (values)) diff --git a/src/index/tags.lisp b/src/index/tags.lisp index 7e1e244..df0fa5c 100644 --- a/src/index/tags.lisp +++ b/src/index/tags.lisp @@ -28,7 +28,13 @@ (:import-from #:staticl/theme #:template-vars) (:import-from #:staticl/url - #:object-url)) + #:object-url) + (:import-from #:staticl/utils + #:slot-documentation) + (:export + #:tags-index + #:page-filename-fn + #:page-title-fn)) (in-package #:staticl/index/tags) @@ -95,6 +101,31 @@ initargs)) +(let ((docs + (fmt " +Creates additional HTML files with post's excerpts grouped by tag names. + +By default `some.html`, `another.html` filenames are used, but this +can be overriden by PAGE-FILENAME-FN argument. + +The same way page title may be overriden by providing a function as PAGE-TITLE-FN argument. + +# Arguments: + +**PAGE-FILENAME-FN**: + +~A + +**PAGE-TITLE-FN**: + +~A +" + (slot-documentation 'tags-index 'page-filename-fn) + (slot-documentation 'tags-index 'page-title-fn)))) + (setf (documentation 'tags-index 'function) + docs)) + + (defclass bound-tag (tag) ((index-page :initarg :index-page :reader tag-index-page)) diff --git a/src/links/prev-next.lisp b/src/links/prev-next.lisp index 015691c..3532e87 100644 --- a/src/links/prev-next.lisp +++ b/src/links/prev-next.lisp @@ -19,7 +19,10 @@ (defun prev-next-links () - "Creates a links between pages." + "Creates a links between pages. + + Links are added to the content item's metadata and available in templates as + `content.prev.url` and `content.next.url` variables." (make-instance 'prev-next-links)) diff --git a/src/main.lisp b/src/main.lisp index 29df710..ba37fb8 100644 --- a/src/main.lisp +++ b/src/main.lisp @@ -1,7 +1,6 @@ (uiop:define-package #:staticl/main (:use #:cl) - (:import-from #:staticl/core - #:stage) + (:import-from #:staticl) (:import-from #:staticl/skeleton) (:import-from #:defmain #:subcommand @@ -44,8 +43,8 @@ (namestring source-dir)) (uiop:quit 1)) - (stage :root-dir source-dir - :stage-dir output-dir) + (staticl:generate :root-dir source-dir + :stage-dir output-dir) (when *verbose* (format t "Site was written to: ~A~%" (namestring output-dir))))) diff --git a/src/plugins/disqus.lisp b/src/plugins/disqus.lisp index 5506b47..1792fda 100644 --- a/src/plugins/disqus.lisp +++ b/src/plugins/disqus.lisp @@ -11,7 +11,10 @@ #:has-tag-p) (:import-from #:serapeum #:fmt - #:->)) + #:->) + (:export + #:disqus + #:disqus-shortname)) (in-package #:staticl/plugins/disqus) @@ -46,7 +49,9 @@ (values disqus &optional)) (defun disqus (shortname) - "Enables Disqus on the page if it's content has tag equal to the TAG-NAME or if FORCE argument was given." + "Enables Disqus on the page. + + To make it work, you have to register your site at Disqus and provide a short site name to the function." (make-instance 'disqus :shortname shortname)) diff --git a/src/plugins/mathjax.lisp b/src/plugins/mathjax.lisp index 0709d32..69280db 100644 --- a/src/plugins/mathjax.lisp +++ b/src/plugins/mathjax.lisp @@ -10,7 +10,11 @@ (:import-from #:staticl/content #:has-tag-p) (:import-from #:serapeum - #:->)) + #:->) + (:export + #:mathjax + #:force-mathjax-p + #:math-tag-name)) (in-package #:staticl/plugins/mathjax) @@ -54,7 +58,7 @@ MathJax = { (values mathjax &optional)) (defun mathjax (&key force (tag-name "math")) - "Enables MathJAX on the page if it's content has tag equal to the TAG-NAME or if FORCE argument was given." + "Enables `MathJAX` on the page if it's content has tag equal to the TAG-NAME or if FORCE argument was given." (make-instance 'mathjax :force force :tag-name tag-name)) diff --git a/src/plugins/sitemap.lisp b/src/plugins/sitemap.lisp index 6f6fccc..20c913b 100644 --- a/src/plugins/sitemap.lisp +++ b/src/plugins/sitemap.lisp @@ -16,7 +16,9 @@ #:make-url #:render-sitemap) (:import-from #:staticl/url - #:object-url)) + #:object-url) + (:export + #:sitemap)) (in-package #:staticl/plugins/sitemap) diff --git a/src/server.lisp b/src/server.lisp index e510c58..5a23366 100644 --- a/src/server.lisp +++ b/src/server.lisp @@ -26,7 +26,9 @@ #:lock #:condition-variable) (:import-from #:staticl/plugins/autoreload - #:autoreload)) + #:autoreload) + (:export + #:serve)) (in-package #:staticl/server) (defvar *port* nil) @@ -132,8 +134,8 @@ (-> serve (&key - (:root-dir pathname) - (:stage-dir pathname) + (:root-dir (or pathname string)) + (:stage-dir (or pathname string)) (:in-thread t) (:port (or null integer)) (:interface string)) @@ -145,20 +147,22 @@ (in-thread t) port (interface "localhost")) - (let ((root-dir - ;; Here we ensure both root and stage dirs are absolute and point to the directories - (merge-pathnames - (uiop:ensure-directory-pathname root-dir))) - (stage-dir - (merge-pathnames - (uiop:ensure-directory-pathname stage-dir))) - (dirs-to-watch - (list root-dir - ;; We also want to watch if staticl - ;; directory changes. This will make it - ;; easier to edit builtin themes. - (asdf:system-relative-pathname "staticl" - #P"")))) + (let* ((root-dir + ;; Here we ensure both root and stage dirs are absolute and point to the directories + (merge-pathnames + (uiop:ensure-directory-pathname root-dir))) + (stage-dir + (merge-pathnames + (uiop:ensure-directory-pathname stage-dir))) + ;; It is important to ensure directories pathnames in this + ;; list. This is why we use LET*. + (dirs-to-watch + (list root-dir + ;; We also want to watch if staticl + ;; directory changes. This will make it + ;; easier to edit builtin themes. + (asdf:system-relative-pathname "staticl" + #P"")))) (when *server* (log:debug "Stopping an old server") @@ -208,6 +212,7 @@ (notify event))) (run-site-autobuilder () + (log:info "Watching on" dirs-to-watch) (fs-watcher:watch dirs-to-watch #'build-site))) (cond (in-thread diff --git a/src/site.lisp b/src/site.lisp index 8b07fb7..314b815 100644 --- a/src/site.lisp +++ b/src/site.lisp @@ -90,7 +90,13 @@ (defun site (title &rest args) (when (getf args :url) - (assert-absolute-url (getf args :url))) + (assert-absolute-url (getf args :url)) + + ;; We need URL end with a backslash to + ;; make URL generation for pages work correctly: + (setf (getf args :url) + (str:ensure-suffix "/" (getf args :url)))) + (apply #'make-instance 'site :title title diff --git a/src/skeleton.lisp b/src/skeleton.lisp index 1dc5ad5..0b2be51 100644 --- a/src/skeleton.lisp +++ b/src/skeleton.lisp @@ -9,7 +9,9 @@ (:import-from #:mystic.util #:read-template-file) (:import-from #:mystic.template.file - #:file)) + #:file) + (:export + #:new-site)) (in-package #:staticl/skeleton) diff --git a/src/user-package.lisp b/src/user-package.lisp index 865d2ee..068b0dd 100644 --- a/src/user-package.lisp +++ b/src/user-package.lisp @@ -33,3 +33,5 @@ #:paginated-index) (:import-from #:staticl/index/tags #:tags-index)) +(in-package #:staticl-user) + diff --git a/src/utils.lisp b/src/utils.lisp index c00353b..e277f93 100644 --- a/src/utils.lisp +++ b/src/utils.lisp @@ -3,6 +3,7 @@ (:import-from #:log) (:import-from #:quri) (:import-from #:str + #:trim #:trim-left) (:import-from #:serapeum #:maphash-new @@ -13,6 +14,10 @@ #:with-gensyms) (:import-from #:cl-fad #:walk-directory) + (:import-from #:closer-mop + #:class-direct-slots) + (:import-from #:40ants-doc/docstring + #:strip-docstring-indentation) (:export #:do-files #:normalize-plist @@ -180,3 +185,26 @@ BODY on files that match the given extension." (error "There is no host in ~S." url)) url)) + + +(-> comma-split (string) + (values (serapeum:soft-list-of string) + &optional)) + +(defun comma-split (text) + (mapcar #'trim + (str:split "," + text + :omit-nulls t))) + + +(-> slot-documentation (symbol symbol) + (values (or null string) + &optional)) + +(defun slot-documentation (class-name slot-name) + (loop for slot in (class-direct-slots (find-class class-name)) + when (eql (closer-mop:slot-definition-name slot) + slot-name) + do (return (strip-docstring-indentation + (documentation slot t)))))