Recently Written · git

coleslaw

Emacs mode for the "Coleslaw" site generator written in Common Lisp.

git clone https://github.com/equwal/coleslaw

Log | Files | Refs


src/content.lisp (4488 bytes)

1 (in-package :coleslaw)
2 
3 ;; Tagging
4 
5 (defclass tag ()
6   ((name :initarg :name :reader tag-name)
7    (slug :initarg :slug :reader tag-slug)
8    (url  :initarg :url)))
9 
10 (defmethod initialize-instance :after ((tag tag) &key)
11   (with-slots (url slug) tag
12     (setf url (compute-url nil slug 'tag-index))))
13 
14 (defun make-tag (str)
15   "Takes a string and returns a TAG instance with a name and slug."
16   (let ((trimmed (string-trim " " str)))
17     (make-instance 'tag :name trimmed :slug (slugify trimmed))))
18 
19 (defun tag-slug= (a b)
20   "Test if the slugs for tag A and B are equal."
21   (string= (tag-slug a) (tag-slug b)))
22 
23 ;; Slugs
24 
25 (defun slug-char-p (char &key (allowed-chars (list #\- #\~)))
26   "Determine if CHAR is a valid slug (i.e. URL) character."
27   ;; use the first char of the general unicode category as kind of
28   ;; hyper general category
29   (let ((cat (char (cl-unicode:general-category char) 0))
30         (allowed-cats (list #\L #\N))) ; allowed Unicode categories in URLs
31         (cond
32           ((member cat allowed-cats)   t)
33           ((member char allowed-chars) t)
34           (t nil))))
35 
36 (defun unicode-space-p (char)
37   "Determine if CHAR is a kind of whitespace by unicode category means."
38   (char= (char (cl-unicode:general-category char) 0) #\Z))
39 
40 (defun slugify (string)
41   "Return a version of STRING suitable for use as a URL."
42   (let ((slugified (remove-if-not #'slug-char-p
43                                   (substitute-if #\- #'unicode-space-p string))))
44 	  (if (zerop (length slugified))
45         (error "Post title '~a' does not contain characters suitable for a slug!" string )
46         slugified)))
47 
48 ;; Content Types
49 
50 (defclass content ()
51   ((url  :initarg :url  :reader page-url)
52    (date :initarg :date :reader content-date)
53    (file :initarg :file :reader content-file)
54    (tags :initarg :tags :reader content-tags)
55    (text :initarg :text :reader content-text))
56   (:default-initargs :tags nil :date nil))
57 
58 (defmethod initialize-instance :after ((object content) &key)
59   (with-slots (tags) object
60     (when (stringp tags)
61       (setf tags (mapcar #'make-tag (cl-ppcre:split "," tags))))))
62 
63 (defun parse-initarg (line)
64   "Given a metadata header, LINE, parse an initarg name/value pair from it."
65   (let ((name (string-upcase (subseq line 0 (position #\: line))))
66         (match (nth-value 1 (scan-to-strings "[a-zA-Z]+:\\s+(.*)" line))))
67     (when match
68       (list (make-keyword name) (aref match 0)))))
69 
70 (defun parse-metadata (stream)
71   "Given a STREAM, parse metadata from it or signal an appropriate condition."
72   (flet ((get-next-line (input)
73            (string-trim '(#\Space #\Return #\Newline #\Tab) (read-line input nil))))
74     (unless (string= (get-next-line stream) (separator *config*))
75       (error "The file, ~a, lacks the expected header: ~a" (file-namestring stream) (separator *config*)))
76     (loop for line = (get-next-line stream)
77        until (string= line (separator *config*))
78        appending (parse-initarg line))))
79 
80 (defun read-content (file)
81   "Returns a plist of metadata from FILE with :text holding the content."
82   (flet ((slurp-remainder (stream)
83            (let ((seq (make-string (- (file-length stream)
84                                       (file-position stream)))))
85              (read-sequence seq stream)
86              (remove #\Nul seq))))
87     (with-open-file (in file :external-format :utf-8)
88       (let ((metadata (parse-metadata in))
89             (content (slurp-remainder in))
90             (filepath (enough-namestring file (repo-dir *config*))))
91         (append metadata (list :text content :file filepath))))))
92 
93 ;; Helper Functions
94 
95 (defun tag-p (tag obj)
96   "Test if OBJ is tagged with TAG."
97   (let ((tag (if (typep tag 'tag) tag (make-tag tag))))
98     (member tag (content-tags obj) :test #'tag-slug=)))
99 
100 (defun month-p (month obj)
101   "Test if OBJ was written in MONTH."
102   (search month (content-date obj)))
103 
104 (defun by-date (content)
105   "Sort CONTENT in reverse chronological order."
106   (sort content #'string> :key #'content-date))
107 
108 (defun find-content-by-path (path)
109   "Find the CONTENT corresponding to the file at PATH."
110   (find path (find-all 'content) :key #'content-file :test #'string=))
111 
112 (defgeneric render-text (text format)
113   (:documentation "Render TEXT of the given FORMAT to HTML for display.")
114   (:method (text (format (eql :html)))
115     text)
116   (:method (text (format (eql :md)))
117     (let ((3bmd-code-blocks:*code-blocks* t))
118       (with-output-to-string (str)
119         (3bmd:parse-string-and-print-to-stream text str)))))