Flexible Lisp Blogware. Fork for personal use. Mirrored from https://github.com/kingcons/coleslaw originally.

content.lisp 3.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. (in-package :coleslaw)
  2. ;; Tagging
  3. (defclass tag ()
  4. ((name :initform nil :initarg :name :accessor tag-name)
  5. (slug :initform nil :Initarg :slug :accessor tag-slug)))
  6. (defun make-tag (str)
  7. "Takes a string and returns a TAG instance with a name and slug."
  8. (let ((trimmed (string-trim " " str)))
  9. (make-instance 'tag :name trimmed :slug (slugify trimmed))))
  10. (defun tag-slug= (a b)
  11. "Test if the slugs for tag A and B are equal."
  12. (string= (tag-slug a) (tag-slug b)))
  13. ;; Slugs
  14. (defun slug-char-p (char)
  15. "Determine if CHAR is a valid slug (i.e. URL) character."
  16. (or (char<= #\0 char #\9)
  17. (char<= #\a char #\z)
  18. (char<= #\A char #\Z)
  19. (member char '(#\_ #\-))))
  20. (defun slugify (string)
  21. "Return a version of STRING suitable for use as a URL."
  22. (remove-if-not #'slug-char-p (substitute #\- #\Space string)))
  23. ;; Content Types
  24. (defclass content ()
  25. ((file :initform nil :initarg :file :accessor content-file)
  26. (tags :initform nil :initarg :tags :accessor content-tags)
  27. (slug :initform nil :initarg :slug :accessor content-slug)
  28. (date :initform nil :initarg :date :accessor content-date)
  29. (text :initform nil :initarg :text :accessor content-text)))
  30. (defmethod initialize-instance :after ((object content) &key)
  31. (with-accessors ((tags content-tags)) object
  32. (when (stringp tags)
  33. (setf tags (mapcar #'make-tag (cl-ppcre:split "," tags))))))
  34. (defun read-content (file)
  35. "Returns a plist of metadata from FILE with :text holding the content as a string."
  36. (flet ((slurp-remainder (stream)
  37. (let ((seq (make-string (- (file-length stream)
  38. (file-position stream)))))
  39. (read-sequence seq stream)
  40. (remove #\Nul seq)))
  41. (parse-field (str)
  42. (nth-value 1 (cl-ppcre:scan-to-strings "[a-zA-Z]+:\\s+(.*)" str)))
  43. (field-name (line)
  44. (make-keyword (string-upcase (subseq line 0 (position #\: line))))))
  45. (with-open-file (in file :external-format '(:utf-8))
  46. (unless (string= (read-line in) (separator *config*))
  47. (error "The provided file lacks the expected header."))
  48. (let ((meta (loop for line = (read-line in nil)
  49. until (string= line (separator *config*))
  50. appending (list (field-name line)
  51. (aref (parse-field line) 0))))
  52. (filepath (enough-namestring file (repo *config*)))
  53. (content (slurp-remainder in)))
  54. (append meta (list :text content :file filepath))))))
  55. ;; Helper Functions
  56. (defun tag-p (tag obj)
  57. "Test if OBJ is tagged with TAG."
  58. (let ((tag (if (typep tag 'tag) tag (make-tag tag))))
  59. (member tag (content-tags obj) :test #'tag-slug=)))
  60. (defun month-p (month obj)
  61. "Test if OBJ was written in MONTH."
  62. (search month (content-date obj)))
  63. (defun by-date (content)
  64. "Sort CONTENT in reverse chronological order."
  65. (sort content #'string> :key #'content-date))
  66. (defgeneric render-text (text format)
  67. (:documentation "Render TEXT of the given FORMAT to HTML for display.")
  68. (:method (text (format (eql :html)))
  69. text)
  70. (:method (text (format (eql :md)))
  71. (let ((3bmd-code-blocks:*code-blocks* t))
  72. (with-output-to-string (str)
  73. (3bmd:parse-string-and-print-to-stream text str)))))