From a8016b352789800d6fe37c2087fa7fae420de825 Mon Sep 17 00:00:00 2001 From: Alexander Artemenko Date: Sat, 4 May 2024 11:34:54 +0300 Subject: [PATCH 01/11] Edited introduction section of the documentation. --- docs/introduction.lisp | 84 +++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/docs/introduction.lisp b/docs/introduction.lisp index b230863..1491872 100644 --- a/docs/introduction.lisp +++ b/docs/introduction.lisp @@ -17,19 +17,15 @@ In the era of high technology and fast access to information, when every second of page loading is worth its weight in gold, static website generators come to the fore. But why are they so important? And which tool should I choose to create the perfect static website? -We present to your attention StatiCL — one of the most promising tools in this niche. But before we dive into the specifics of StatiCL, let's figure out what static site generators are for and what advantages they bring. +We present to your attention `StatiCL` — one of the most promising tools in this niche. But before we dive into the specifics of `StatiCL`, let's figure out what static site generators are for and what advantages they bring. ## The main advantages of static site generators -— no movement, no problem. Static sites are much more resistant to hacker attacks, because they simply do not have a server that can be hacked. - -— Instant page loading becomes a reality, as static content is easily cached and does not require additional processing by the server. - -— has your website suddenly gained popularity? Static sites can easily withstand traffic growth without the need for complex infrastructure configuration. - -— using version control systems such as Git, you can always roll back to a previous version of the site or make changes without risking the current operating environment. - -— static pages do not require complex server solutions, which significantly reduces the cost of hosting. +- No movement, no problem. Static sites are much more resistant to hacker attacks, because they simply do not have a server that can be hacked. +- Instant page loading becomes a reality, as static content is easily cached and does not require additional processing by the server. +- Has your website suddenly gained popularity? Static sites can easily withstand traffic growth without the need for complex infrastructure configuration. +- Using version control systems such as Git, you can always roll back to a previous version of the site or make changes without risking the current operating environment. +- Static pages do not require complex server solutions, which significantly reduces the cost of hosting. ## How static site generators work @@ -37,46 +33,52 @@ Generators convert content from a simple Markdown markup language to HTML and CS You can use various methods to host a static site, including Github Pages, CDN, or other affordable cheap hosting services. Github Pages is a free service provided by Github that allows you to host static sites directly from the repository on Github. This is a convenient way to host small projects or personal pages. CDN (Content Delivery Network) is a network of servers distributed around the world that helps speed up the loading of content on a site due to the proximity of servers to end users. This is especially useful for sites with a large number of visitors and traffic. If you have a budget, you can also consider other cheap hosting providers that offer good conditions for hosting static sites. It is important to consider the requirements of your project and choose the appropriate option that meets your needs in terms of performance, reliability and price. -## Why was StatiCL created +## Why was `StatiCL` created -At 40, we believe in the power of Common Lisp and use it as the basis of all our projects. We used to work with the Coleslaw static blog generator, but we encountered some of its limitations. For example, it was difficult for us to create a website in several languages and set up the main page in a different way than just a list of articles. Coleslaw is more suitable for blogs than for sites with a diverse structure. Therefore, we have developed a StatiCL tool with even more flexibility. With StatiCL, you can create a static website of any complexity, without limiting yourself to blog templates. +At [40Ants](https://40ants.com/), we believe in the power of Common Lisp and use it as the basis of all our projects. We used to work with the [Coleslaw](https://github.com/coleslaw-org/coleslaw) static blog generator, but we encountered some of its limitations. For example, it was difficult for us to create a website in several languages and set up the main page in a different way than just a list of articles. Coleslaw is more suitable for blogs than for sites with a diverse structure. Therefore, we have developed a `StatiCL` tool with even more flexibility. With `StatiCL`, you can create a static website of any complexity, without limiting yourself to blog templates. -In StatiCL, you can easily create extensions, as well as use any template engine, not limited to Clozure Templates. This gives you more freedom in choosing tools to work with your static sites and allows you to use those technologies that are more convenient and familiar to you. The flexibility of StatiCL makes it an excellent choice for developers who want to create high-quality static websites optimized for their needs and preferences. +In `StatiCL`, you can easily create extensions, as well as use any template engine, not limited to Clozure Templates. This gives you more freedom in choosing tools to work with your static sites and allows you to use those technologies that are more convenient and familiar to you. The flexibility of `StatiCL` makes it an excellent choice for developers who want to create high-quality static websites optimized for their needs and preferences. -## The basic principles underlying StatiCL +## The basic principles underlying `StatiCL` -StatiCL is an innovative content processing system based on the pipeline concept. The pipeline consists of various nodes, each of which receives all the content objects generated by the previous parts of the pipeline. Each node has the ability to modify existing content or add new elements to it. This allows you to create unique and high-quality content enriched with a variety of data and information. In addition, thanks to the use of a content processing pipeline, StatiCL provides efficient and fast information processing. Each stage of the pipeline is performed sequentially, which allows you to optimize the process of creating content and improve its quality. This approach allows users to easily manage the content processing process and create unique materials for various purposes. Thanks to the flexible pipeline structure and the ability to add new nodes, StatiCL provides a high degree of personalization and customization of the content processing process for specific user needs. Thus, the system allows you to create content that meets the individual requirements and tasks of users, ensuring high efficiency and effectiveness of work. +`StatiCL` is an innovative content processing system based on the pipeline concept. The pipeline consists of various nodes, each of which receives all the content objects generated by the previous parts of the pipeline. Each node has the ability to modify existing content or add new elements to it. This allows you to create unique and high-quality content enriched with a variety of data and information. In addition, thanks to the use of a content processing pipeline, `StatiCL` provides efficient and fast information processing. Each stage of the pipeline is performed sequentially, which allows you to optimize the process of creating content and improve its quality. This approach allows users to easily manage the content processing process and create unique materials for various purposes. Thanks to the flexible pipeline structure and the ability to add new nodes, `StatiCL` provides a high degree of personalization and customization of the content processing process for specific user needs. Thus, the system allows you to create content that meets the individual requirements and tasks of users, ensuring high efficiency and effectiveness of work. -The pipeline describing the site generation is written in Lisp and is a series of nested function calls. Each site using StatiCL must contain a .staticlrc file in its root directory, which contains a description of the pipeline. This is how the simplest description of the site looks like: +The pipeline describing the site generation is written in Lisp and is a series of nested function calls. Each site using `StatiCL` must contain a .staticlrc file in its root directory, which contains a description of the pipeline. This is how the simplest description of the site looks like: - (site "Trivial Site" - :description "A trivial staticl site." - :url "https://example.com" - :pipeline (list (load-content)) - :theme "readable") +```lisp +(site "Trivial Site" + :description "A trivial staticl site." + :url "https://example.com" + :pipeline (list (load-content)) + :theme "readable") +``` When you run the staticl generate command, the engine will start processing content from files on disk. It will read all the files with the post and page extensions, and then create the corresponding html files. This way, you will receive ready-made files that can be easily placed on the site for visitors to view. -To create an RSS feed for all blog posts, you need to add a new step in the StatiCL pipeline. Let's create an element (rss) that will be responsible for generating the RSS feed. This element will extract data about blog posts and generate an appropriate XML file containing information about the titles, publication date and content of each post. This way users will be able to subscribe to blog updates via RSS readers. - - (site "Trivial Site" - :description "A trivial staticl site." - :url "https://example.com" - :pipeline (list (load-content) - (rss)) - :theme "readable") - -When creating sitemap.xml In addition to RSS, we can add another step to our pipeline - creating a sitemap. This file is a special format that allows search engines to index pages of our site more efficiently. Creation sitemap.xml It will help to improve the SEO optimization of the site, as search engines will be able to detect and index new content faster. In addition, sitemap.xml allows you to tell search engines which pages are the most important for indexing, which can also affect the ranking of the site in search results. - - (site "Trivial Site" - :description "A trivial staticl site." - :url "https://example.com" - :pipeline (list (load-content) - (rss) - (sitemap)) - :theme "readable") - -The sitemap step will receive all the content created by the previous steps and create a file at the output sitemap.xml . +To create an RSS feed for all blog posts, you need to add a new step in the `StatiCL` pipeline. Let's create an element `(rss)` that will be responsible for generating the RSS feed. This element will extract data about blog posts and generate an appropriate XML file containing information about the titles, publication date and content of each post. This way users will be able to subscribe to blog updates via RSS readers. + +```lisp +(site "Trivial Site" + :description "A trivial staticl site." + :url "https://example.com" + :pipeline (list (load-content) + (rss)) + :theme "readable") +``` + +When creating `sitemap.xml` In addition to RSS, we can add another step to our pipeline - creating a sitemap. This file is a special format that allows search engines to index pages of our site more efficiently. Creation `sitemap.xml` It will help to improve the SEO optimization of the site, as search engines will be able to detect and index new content faster. In addition, `sitemap.xml` allows you to tell search engines which pages are the most important for indexing, which can also affect the ranking of the site in search results. + +```lisp +(site "Trivial Site" + :description "A trivial staticl site." + :url "https://example.com" + :pipeline (list (load-content) + (rss) + (sitemap)) + :theme "readable") +``` + +The `(sitemap)` step will receive all the content created by the previous steps and create a file at the output `sitemap.xml`. In addition to sequentially executing the pipeline steps, you can split the content into different "streams". For example, you can filter them by the language in which the texts are written, and perform different pipeline steps for each language. For example, you can create separate RSS feeds for each language. You can read more about this in the tutorial. """ From d50adccef98b3c7a0df4535bb636c308cf2af51e Mon Sep 17 00:00:00 2001 From: Alexander Artemenko Date: Sat, 4 May 2024 18:05:46 +0300 Subject: [PATCH 02/11] Shorter ASDF system section title. --- docs/index.lisp | 6 ++++++ qlfile.lock | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/index.lisp b/docs/index.lisp index 7683002..c733748 100644 --- a/docs/index.lisp +++ b/docs/index.lisp @@ -19,6 +19,8 @@ #:@introduction) (:import-from #:serapeum #:eval-always) + (:import-from #:40ants-doc/locatives/asdf-system + #:asdf-system-documentation-title) (:export #:@index #:@readme #:@changelog)) @@ -54,6 +56,10 @@ "GIT"))) +(defmethod asdf-system-documentation-title ((system (eql (asdf:find-system "staticl")))) + "ASDF System Details") + + (defmethod docs-config ((system (eql (asdf:find-system "staticl-docs")))) ;; 40ANTS-DOC-THEME-40ANTS system will bring ;; as dependency a full 40ANTS-DOC but we don't want diff --git a/qlfile.lock b/qlfile.lock index 0aab7e8..45e3660 100644 --- a/qlfile.lock +++ b/qlfile.lock @@ -5,7 +5,7 @@ ("ultralisp" . (:class qlot/source/dist:source-dist :initargs (:distribution "https://dist.ultralisp.org/" :%version :latest) - :version "20240430080501")) + :version "20240504133502")) ("slynk" . (:class qlot/source/github:source-github :initargs (:repos "svetlyak40wt/sly" :ref nil :branch "patches" :tag nil) From 221ceab017166ea7be84d94101a60d06abeb8b72 Mon Sep 17 00:00:00 2001 From: Alexander Artemenko Date: Sat, 4 May 2024 18:08:07 +0300 Subject: [PATCH 03/11] Wrap images with a table. --- docs/index.lisp | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/index.lisp b/docs/index.lisp index c733748..bfc786f 100644 --- a/docs/index.lisp +++ b/docs/index.lisp @@ -76,20 +76,31 @@ @api))) -(defsection @index (:title "staticl - Flexible static site generator." +(defsection @index (:title "StatiCL - Flexible static site generator" :ignore-words *ignore-words*) + ;; "" + (staticl system) " -[![](https://github-actions.40ants.com/40ants/staticl/matrix.svg?only=ci.run-tests)](https://github.com/40ants/staticl/actions) - -![Quicklisp](http://quickdocs.org/badge/staticl.svg) + + + + + + + + +
+ + + +
+ +
" + (@installation section) - (@introduction section) - ;; (@usage section) - ;; (@processing-pipeline section) - ;; (@api section) - ) + (@introduction section)) (defsection-copy @readme @index) From 94aa97a284e7a1ba64b9935f6ccc60c1615dfc6a Mon Sep 17 00:00:00 2001 From: Alexander Artemenko Date: Sat, 4 May 2024 18:10:59 +0300 Subject: [PATCH 04/11] Fixed heading levels. --- docs/introduction.lisp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/introduction.lisp b/docs/introduction.lisp index 1491872..43f7c87 100644 --- a/docs/introduction.lisp +++ b/docs/introduction.lisp @@ -19,7 +19,7 @@ In the era of high technology and fast access to information, when every second We present to your attention `StatiCL` — one of the most promising tools in this niche. But before we dive into the specifics of `StatiCL`, let's figure out what static site generators are for and what advantages they bring. -## The main advantages of static site generators +# The main advantages of static site generators - No movement, no problem. Static sites are much more resistant to hacker attacks, because they simply do not have a server that can be hacked. - Instant page loading becomes a reality, as static content is easily cached and does not require additional processing by the server. @@ -27,19 +27,19 @@ We present to your attention `StatiCL` — one of the most promising tools in th - Using version control systems such as Git, you can always roll back to a previous version of the site or make changes without risking the current operating environment. - Static pages do not require complex server solutions, which significantly reduces the cost of hosting. -## How static site generators work +# How static site generators work Generators convert content from a simple Markdown markup language to HTML and CSS. They allow you to create beautiful and readable web pages without requiring the developer to write complex code. Generators greatly simplify the process of website development by providing fast and efficient formatting of text, inserting images, videos and other multimedia components. You can use various methods to host a static site, including Github Pages, CDN, or other affordable cheap hosting services. Github Pages is a free service provided by Github that allows you to host static sites directly from the repository on Github. This is a convenient way to host small projects or personal pages. CDN (Content Delivery Network) is a network of servers distributed around the world that helps speed up the loading of content on a site due to the proximity of servers to end users. This is especially useful for sites with a large number of visitors and traffic. If you have a budget, you can also consider other cheap hosting providers that offer good conditions for hosting static sites. It is important to consider the requirements of your project and choose the appropriate option that meets your needs in terms of performance, reliability and price. -## Why was `StatiCL` created +# Why was `StatiCL` created At [40Ants](https://40ants.com/), we believe in the power of Common Lisp and use it as the basis of all our projects. We used to work with the [Coleslaw](https://github.com/coleslaw-org/coleslaw) static blog generator, but we encountered some of its limitations. For example, it was difficult for us to create a website in several languages and set up the main page in a different way than just a list of articles. Coleslaw is more suitable for blogs than for sites with a diverse structure. Therefore, we have developed a `StatiCL` tool with even more flexibility. With `StatiCL`, you can create a static website of any complexity, without limiting yourself to blog templates. In `StatiCL`, you can easily create extensions, as well as use any template engine, not limited to Clozure Templates. This gives you more freedom in choosing tools to work with your static sites and allows you to use those technologies that are more convenient and familiar to you. The flexibility of `StatiCL` makes it an excellent choice for developers who want to create high-quality static websites optimized for their needs and preferences. -## The basic principles underlying `StatiCL` +# The basic principles underlying `StatiCL` `StatiCL` is an innovative content processing system based on the pipeline concept. The pipeline consists of various nodes, each of which receives all the content objects generated by the previous parts of the pipeline. Each node has the ability to modify existing content or add new elements to it. This allows you to create unique and high-quality content enriched with a variety of data and information. In addition, thanks to the use of a content processing pipeline, `StatiCL` provides efficient and fast information processing. Each stage of the pipeline is performed sequentially, which allows you to optimize the process of creating content and improve its quality. This approach allows users to easily manage the content processing process and create unique materials for various purposes. Thanks to the flexible pipeline structure and the ability to add new nodes, `StatiCL` provides a high degree of personalization and customization of the content processing process for specific user needs. Thus, the system allows you to create content that meets the individual requirements and tasks of users, ensuring high efficiency and effectiveness of work. From 86a5de6608e29b31f1cafadda58209c953fba4fe Mon Sep 17 00:00:00 2001 From: Alexander Artemenko Date: Mon, 6 May 2024 16:18:08 +0300 Subject: [PATCH 05/11] A prototype of a static server with a file watch. --- docs/making-a-static-site.lisp | 19 +++ example/.staticlrc | 4 +- example/index.page | 3 +- src/builder.lisp | 50 ++++++++ src/content.lisp | 17 ++- src/core.lisp | 48 ++----- src/main.lisp | 45 +++++-- src/server.lisp | 223 +++++++++++++++++++++++++++++++++ src/skeleton.lisp | 26 ++-- staticl.asd | 5 + 10 files changed, 378 insertions(+), 62 deletions(-) create mode 100644 docs/making-a-static-site.lisp create mode 100644 src/builder.lisp create mode 100644 src/server.lisp diff --git a/docs/making-a-static-site.lisp b/docs/making-a-static-site.lisp new file mode 100644 index 0000000..2e12368 --- /dev/null +++ b/docs/making-a-static-site.lisp @@ -0,0 +1,19 @@ +(uiop:define-package #:staticl-docs/making-a-static-site + (:use #:cl) + (:import-from #:named-readtables + #:in-readtable) + (:import-from #:40ants-doc + #:defsection) + (:import-from #:pythonic-string-reader + #:pythonic-string-syntax)) +(in-package #:staticl-docs/making-a-static-site) + + +(in-readtable pythonic-string-syntax) + + +(defsection @introduction (:title "Making a Static Site") + """ + +""" + ) diff --git a/example/.staticlrc b/example/.staticlrc index ec58c76..18a31f4 100644 --- a/example/.staticlrc +++ b/example/.staticlrc @@ -26,6 +26,6 @@ (atom :target-path #P"blog/atom.xml") (tags-index :target-path "tags/")) (sitemap)) - ;; :theme "readable" - :theme "hyde" + :theme "readable" + ;; :theme "hyde" ) diff --git a/example/index.page b/example/index.page index bbfc620..aa7860f 100644 --- a/example/index.page +++ b/example/index.page @@ -1,5 +1,5 @@ ;;;;; -title: An index page. +title: An index page created-at: 2024-03-30 08:18:05 format: md ;;;;; @@ -11,3 +11,4 @@ Englush are available at [/blog/](/blog/) URL whereas posts in Russian can be fo [/ru/blog/](/ru/blog/). Each blog has it's own rss feeds, but all pages are gathered into a single [sitemap.xml](/sitemap.xml) file. +You can edit it as you want. diff --git a/src/builder.lisp b/src/builder.lisp new file mode 100644 index 0000000..aef363b --- /dev/null +++ b/src/builder.lisp @@ -0,0 +1,50 @@ +(uiop:define-package #:staticl/builder + (:use #:cl) + (:import-from #:staticl/site + #:site-content-root + #:site-theme + #:site-url + #:make-site) + (:import-from #:staticl/content + #:write-content + #:read-contents + #:preprocess) + (:import-from #:serapeum + #:->) + (:import-from #:staticl/theme + #:copy-static) + (:import-from #:staticl/pipeline + #:execute-pipeline) + (:import-from #:staticl/current-root + #:with-current-root) + (:import-from #:staticl/url + #:with-base-url)) +(in-package #:staticl/builder) + + +(-> generate (&key + (:root-dir (or pathname string)) + (:stage-dir (or pathname string))) + (values pathname &optional)) + +(defun generate (&key + (root-dir *default-pathname-defaults*) + (stage-dir (merge-pathnames (make-pathname :directory '(:relative "stage")) + (uiop:ensure-directory-pathname root-dir)))) + (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))) + (site (make-site root-dir))) + (with-current-root ((site-content-root site)) + (with-base-url ((site-url site)) + (loop with all-content = (execute-pipeline site) + for content in all-content + do (write-content site content stage-dir)))) + + (copy-static (site-theme site) + stage-dir) + (values stage-dir))) diff --git a/src/content.lisp b/src/content.lisp index 3d5c600..da18041 100644 --- a/src/content.lisp +++ b/src/content.lisp @@ -304,7 +304,22 @@ (content-vars (template-vars site content)) (site-vars (template-vars site site)) (vars (dict "site" site-vars - "content" content-vars)) + "content" content-vars + "injections" + (dict "head" + (list " +")))) (template-name (content-template content))) (staticl/theme:render theme template-name vars stream)))) diff --git a/src/core.lisp b/src/core.lisp index e890a55..797c754 100644 --- a/src/core.lisp +++ b/src/core.lisp @@ -1,45 +1,15 @@ (uiop:define-package #:staticl (:use #:cl) - (:import-from #:staticl/site - #:site-content-root - #:site-theme - #:site-url - #:make-site) - (:import-from #:staticl/content - #:write-content - #:read-contents - #:preprocess) - (:import-from #:serapeum - #:->) - (:import-from #:staticl/theme - #:copy-static) - (:import-from #:staticl/pipeline - #:execute-pipeline) - (:import-from #:staticl/current-root - #:with-current-root) - (:import-from #:staticl/url - #:with-base-url) (:nicknames #:staticl/core) - (:export #:stage)) + (:import-from #:staticl/skeleton + #:new-site) + (:import-from #:staticl/builder + #:generate) + (:import-from #:staticl/server + #:serve) + (:export #:generate + #:new-site + #:serve)) (in-package #:staticl) -(-> stage (&key - (:root-dir pathname) - (:stage-dir pathname)) - (values &optional)) - -(defun stage (&key - (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))) - (with-current-root ((site-content-root site)) - (with-base-url ((site-url site)) - (loop with all-content = (execute-pipeline site) - for content in all-content - do (write-content site content stage-dir)))) - - (copy-static (site-theme site) - stage-dir) - (values))) diff --git a/src/main.lisp b/src/main.lisp index 82ccab2..29df710 100644 --- a/src/main.lisp +++ b/src/main.lisp @@ -17,6 +17,10 @@ :flag t) &subcommand) (let ((*verbose* verbose)) + (if verbose + (log:config :debug) + (log:config :info)) + (defmain:subcommand))) @@ -55,12 +59,39 @@ :default "A site description.") title url) "Creates a new site skeleton with a few posts." - (let ((output-dir (uiop:ensure-directory-pathname - output-dir))) - (staticl/skeleton:create-site output-dir - title - url - :description description) + (let ((full-output-dir + (staticl:new-site output-dir + title + url + :description description))) + (when *verbose* (format t "Site's content was written to: ~A~%" - (namestring output-dir))))) + (namestring full-output-dir))))) + + +(defcommand (main serve) ((source-dir "A with site's source files." + :default (namestring + (uiop:ensure-directory-pathname + *default-pathname-defaults*))) + (output-dir "An output directory to write HTML files to." + :default (namestring + (uiop:ensure-directory-pathname + (merge-pathnames "stage")))) + (port "A port number to listen on. If not given, then will be choosen automatically.") + (interface "A network interface to listen on." + :default "localhost")) + "Serves site's static from OUTPUT-DIR and rebuilds it if some sources in SOURCE-DIR were changed. + + Pages opened in the browser will be reloaded automatically after each rebuild." + (staticl:serve :root-dir (uiop:ensure-directory-pathname source-dir) + :stage-dir (uiop:ensure-directory-pathname output-dir) + :in-thread nil + :port (handler-case + (when port + (parse-integer port)) + (serious-condition () + (format *standard-output* "Unable to parse port \"~A\"." + port) + (uiop:quit 1))) + :interface interface)) diff --git a/src/server.lisp b/src/server.lisp new file mode 100644 index 0000000..4a686f5 --- /dev/null +++ b/src/server.lisp @@ -0,0 +1,223 @@ +(uiop:define-package #:staticl/server + (:use #:cl) + (:import-from #:lack.component) + (:import-from #:lack.app.file) + (:import-from #:usocket) + (:import-from #:clack) + (:import-from #:docs-builder) + (:import-from #:fs-watcher) + (:import-from #:trivial-open-browser + #:open-browser) + (:import-from #:lack/util/writer-stream + #:make-writer-stream) + (:import-from #:serapeum + #:-> + #:fmt) + (:import-from #:bordeaux-threads-2 + #:destroy-thread + #:thread-alive-p + #:make-thread + #:lock + #:condition-variable)) +(in-package #:staticl/server) + +(defvar *app* nil) +(defvar *server* nil) +(defvar *thread* nil) + + +(defun port-available-p (port interface) + (handler-case (let ((socket (usocket:socket-listen interface port :reuse-address t))) + (usocket:socket-close socket)) + (usocket:address-in-use-error (e) (declare (ignore e)) nil))) + + +(defun available-port (interface) + "Return a port number not in use from 8000 to 60000." + (loop for port from 8000 upto 60000 + if (port-available-p port interface) + return port)) + + +(defun serve-docs (root env) + (let* ((path-info (string-left-trim (list #\/) + (getf env :path-info))) + (path (if (uiop:directory-pathname-p path-info) + (merge-pathnames "index.html" path-info) + path-info)) + (full-path (merge-pathnames path root))) + (if (probe-file full-path) + (lack.component:call (make-instance 'lack.app.file:lack-app-file + :root root + :file path) + env) + (list 404 + (list :content-type "text/plain") + (list (format nil "File ~A not found." + full-path)))))) + + +(-> make-app (pathname condition-variable lock)) + +(defun make-app (root update-condition update-condition-lock) + (flet ((docs-server-app (env) + (cond + ((string-equal (getf env :path-info) + "/events") + (lambda (responder) + (let* ((event-id 0) + (remote-side (fmt "~A:~A" + (getf env :remote-addr) + (getf env :remote-port))) + (writer (funcall responder '(200 (:content-type "text/event-stream" + :cache-control "no-cache" + :x-accel-buffering "no")))) + (stream (make-writer-stream writer))) + + (handler-case + (unwind-protect + (loop for event-received = (bt2:with-lock-held (update-condition-lock) + (bt2:condition-wait update-condition update-condition-lock + ;; Every 5 seconds we'll send a ping + ;; event to ensure the connection is + ;; open or will close connection otherwise. + :timeout 5)) + do (cond + (event-received + (log:debug "Sending event to reload the page to" remote-side) + (write-string (fmt "id: ~A" (incf event-id)) stream) + (terpri stream) + (write-string "event: reload-page" stream) + (terpri stream) + (write-string "data: {}" stream) + (terpri stream) + (terpri stream)) + (t + (log:debug "Sending ping event to" remote-side) + (write-string (fmt "id: ~A" (incf event-id)) stream) + (terpri stream) + (write-string "event: ping" stream) + (terpri stream) + (write-string "data: {}" stream) + (terpri stream) + (terpri stream) + )) + (sleep 1)) + (finish-output stream)) + ((or + sb-int:broken-pipe + ;; To handle "Connection reset by peer" error: + sb-int:simple-stream-error) () + (log:debug "Closing connection to" + remote-side)))))) + (t + (serve-docs root env))))) + #'docs-server-app)) + + +(defun in-subdir-p (root file) + (let ((root (namestring root)) + (file (namestring file))) + (and (> (length file) + (length root)) + (string-equal root + (subseq file 0 (length root)))))) + + +(-> serve (&key + (:root-dir pathname) + (:stage-dir pathname) + (:in-thread t) + (:port (or null integer)) + (:interface string)) + (values &optional)) + +(defun serve (&key (root-dir *default-pathname-defaults*) + (stage-dir (merge-pathnames (make-pathname :directory '(:relative "stage")) + (uiop:ensure-directory-pathname root-dir))) + (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"")))) + + (when *server* + (log:debug "Stopping an old server") + (stop)) + + (let* ((real-stage-dir (staticl/builder::generate :root-dir root-dir + :stage-dir stage-dir)) + (port (or port + (available-port interface))) + (update-condition (bordeaux-threads-2:make-condition-variable :name "Static site updated")) + (update-condition-lock (bordeaux-threads-2:make-lock :name "Static site updated (lock)")) + (app (make-app real-stage-dir update-condition update-condition-lock)) + (server (progn + (log:info "Starting Clack server to serve site from ~A" real-stage-dir) + (clack:clackup app + :port port + :address interface))) + (url (format nil "http://~A:~A/" + interface port))) + (open-browser url) + + (labels ((build-site (changed-file) + (unless (in-subdir-p real-stage-dir changed-file) + (log:info "File ~A was changed. Rebuilding the site at ~A" + changed-file + root-dir) + (handler-case + (staticl/builder::generate :root-dir root-dir + :stage-dir stage-dir) + (serious-condition (condition) + (log:error "Unable to build static for ~A system: ~A" + root-dir + condition))) + + ;; Notifying the browser that it must reload the page: + (bordeaux-threads-2:condition-broadcast update-condition))) + + (run-site-autobuilder () + (fs-watcher:watch dirs-to-watch #'build-site))) + (cond + (in-thread + (setf *app* app) + (setf *server* server) + (setf *thread* + (make-thread #'run-site-autobuilder + :name (format nil "Site Autobuilder for ~A: ~A" + root-dir url)))) + (t + (unwind-protect + (run-site-autobuilder) + (clack:stop server)))) + + (values))))) + + +(-> stop () + (values &optional)) + +(defun stop () + (when *server* + (clack:stop *server*) + (setf *server* nil)) + (when *thread* + (when (thread-alive-p *thread*) + (destroy-thread *thread*)) + (setf *thread* + nil)) + + (values)) diff --git a/src/skeleton.lisp b/src/skeleton.lisp index e0e9bd8..1dc5ad5 100644 --- a/src/skeleton.lisp +++ b/src/skeleton.lisp @@ -9,9 +9,7 @@ (:import-from #:mystic.util #:read-template-file) (:import-from #:mystic.template.file - #:file) - (:export - #:create-site)) + #:file)) (in-package #:staticl/skeleton) @@ -43,20 +41,24 @@ :content (alexandria:read-file-into-string filename)))) -(-> create-site ((or pathname string) string string &key (:description string)) - (values &optional)) +(-> new-site ((or pathname string) string string &key (:description string)) + (values pathname &optional)) -(defun create-site (path title url &key (description "")) +(defun new-site (path title url &key (description "")) "Creates a new site skeleton with a few posts." - (let ((files (read-all-files - (asdf:system-relative-pathname :staticl - (make-pathname - :directory '(:relative "skeleton")))))) + (let ((files + (read-all-files + (asdf:system-relative-pathname :staticl + (make-pathname + :directory '(:relative "skeleton"))))) + (full-output-dir + (uiop:merge-pathnames* + (uiop:ensure-directory-pathname path)))) (mystic:render (make-instance 'staticl-site :files files) (list :title title :url url :description description) - (uiop:merge-pathnames* - (uiop:ensure-directory-pathname path))))) + full-output-dir) + (values full-output-dir))) diff --git a/staticl.asd b/staticl.asd index 066e0e1..15251a2 100644 --- a/staticl.asd +++ b/staticl.asd @@ -21,6 +21,7 @@ +(asdf:register-system-packages "bordeaux-threads" '("BORDEAUX-THREADS-2")) (asdf:register-system-packages "log4cl" '("LOG")) (asdf:register-system-packages "3bmd-ext-code-blocks" '("3BMD-CODE-BLOCKS")) (asdf:register-system-packages "fuzzy-dates" '("ORG.SHIRAKUMO.FUZZY-DATES")) @@ -28,3 +29,7 @@ (asdf:register-system-packages "mystic" '("MYSTIC.UTIL")) (asdf:register-system-packages "mystic-file-mixin" '("MYSTIC.TEMPLATE.FILE")) + +(asdf:register-system-packages "lack-app-file" '("LACK.APP.FILE")) +(asdf:register-system-packages "lack" '("LACK.COMPONENT")) +(asdf:register-system-packages "lack-util-writer-stream" '("LACK/UTIL/WRITER-STREAM")) From f59ac47f7f482d8a917e4cd63a88f5679dab7af9 Mon Sep 17 00:00:00 2001 From: Alexander Artemenko Date: Wed, 8 May 2024 01:09:57 +0300 Subject: [PATCH 06/11] Command serve now autoreloads page after rebuild. Also injections support was added. --- docs/difference-from-coleslaw.lisp | 4 + src/builder.lisp | 9 +- src/content.lisp | 26 ++--- src/event.lisp | 127 +++++++++++++++++++++ src/index/base.lisp | 6 +- src/injections.lisp | 43 ++++++++ src/pipeline.lisp | 7 +- src/plugins/autoreload.lisp | 40 +++++++ src/plugins/sitemap.lisp | 13 ++- src/server.lisp | 172 ++++++++++++++++------------- src/site.lisp | 2 +- themes/hyde/base.tmpl | 17 ++- themes/hyde/post.tmpl | 4 +- themes/readable/base.tmpl | 18 ++- themes/readable/post.tmpl | 4 +- 15 files changed, 367 insertions(+), 125 deletions(-) create mode 100644 src/event.lisp create mode 100644 src/injections.lisp create mode 100644 src/plugins/autoreload.lisp diff --git a/docs/difference-from-coleslaw.lisp b/docs/difference-from-coleslaw.lisp index dd1d362..b23b01d 100644 --- a/docs/difference-from-coleslaw.lisp +++ b/docs/difference-from-coleslaw.lisp @@ -21,6 +21,10 @@ For objects in `content.items` attribute `obj.text` was renamed to `obj.excerpt` Coleslaw always rendered pages where posts are grouped by tags. But with Staticl you have to include TAGS-INDEX function call into the site's pipeline. Without this step, tag objects will not have a \"url\" slot and template might be ready to render tags without the URL. +## Injections + +In Coleslaw there was a `injections` variable. Now it was moved to `content.injections` variable. Also, instead of two injection points `head` and `body`, `StatiCL` has `head`, `before_content` and `after_content`. + ## Other field renames - `pubdate -> site.pubdate` diff --git a/src/builder.lisp b/src/builder.lisp index aef363b..eec6cc7 100644 --- a/src/builder.lisp +++ b/src/builder.lisp @@ -24,13 +24,15 @@ (-> generate (&key (:root-dir (or pathname string)) - (:stage-dir (or pathname string))) + (:stage-dir (or pathname string)) + (:alter-pipeline function)) (values pathname &optional)) (defun generate (&key (root-dir *default-pathname-defaults*) (stage-dir (merge-pathnames (make-pathname :directory '(:relative "stage")) - (uiop:ensure-directory-pathname root-dir)))) + (uiop:ensure-directory-pathname root-dir))) + (alter-pipeline #'identity)) (let* ((root-dir ;; Here we ensure both root and stage dirs are absolute and point to the directories (merge-pathnames @@ -41,7 +43,8 @@ (site (make-site root-dir))) (with-current-root ((site-content-root site)) (with-base-url ((site-url site)) - (loop with all-content = (execute-pipeline site) + (loop with all-content = (execute-pipeline site + :alter-pipeline alter-pipeline) for content in all-content do (write-content site content stage-dir)))) diff --git a/src/content.lisp b/src/content.lisp index da18041..828d478 100644 --- a/src/content.lisp +++ b/src/content.lisp @@ -48,6 +48,8 @@ #:content-html) (:import-from #:staticl/clean-urls #:transform-filename) + (:import-from #:staticl/injections + #:content-with-injections-mixin) (:export #:supported-content-types #:content-type #:content @@ -121,6 +123,7 @@ (defclass content-from-file (content-with-title-mixin content-with-tags-mixin + content-with-injections-mixin content) ((format :initarg :format :type string @@ -304,29 +307,14 @@ (content-vars (template-vars site content)) (site-vars (template-vars site site)) (vars (dict "site" site-vars - "content" content-vars - "injections" - (dict "head" - (list " -")))) + "content" content-vars)) (template-name (content-template content))) (staticl/theme:render theme template-name vars stream)))) -(defgeneric preprocess (site plugin content-objects) - (:documentation "Returns an additional list content objects such as RSS feeds or sitemaps.")) +;; (defgeneric preprocess (site plugin content-objects) +;; (:documentation "Returns an additional list content objects such as RSS feeds or sitemaps.")) (defmethod template-vars :around ((site site) (content content) &key (hash (dict))) @@ -407,7 +395,7 @@ eventSource.onerror = function(err) { (content-tags content))) (if (next-method-p) - (call-next-method content :hash hash) + (call-next-method site content :hash hash) (values hash))) diff --git a/src/event.lisp b/src/event.lisp new file mode 100644 index 0000000..70de472 --- /dev/null +++ b/src/event.lisp @@ -0,0 +1,127 @@ +(uiop:define-package #:staticl/event + (:use #:cl) + (:import-from #:serapeum + #:-> + #:fmt) + (:import-from #:bordeaux-threads-2 + #:destroy-thread + #:thread-alive-p + #:make-thread + #:lock + #:condition-variable) + (:import-from #:alexandria + #:with-gensyms + #:once-only)) +(in-package #:staticl/event) + + +(defclass event () + ((name :initarg :name + :initform "Event" + :type string + :reader %event-name) + (counter :initform 0 + :accessor %event-counter) + (condition :initarg :condition + :type bt2:condition-variable + :reader %event-condition) + (lock :initarg :lock + :type bt2:lock + :reader %event-lock))) + + +(defmethod print-object ((event event) stream) + (print-unreadable-object (event stream :type t) + (format stream "\"~A\" counter: ~A" + (%event-name event) + (%event-counter event)))) + +(defun make-event (name) + (make-instance 'event + :condition (bt2:make-condition-variable :name (fmt "Event condition \"~A\"" name)) + :lock (bt2:make-lock :name (fmt "Event lock \"~A\"" name)))) + + +(-> notify (event) + (values &optional)) + +(defun notify (event) + (bt2:with-lock-held ((%event-lock event)) + (incf (%event-counter event)) + (bt2:condition-broadcast (%event-condition event)))) + + +(defvar *event-to-wait*) +(defvar *there-were-new-events-p*) + + +(defun there-were-new-events-p () + (unless (boundp '*there-were-new-events-p*) + (error "Function THERE-WERE-NEW-EVENTS-P should be called in context of WITH-EVENT-WAITING macro.")) + (funcall *there-were-new-events-p*)) + + +(-> call-with-event-waiting (function event)) + +(defun call-with-event-waiting (thunk event) + (when (boundp '*event-to-wait*) + (error "Unable to nest WITH-EVENT-WAITING forms!")) + + (let ((*event-to-wait* event) + (seen-counter (%event-counter event))) + (flet ((%there-were-new-events-p () + (let ((current-counter (%event-counter event))) + (prog1 (/= current-counter + seen-counter) + (setf seen-counter + current-counter))))) + (declare (dynamic-extent #'%there-were-new-events-p)) + + (let ((*there-were-new-events-p* #'%there-were-new-events-p)) + (funcall thunk))))) + + +(defmacro with-event-waiting ((event) &body body) + `(flet ((event-waiting-thunk () + ,@body)) + (call-with-event-waiting #'event-waiting-thunk ,event))) + + +(-> wait (&key (:timeout (or null alexandria:positive-integer))) + (values t &optional)) + +(defun wait (&key timeout) + (unless (and (boundp '*there-were-new-events-p*) + (boundp '*event-to-wait*)) + (error "Function WAIT should be called in context of WITH-EVENT-WAITING macro.")) + + (flet ((now () + (ceiling (/ (get-internal-real-time) + internal-time-units-per-second)))) + (bt2:with-lock-held ((%event-lock *event-to-wait*)) + (loop with wait-until = (when timeout + (+ (now) + timeout)) + do (bt2:condition-wait (%event-condition *event-to-wait*) + (%event-lock *event-to-wait*) + :timeout (when wait-until + (max 0 + (- wait-until (now))))) + (cond + ((there-were-new-events-p) + (return-from wait t)) + ((and wait-until + (>= (now) wait-until)) + (return-from wait nil))))))) + + + +;; (defun make-event-waiter (event waiter-id) +;; (flet ((do-job () +;; (loop for result = (wait event :timeout 5) +;; do (if result +;; (log:info "~A: Event occured" waiter-id) +;; (log:info "~A: Timeout" waiter-id))))) +;; (bt2:make-thread #'do-job +;; :name (fmt "Event waiter ~A" +;; waiter-id)))) diff --git a/src/index/base.lisp b/src/index/base.lisp index 44abd07..8b29c62 100644 --- a/src/index/base.lisp +++ b/src/index/base.lisp @@ -16,6 +16,8 @@ #:object-url) (:import-from #:staticl/current-root #:current-root) + (:import-from #:staticl/injections + #:content-with-injections-mixin) (:export #:index-page #:page-items #:prev-page @@ -53,7 +55,7 @@ :template *default-template*)) -(defclass index-page (content) +(defclass index-page (content-with-injections-mixin content) ((target-path :initarg :target-path :type pathname :documentation "Relative pathname to a file with page content." @@ -119,7 +121,7 @@ (staticl/url:object-url site (next-page content)))))) (if (next-method-p) - (call-next-method content :hash hash) + (call-next-method site content :hash hash) (values hash))) diff --git a/src/injections.lisp b/src/injections.lisp new file mode 100644 index 0000000..9054cea --- /dev/null +++ b/src/injections.lisp @@ -0,0 +1,43 @@ +(uiop:define-package #:staticl/injections + (:use #:cl) + (:import-from #:serapeum + #:-> + #:dict) + (:import-from #:log) + (:import-from #:staticl/theme + #:template-vars) + (:import-from #:staticl/site + #:site) + (:export #:content-injections + #:content-with-injections-mixin + #:add-injection)) +(in-package #:staticl/injections) + + +(defclass content-with-injections-mixin () + ((injections :initform (dict) + :reader content-injections))) + + +(defgeneric add-injection (content point-name html) + (:documentation "Adds a piece of HTML to the list of pieces to be inserted to a given point when content will be rendered to a file.") + + (:method ((content t) (point-name string) (html string)) + (log:warn "Injections are not supported by content of type ~S" + (class-name (class-of content))) + (values)) + + (:method ((content content-with-injections-mixin) (point-name string) (html string)) + (push html + (gethash point-name + (slot-value content 'injections))) + (values))) + + +(defmethod template-vars ((site site) (content content-with-injections-mixin) &key (hash (dict))) + (setf (gethash "injections" hash) + (content-injections content)) + + (if (next-method-p) + (call-next-method site content :hash hash) + (values hash))) diff --git a/src/pipeline.lisp b/src/pipeline.lisp index 656b9c0..e198354 100644 --- a/src/pipeline.lisp +++ b/src/pipeline.lisp @@ -34,10 +34,10 @@ (funcall *remove-item-func* item)) -(-> execute-pipeline (site) +(-> execute-pipeline (site &key (:alter-pipeline function)) (values (soft-list-of content))) -(defun execute-pipeline (site) +(defun execute-pipeline (site &key (alter-pipeline #'identity)) (let ((known-items nil) (items-to-remove nil)) (flet ((produce-item-func (item) @@ -54,7 +54,8 @@ (let ((*produce-item-func* #'produce-item-func) (*remove-item-func* #'remove-item-func)) - (loop for pipeline-node in (site-pipeline site) + (loop for pipeline-node in (funcall alter-pipeline + (site-pipeline site)) do (process-items site pipeline-node known-items) (when items-to-remove ;; This is N*M complexity, but length of items-to-remove diff --git a/src/plugins/autoreload.lisp b/src/plugins/autoreload.lisp new file mode 100644 index 0000000..e8db41c --- /dev/null +++ b/src/plugins/autoreload.lisp @@ -0,0 +1,40 @@ +(uiop:define-package #:staticl/plugins/autoreload + (:use #:cl) + (:import-from #:staticl/plugin + #:plugin) + (:import-from #:staticl/site + #:site) + (:import-from #:staticl/injections + #:add-injection + #:content-with-injections-mixin)) +(in-package #:staticl/plugins/autoreload) + + +(defparameter *code-to-inject* " +") + + +(defclass autoreload (plugin) + ()) + + +(defun autoreload () + (make-instance 'autoreload)) + + +(defmethod staticl/pipeline:process-items ((site site) (node autoreload) content-items) + (loop for item in content-items + when (typep item 'content-with-injections-mixin) + do (add-injection item "head" + *code-to-inject*))) diff --git a/src/plugins/sitemap.lisp b/src/plugins/sitemap.lisp index 738072c..6f6fccc 100644 --- a/src/plugins/sitemap.lisp +++ b/src/plugins/sitemap.lisp @@ -6,7 +6,8 @@ #:write-content-to-stream #:get-target-filename #:content - #:preprocess) + ;; #:preprocess + ) (:import-from #:staticl/site #:site) (:import-from #:serapeum @@ -33,11 +34,11 @@ :reader sitemap-content))) -(defmethod preprocess ((site site) (sitemap sitemap) contents) - (error "Old function will be removed!") - ;; (list (make-instance 'sitemap-file - ;; :contents contents)) - ) +;; (defmethod preprocess ((site site) (sitemap sitemap) contents) +;; (error "Old function will be removed!") +;; ;; (list (make-instance 'sitemap-file +;; ;; :contents contents)) +;; ) (defmethod get-target-filename ((site site) (sitemap sitemap-file) stage-dir) diff --git a/src/server.lisp b/src/server.lisp index 4a686f5..f4204a6 100644 --- a/src/server.lisp +++ b/src/server.lisp @@ -13,12 +13,20 @@ (:import-from #:serapeum #:-> #:fmt) + (:import-from #:staticl/event + #:with-event-waiting + #:event + #:make-event + #:wait + #:notify) (:import-from #:bordeaux-threads-2 #:destroy-thread #:thread-alive-p #:make-thread #:lock - #:condition-variable)) + #:condition-variable) + (:import-from #:staticl/plugins/autoreload + #:autoreload)) (in-package #:staticl/server) (defvar *app* nil) @@ -57,9 +65,9 @@ full-path)))))) -(-> make-app (pathname condition-variable lock)) +(-> make-app (pathname event)) -(defun make-app (root update-condition update-condition-lock) +(defun make-app (root event) (flet ((docs-server-app (env) (cond ((string-equal (getf env :path-info) @@ -76,33 +84,31 @@ (handler-case (unwind-protect - (loop for event-received = (bt2:with-lock-held (update-condition-lock) - (bt2:condition-wait update-condition update-condition-lock - ;; Every 5 seconds we'll send a ping - ;; event to ensure the connection is - ;; open or will close connection otherwise. - :timeout 5)) - do (cond - (event-received - (log:debug "Sending event to reload the page to" remote-side) - (write-string (fmt "id: ~A" (incf event-id)) stream) - (terpri stream) - (write-string "event: reload-page" stream) - (terpri stream) - (write-string "data: {}" stream) - (terpri stream) - (terpri stream)) - (t - (log:debug "Sending ping event to" remote-side) - (write-string (fmt "id: ~A" (incf event-id)) stream) - (terpri stream) - (write-string "event: ping" stream) - (terpri stream) - (write-string "data: {}" stream) - (terpri stream) - (terpri stream) - )) - (sleep 1)) + (with-event-waiting (event) + (loop for event-received = (wait ;; Every 5 seconds we'll send a ping + ;; event to ensure the connection is + ;; open or will close connection otherwise. + :timeout 5) + do (cond + (event-received + (log:debug "Sending event to reload the page to" remote-side) + (write-string (fmt "id: ~A" (incf event-id)) stream) + (terpri stream) + (write-string "event: reload-page" stream) + (terpri stream) + (write-string "data: {}" stream) + (terpri stream) + (terpri stream)) + (t + (log:debug "Sending ping event to" remote-side) + (write-string (fmt "id: ~A" (incf event-id)) stream) + (terpri stream) + (write-string "event: ping" stream) + (terpri stream) + (write-string "data: {}" stream) + (terpri stream) + (terpri stream) + )))) (finish-output stream)) ((or sb-int:broken-pipe @@ -157,54 +163,64 @@ (log:debug "Stopping an old server") (stop)) - (let* ((real-stage-dir (staticl/builder::generate :root-dir root-dir - :stage-dir stage-dir)) - (port (or port - (available-port interface))) - (update-condition (bordeaux-threads-2:make-condition-variable :name "Static site updated")) - (update-condition-lock (bordeaux-threads-2:make-lock :name "Static site updated (lock)")) - (app (make-app real-stage-dir update-condition update-condition-lock)) - (server (progn - (log:info "Starting Clack server to serve site from ~A" real-stage-dir) - (clack:clackup app - :port port - :address interface))) - (url (format nil "http://~A:~A/" - interface port))) - (open-browser url) - - (labels ((build-site (changed-file) - (unless (in-subdir-p real-stage-dir changed-file) - (log:info "File ~A was changed. Rebuilding the site at ~A" - changed-file - root-dir) - (handler-case - (staticl/builder::generate :root-dir root-dir - :stage-dir stage-dir) - (serious-condition (condition) - (log:error "Unable to build static for ~A system: ~A" - root-dir - condition))) - - ;; Notifying the browser that it must reload the page: - (bordeaux-threads-2:condition-broadcast update-condition))) - - (run-site-autobuilder () - (fs-watcher:watch dirs-to-watch #'build-site))) - (cond - (in-thread - (setf *app* app) - (setf *server* server) - (setf *thread* - (make-thread #'run-site-autobuilder - :name (format nil "Site Autobuilder for ~A: ~A" - root-dir url)))) - (t - (unwind-protect - (run-site-autobuilder) - (clack:stop server)))) - - (values))))) + (labels ((alter-pipeline (pipeline) + (append pipeline + ;; Here we add a node which will inject + ;; a piece of code which will listen for + ;; server side event and reload code + ;; when static was regenerated: + (list (autoreload)))) + (generate-content () + (staticl/builder::generate :root-dir root-dir + :stage-dir stage-dir + :alter-pipeline #'alter-pipeline))) + (declare (dynamic-extent #'generate-content + #'alter-pipeline)) + (let* ((real-stage-dir (generate-content)) + (port (or port + (available-port interface))) + (event (make-event "Static site updated")) + (app (make-app real-stage-dir event)) + (server (progn + (log:info "Starting Clack server to serve site from ~A" real-stage-dir) + (clack:clackup app + :port port + :address interface))) + (url (format nil "http://~A:~A/" + interface port))) + (open-browser url) + + (labels ((build-site (changed-file) + (unless (in-subdir-p real-stage-dir changed-file) + (log:info "File ~A was changed. Rebuilding the site at ~A" + changed-file + root-dir) + (handler-case + (generate-content) + (serious-condition (condition) + (log:error "Unable to build static for ~A system: ~A" + root-dir + condition))) + + ;; Notifying the browser that it must reload the page: + (notify event))) + + (run-site-autobuilder () + (fs-watcher:watch dirs-to-watch #'build-site))) + (cond + (in-thread + (setf *app* app) + (setf *server* server) + (setf *thread* + (make-thread #'run-site-autobuilder + :name (format nil "Site Autobuilder for ~A: ~A" + root-dir url)))) + (t + (unwind-protect + (run-site-autobuilder) + (clack:stop server)))) + + (values)))))) (-> stop () diff --git a/src/site.lisp b/src/site.lisp index e8a0c98..8b07fb7 100644 --- a/src/site.lisp +++ b/src/site.lisp @@ -75,7 +75,7 @@ :reader site-theme :documentation "A theme object for the site.") (pipeline :initarg :pipeline - ;; :type (soft-list-of plugin) + :type list :reader site-pipeline :documentation "A list of pipline nodes")) (:default-initargs diff --git a/themes/hyde/base.tmpl b/themes/hyde/base.tmpl index 5228eae..da4dda4 100644 --- a/themes/hyde/base.tmpl +++ b/themes/hyde/base.tmpl @@ -11,8 +11,8 @@ - {if $injections.head} - {foreach $injection in $injections.head} + {if $content.injections.head} + {foreach $injection in $content.injections.head} {$injection |noAutoescape} {/foreach} {/if} @@ -25,14 +25,23 @@ {if not isLast($link)} {sp}|{sp} {/if} {/foreach} + + {if $content.injections.before_content} + {foreach $injection in $content.injections.before_content} + {$injection |noAutoescape} + {/foreach} + {/if} +
{$raw |noAutoescape}
- {if $injections.body} - {foreach $injection in $injections.body} + + {if $content.injections.after_content} + {foreach $injection in $content.injections.after_content} {$injection |noAutoescape} {/foreach} {/if} +

diff --git a/themes/hyde/post.tmpl b/themes/hyde/post.tmpl index 5137957..5724663 100644 --- a/themes/hyde/post.tmpl +++ b/themes/hyde/post.tmpl @@ -21,7 +21,7 @@ {$content.html |noAutoescape}
{\n}
{\n} - {if $content.prev} Previous
{/if}{\n} - {if $content.next} Next
{/if}{\n} + {if $content.prev} Previous
{/if}{\n} + {if $content.next} Next
{/if}{\n}
{\n} {/template} diff --git a/themes/readable/base.tmpl b/themes/readable/base.tmpl index 485d5ea..2bae296 100644 --- a/themes/readable/base.tmpl +++ b/themes/readable/base.tmpl @@ -10,8 +10,8 @@ - {if $injections.head} - {foreach $injection in $injections.head} + {if $content.injections.head} + {foreach $injection in $content.injections.head} {$injection |noAutoescape} {/foreach} {/if} @@ -33,14 +33,22 @@
- + + + {if $content.injections.before_content} + {foreach $injection in $content.injections.before_content} +
+ {$injection |noAutoescape} +
+ {/foreach} + {/if}
{$raw |noAutoescape}
- {if $injections.body} - {foreach $injection in $injections.body} + {if $content.injections.after_content} + {foreach $injection in $content.injections.after_content}
{$injection |noAutoescape}
diff --git a/themes/readable/post.tmpl b/themes/readable/post.tmpl index e2ee6e0..6912e8c 100644 --- a/themes/readable/post.tmpl +++ b/themes/readable/post.tmpl @@ -20,8 +20,8 @@ {$content.html |noAutoescape} {\n} {/template} From afa3962a8d4b60caa9ab82ee504fd29244ad9890 Mon Sep 17 00:00:00 2001 From: Alexander Artemenko Date: Wed, 8 May 2024 01:44:44 +0300 Subject: [PATCH 07/11] Add a first version of mathjax plugin. --- example/.staticlrc | 1 + example/blog/second.post | 3 +++ src/plugins/mathjax.lisp | 42 ++++++++++++++++++++++++++++++++++++++++ src/user-package.lisp | 2 ++ 4 files changed, 48 insertions(+) create mode 100644 src/plugins/mathjax.lisp diff --git a/example/.staticlrc b/example/.staticlrc index 18a31f4..6b1a1a5 100644 --- a/example/.staticlrc +++ b/example/.staticlrc @@ -25,6 +25,7 @@ (rss :target-path #P"blog/rss.xml") (atom :target-path #P"blog/atom.xml") (tags-index :target-path "tags/")) + (mathjax) (sitemap)) :theme "readable" ;; :theme "hyde" diff --git a/example/blog/second.post b/example/blog/second.post index 12b5040..d3d5fe2 100644 --- a/example/blog/second.post +++ b/example/blog/second.post @@ -5,6 +5,9 @@ created-at: 2024-04-05 10:00 format: md ;;;;; +When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\) and they are + \[x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\] + Сегодня расскажу ещё про один мой проект, который так и не превратился в продукт. Правда в отличие от 12forks.com, этот проект жив до сих пор. Проект связан с GitHub и полезен тем, кто развивает фреймворк или библиотеку, предназначенную для разных платформ, языков программирования или операционных систем. diff --git a/src/plugins/mathjax.lisp b/src/plugins/mathjax.lisp new file mode 100644 index 0000000..450e9bf --- /dev/null +++ b/src/plugins/mathjax.lisp @@ -0,0 +1,42 @@ +(uiop:define-package #:staticl/plugins/mathjax + (:use #:cl) + (:import-from #:staticl/plugin + #:plugin) + (:import-from #:staticl/site + #:site) + (:import-from #:staticl/injections + #:add-injection + #:content-with-injections-mixin)) +(in-package #:staticl/plugins/mathjax) + + +(defparameter *code-to-inject* " + + + + +") + + +(defclass mathjax (plugin) + ()) + + +(defun mathjax () + (make-instance 'mathjax)) + + +(defmethod staticl/pipeline:process-items ((site site) (node mathjax) content-items) + (loop for item in content-items + when (typep item 'content-with-injections-mixin) + do (add-injection item "head" + *code-to-inject*))) diff --git a/src/user-package.lisp b/src/user-package.lisp index 5287d4d..6e394dd 100644 --- a/src/user-package.lisp +++ b/src/user-package.lisp @@ -13,6 +13,8 @@ #:prev-next-links) (:import-from #:staticl/plugins/sitemap #:sitemap) + (:import-from #:staticl/plugins/mathjax + #:mathjax) (:import-from #:staticl/site #:site) (:import-from #:staticl/content-pipeline From 3a88f8df4487491e7b1c19698ce346f327fa1940 Mon Sep 17 00:00:00 2001 From: Alexander Artemenko Date: Wed, 8 May 2024 02:00:55 +0300 Subject: [PATCH 08/11] Require "math" tag for force argument. --- example/blog/second.post | 2 +- src/content.lisp | 17 ++++++++++++++++- src/index/base.lisp | 7 +++++++ src/plugins/mathjax.lisp | 29 ++++++++++++++++++++++++----- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/example/blog/second.post b/example/blog/second.post index d3d5fe2..7756117 100644 --- a/example/blog/second.post +++ b/example/blog/second.post @@ -1,6 +1,6 @@ ;;;;; title: Github Actions Badger -tags: project +tags: project, math created-at: 2024-04-05 10:00 format: md ;;;;; diff --git a/src/content.lisp b/src/content.lisp index 828d478..948ba93 100644 --- a/src/content.lisp +++ b/src/content.lisp @@ -35,6 +35,7 @@ #:class-slots #:slot-definition-initargs) (:import-from #:staticl/tag + #:tag-name #:tag) (:import-from #:staticl/format #:to-html) @@ -73,7 +74,8 @@ #:set-metadata #:content-tags #:content-metadata - #:content-file-type)) + #:content-file-type + #:has-tag-p)) (in-package #:staticl/content) @@ -121,6 +123,19 @@ :tags nil)) +(defgeneric has-tag-p (content tag-name) + (:documentation "Returns T if content has a given TAG-NAME. For content which does not support tags, returns NIL.") + + (:method ((content t) (tag-name string)) + nil) + (:method ((content content-with-tags-mixin) (tag-name string)) + (when (member tag-name + (content-tags content) + :key #'tag-name + :test #'string-equal) + t))) + + (defclass content-from-file (content-with-title-mixin content-with-tags-mixin content-with-injections-mixin diff --git a/src/index/base.lisp b/src/index/base.lisp index 8b29c62..b14a3a5 100644 --- a/src/index/base.lisp +++ b/src/index/base.lisp @@ -4,6 +4,7 @@ #:dict #:soft-list-of) (:import-from #:staticl/content + #:has-tag-p #:content-template #:write-content-to-stream #:get-target-filename @@ -130,3 +131,9 @@ (relative-path (enough-namestring (page-target-path index) root))) (uiop:unix-namestring relative-path))) + + +(defmethod has-tag-p ((index index-page) (tag-name string)) + "For index pages this method will return T if at least one content item on the page has required tag name." + (loop for item in (page-items index) + thereis (has-tag-p item tag-name))) diff --git a/src/plugins/mathjax.lisp b/src/plugins/mathjax.lisp index 450e9bf..95c942f 100644 --- a/src/plugins/mathjax.lisp +++ b/src/plugins/mathjax.lisp @@ -6,7 +6,11 @@ #:site) (:import-from #:staticl/injections #:add-injection - #:content-with-injections-mixin)) + #:content-with-injections-mixin) + (:import-from #:staticl/content + #:has-tag-p) + (:import-from #:serapeum + #:->)) (in-package #:staticl/plugins/mathjax) @@ -28,15 +32,30 @@ MathJax = { (defclass mathjax (plugin) - ()) + ((force :initarg :force + :initform nil + :reader force-mathjax-p) + (tag-name :initarg :tag-name + :initform "math" + :reader math-tag-name))) -(defun mathjax () - (make-instance 'mathjax)) +(-> mathjax (&key + (:force boolean) + (:tag-name (or null string))) + (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." + (make-instance 'mathjax + :force force + :tag-name tag-name)) (defmethod staticl/pipeline:process-items ((site site) (node mathjax) content-items) (loop for item in content-items - when (typep item 'content-with-injections-mixin) + when (and (typep item 'content-with-injections-mixin) + (or (force-mathjax-p node) + (has-tag-p item (math-tag-name node)))) do (add-injection item "head" *code-to-inject*))) From 7e5f082c3014ab5fbb09d24ec418a4d08070827d Mon Sep 17 00:00:00 2001 From: Alexander Artemenko Date: Wed, 8 May 2024 18:44:44 +0300 Subject: [PATCH 09/11] Fixed the issue when mathjax replaced (some-text) in nodes outside post content. --- example/blog/math-example.post | 8 ++++++++ example/blog/second.post | 5 +---- src/plugins/mathjax.lisp | 13 ++++++++++++- src/server.lisp | 5 ++++- themes/hyde/base.tmpl | 2 +- themes/hyde/css/style.css | 6 +++--- themes/hyde/index.tmpl | 2 +- themes/hyde/post.tmpl | 2 +- themes/readable/base.tmpl | 2 +- themes/readable/index.tmpl | 4 ++-- themes/readable/post.tmpl | 6 +++--- 11 files changed, 37 insertions(+), 18 deletions(-) create mode 100644 example/blog/math-example.post diff --git a/example/blog/math-example.post b/example/blog/math-example.post new file mode 100644 index 0000000..2ee07b1 --- /dev/null +++ b/example/blog/math-example.post @@ -0,0 +1,8 @@ +;;;;; +title: Math Example +tags: plugin,math +created-at: 2024-05-09 10:00 +format: md +;;;;; + +When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\) and they are \[x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\] diff --git a/example/blog/second.post b/example/blog/second.post index 7756117..12b5040 100644 --- a/example/blog/second.post +++ b/example/blog/second.post @@ -1,13 +1,10 @@ ;;;;; title: Github Actions Badger -tags: project, math +tags: project created-at: 2024-04-05 10:00 format: md ;;;;; -When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\) and they are - \[x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\] - Сегодня расскажу ещё про один мой проект, который так и не превратился в продукт. Правда в отличие от 12forks.com, этот проект жив до сих пор. Проект связан с GitHub и полезен тем, кто развивает фреймворк или библиотеку, предназначенную для разных платформ, языков программирования или операционных систем. diff --git a/src/plugins/mathjax.lisp b/src/plugins/mathjax.lisp index 95c942f..eacbc4d 100644 --- a/src/plugins/mathjax.lisp +++ b/src/plugins/mathjax.lisp @@ -14,9 +14,16 @@ (in-package #:staticl/plugins/mathjax) +;; Here we explicitly tell MathJax to work only inside "content" and "excerpt" +;; HTML nodes, because "staticl-page" class should be on each "body" node +;; in StatiCL themes. (defparameter *code-to-inject* " + ") +" +" + (defclass mathjax (plugin) ((force :initarg :force @@ -57,5 +68,5 @@ MathJax = { when (and (typep item 'content-with-injections-mixin) (or (force-mathjax-p node) (has-tag-p item (math-tag-name node)))) - do (add-injection item "head" + do (add-injection item "after_content" *code-to-inject*))) diff --git a/src/server.lisp b/src/server.lisp index f4204a6..e510c58 100644 --- a/src/server.lisp +++ b/src/server.lisp @@ -29,6 +29,7 @@ #:autoreload)) (in-package #:staticl/server) +(defvar *port* nil) (defvar *app* nil) (defvar *server* nil) (defvar *thread* nil) @@ -188,6 +189,7 @@ :address interface))) (url (format nil "http://~A:~A/" interface port))) + (setf *port* port) (open-browser url) (labels ((build-site (changed-file) @@ -229,7 +231,8 @@ (defun stop () (when *server* (clack:stop *server*) - (setf *server* nil)) + (setf *server* nil) + (setf *port* nil)) (when *thread* (when (thread-alive-p *thread*) (destroy-thread *thread*)) diff --git a/themes/hyde/base.tmpl b/themes/hyde/base.tmpl index da4dda4..2bf1f87 100644 --- a/themes/hyde/base.tmpl +++ b/themes/hyde/base.tmpl @@ -17,7 +17,7 @@ {/foreach} {/if} - + {\n} {\n} -
{\n} +
{\n} {$content.html |noAutoescape}
{\n}
{\n} diff --git a/themes/readable/base.tmpl b/themes/readable/base.tmpl index 2bae296..b5155fa 100644 --- a/themes/readable/base.tmpl +++ b/themes/readable/base.tmpl @@ -16,7 +16,7 @@ {/foreach} {/if} - +
diff --git a/themes/readable/index.tmpl b/themes/readable/index.tmpl index 2d986a5..8bfe5b5 100644 --- a/themes/readable/index.tmpl +++ b/themes/readable/index.tmpl @@ -3,8 +3,8 @@ {template index}

{$content.title}

{foreach $obj in $content.items} -
-

{$obj.title}

+
+

{$obj.title}

posted on {$obj.created_at | date}

{$obj.excerpt |noAutoescape} diff --git a/themes/readable/post.tmpl b/themes/readable/post.tmpl index 6912e8c..9af0e83 100644 --- a/themes/readable/post.tmpl +++ b/themes/readable/post.tmpl @@ -1,7 +1,7 @@ {namespace coleslaw.theme.readable} {template post} -
{\n} +
{\n}

{$content.title}

{\n}

{if $content.tags} @@ -12,8 +12,8 @@ {/if}

- {if $content.date} - Written on {$content.date} + {if $content.created_at} + Written on {$content.created_at | date} {/if}

From 0ea40a4d047f9219206e258c78894a22f3ef8626 Mon Sep 17 00:00:00 2001 From: Alexander Artemenko Date: Wed, 8 May 2024 19:08:45 +0300 Subject: [PATCH 10/11] Added Disqus plugin. --- example/.staticlrc | 3 +- example/blog/math-example.post | 2 +- src/plugins/disqus.lisp | 59 ++++++++++++++++++++++++++++++++++ src/plugins/mathjax.lisp | 3 -- src/user-package.lisp | 2 ++ 5 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 src/plugins/disqus.lisp diff --git a/example/.staticlrc b/example/.staticlrc index 6b1a1a5..9a0f093 100644 --- a/example/.staticlrc +++ b/example/.staticlrc @@ -26,7 +26,8 @@ (atom :target-path #P"blog/atom.xml") (tags-index :target-path "tags/")) (mathjax) - (sitemap)) + (sitemap) + (disqus "staticl-example")) :theme "readable" ;; :theme "hyde" ) diff --git a/example/blog/math-example.post b/example/blog/math-example.post index 2ee07b1..9ddaea9 100644 --- a/example/blog/math-example.post +++ b/example/blog/math-example.post @@ -1,6 +1,6 @@ ;;;;; title: Math Example -tags: plugin,math +tags: plugin, math created-at: 2024-05-09 10:00 format: md ;;;;; diff --git a/src/plugins/disqus.lisp b/src/plugins/disqus.lisp new file mode 100644 index 0000000..5506b47 --- /dev/null +++ b/src/plugins/disqus.lisp @@ -0,0 +1,59 @@ +(uiop:define-package #:staticl/plugins/disqus + (:use #:cl) + (:import-from #:staticl/plugin + #:plugin) + (:import-from #:staticl/site + #:site) + (:import-from #:staticl/injections + #:add-injection + #:content-with-injections-mixin) + (:import-from #:staticl/content + #:has-tag-p) + (:import-from #:serapeum + #:fmt + #:->)) +(in-package #:staticl/plugins/disqus) + + +(defparameter *code-to-inject* " +
+ + +comments powered by Disqus +") + + +(defclass disqus (plugin) + ((shortname :initarg :shortname + :initform (error "Shortname should be given.") + :type string + :reader disqus-shortname))) + + +(-> disqus (string) + (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." + (make-instance 'disqus + :shortname shortname)) + + +(defmethod staticl/pipeline:process-items ((site site) (node disqus) content-items) + (loop for item in content-items + when (typep item 'staticl/content/post:post) + do (add-injection item "after_content" + (fmt *code-to-inject* + (disqus-shortname node))))) diff --git a/src/plugins/mathjax.lisp b/src/plugins/mathjax.lisp index eacbc4d..0709d32 100644 --- a/src/plugins/mathjax.lisp +++ b/src/plugins/mathjax.lisp @@ -38,9 +38,6 @@ MathJax = { ") -" -" - (defclass mathjax (plugin) ((force :initarg :force diff --git a/src/user-package.lisp b/src/user-package.lisp index 6e394dd..865d2ee 100644 --- a/src/user-package.lisp +++ b/src/user-package.lisp @@ -15,6 +15,8 @@ #:sitemap) (:import-from #:staticl/plugins/mathjax #:mathjax) + (:import-from #:staticl/plugins/disqus + #:disqus) (:import-from #:staticl/site #:site) (:import-from #:staticl/content-pipeline From 53520ec0ff99f150820f7f067c4dde75f283dbd9 Mon Sep 17 00:00:00 2001 From: Alexander Artemenko Date: Thu, 9 May 2024 21:45:52 +0300 Subject: [PATCH 11/11] 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)))))