|
|
generator.lisp - cl-yag - Common Lisp Yet Another website Generator |
|
|
 |
git clone git://bitreich.org/cl-yag/ git://enlrupgkhuxnvlhsf6lc3fziv5h2hhfrinws65d7roiv6bfj7d652fid.onion/cl-yag/ (git://bitreich.org) |
|
|
 |
Log |
|
|
 |
Files |
|
|
 |
Refs |
|
|
 |
Tags |
|
|
 |
README |
|
|
 |
LICENSE |
|
|
|
--- |
|
|
|
generator.lisp (20812B) |
|
|
|
--- |
|
|
|
1 ;;;; GLOBAL VARIABLES |
|
|
|
2 |
|
|
|
3 (defparameter *articles* '()) |
|
|
|
4 (defparameter *converters* '()) |
|
|
|
5 (defparameter *days* '("Monday" "Tuesday" "Wednesday" "Thursday" |
|
|
|
6 "Friday" "Saturday" "Sunday")) |
|
|
|
7 (defparameter *months* '("January" "February" "March" "April" |
|
|
|
8 "May" "June" "July" "August" "September" |
|
|
|
9 "October" "November" "December")) |
|
|
|
10 |
|
|
|
11 ;; structure to store links |
|
|
|
12 (defstruct article title tag date id tiny author rawdate converter) |
|
|
|
13 (defstruct converter name command extension) |
|
|
|
14 |
|
|
|
15 ;;;; FUNCTIONS |
|
|
|
16 |
|
|
|
17 (require 'asdf) |
|
|
|
18 |
|
|
|
19 ;; return the day of the week |
|
|
|
20 (defun get-day-of-week(day month year) |
|
|
|
21 (multiple-value-bind |
|
|
|
22 (second minute hour date month year day-of-week dst-p tz) |
|
|
|
23 (decode-universal-time (encode-universal-time 0 0 0 day month year)) |
|
|
|
24 (declare (ignore second minute hour date month year dst-p tz)) |
|
|
|
25 day-of-week)) |
|
|
|
26 |
|
|
|
27 ;; parse the date to |
|
|
|
28 (defun date-parse(date) |
|
|
|
29 (if (= 8 (length date)) |
|
|
|
30 (let* ((year (parse-integer date :start 0 :end 4)) |
|
|
|
31 (monthnum (parse-integer date :start 4 :end 6)) |
|
|
|
32 (daynum (parse-integer date :start 6 :end 8)) |
|
|
|
33 (day (nth (get-day-of-week daynum monthnum year) *days*)) |
|
|
|
34 (month (nth (- monthnum 1) *months*))) |
|
|
|
35 (list |
|
|
|
36 :dayname day |
|
|
|
37 :daynumber daynum |
|
|
|
38 :monthname month |
|
|
|
39 :monthnumber monthnum |
|
|
|
40 :year year)) |
|
|
|
41 nil)) |
|
|
|
42 |
|
|
|
43 (defun post(&optional &key title tag date id (tiny nil) (author (getf *config* :webmaster)) (converter nil)) |
|
|
|
44 (push (make-article :title title |
|
|
|
45 :tag tag |
|
|
|
46 :date (date-parse date) |
|
|
|
47 :rawdate date |
|
|
|
48 :tiny tiny |
|
|
|
49 :author author |
|
|
|
50 :id id |
|
|
|
51 :converter converter) |
|
|
|
52 *articles*)) |
|
|
|
53 |
|
|
|
54 ;; we add a converter to the list of the one availables |
|
|
|
55 (defun converter(&optional &key name command extension) |
|
|
|
56 (setf *converters* |
|
|
|
57 (append |
|
|
|
58 (list name |
|
|
|
59 (make-converter :name name |
|
|
|
60 :command command |
|
|
|
61 :extension extension)) |
|
|
|
62 *converters*))) |
|
|
|
63 |
|
|
|
64 ;; load data from metadata and load config |
|
|
|
65 (load "data/articles.lisp") |
|
|
|
66 (setf *articles* (reverse *articles*)) |
|
|
|
67 |
|
|
|
68 |
|
|
|
69 ;; common-lisp don't have a replace string function natively |
|
|
|
70 (defun replace-all (string part replacement &key (test #'char=)) |
|
|
|
71 (with-output-to-string (out) |
|
|
|
72 (loop with part-length = (length part) |
|
|
|
73 for old-pos = 0 then (+ pos part-length) |
|
|
|
74 for pos = (search part string |
|
|
|
75 :start2 old-pos |
|
|
|
76 :test test) |
|
|
|
77 do (write-string string out |
|
|
|
78 :start old-pos |
|
|
|
79 :end (or pos (length string))) |
|
|
|
80 when pos do (write-string replacement out) |
|
|
|
81 while pos))) |
|
|
|
82 |
|
|
|
83 ;; common-lisp don't have a split string function natively |
|
|
|
84 (defun split-str(text &optional (separator #\Space)) |
|
|
|
85 "this function split a string with separator and return a list" |
|
|
|
86 (let ((text (concatenate 'string text (string separator)))) |
|
|
|
87 (loop for char across text |
|
|
|
88 counting char into count |
|
|
|
89 when (char= char separator) |
|
|
|
90 collect |
|
|
|
91 ;; we look at the position of the left separator from right to left |
|
|
|
92 (let ((left-separator-position (position separator text :from-end t :end (- count 1)))) |
|
|
|
93 (subseq text |
|
|
|
94 ;; if we can't find a separator at the left of the current, then it's the start of |
|
|
|
95 ;; the string |
|
|
|
96 (if left-separator-position (+ 1 left-separator-position) 0) |
|
|
|
97 (- count 1)))))) |
|
|
|
98 |
|
|
|
99 ;; load a file as a string |
|
|
|
100 ;; we escape ~ to avoid failures with format |
|
|
|
101 (defun load-file(path) |
|
|
|
102 (if (probe-file path) |
|
|
|
103 (with-open-file (stream path) |
|
|
|
104 (let ((contents (make-string (file-length stream)))) |
|
|
|
105 (read-sequence contents stream) |
|
|
|
106 contents)) |
|
|
|
107 (progn |
|
|
|
108 (format t "ERROR : file ~a not found. Aborting~%" path) |
|
|
|
109 (quit)))) |
|
|
|
110 |
|
|
|
111 ;; save a string in a file |
|
|
|
112 (defun save-file(path data) |
|
|
|
113 (with-open-file (stream path :direction :output :if-exists :supersede) |
|
|
|
114 (write-sequence data stream))) |
|
|
|
115 |
|
|
|
116 ;; simplify the str replace work |
|
|
|
117 (defmacro template(before &body after) |
|
|
|
118 `(progn |
|
|
|
119 (setf output (replace-all output ,before ,@after)))) |
|
|
|
120 |
|
|
|
121 ;; get the converter object of "article" |
|
|
|
122 (defmacro with-converter(&body code) |
|
|
|
123 `(progn |
|
|
|
124 (let ((converter-name (if (article-converter article) |
|
|
|
125 (article-converter article) |
|
|
|
126 (getf *config* :default-converter)))) |
|
|
|
127 (let ((converter-object (getf *converters* converter-name))) |
|
|
|
128 ,@code)))) |
|
|
|
129 |
|
|
|
130 ;; generate the html file from the source file |
|
|
|
131 ;; using the converter associated with the post |
|
|
|
132 (defun use-converter-to-html(filename &optional (converter-name nil)) |
|
|
|
133 (let* ((converter-object (getf *converters* |
|
|
|
134 (or converter-name |
|
|
|
135 converter-name |
|
|
|
136 (getf *config* :default-converter)))) |
|
|
|
137 (output (converter-command converter-object)) |
|
|
|
138 (src-file (format nil "~a~a" filename (converter-extension converter-object))) |
|
|
|
139 (dst-file (format nil "temp/data/~a.html" filename )) |
|
|
|
140 (full-src-file (format nil "data/~a" src-file))) |
|
|
|
141 ;; skip generating if the destination exists |
|
|
|
142 ;; and is more recent than source |
|
|
|
143 (unless (and |
|
|
|
144 (probe-file dst-file) |
|
|
|
145 (>= |
|
|
|
146 (file-write-date dst-file) |
|
|
|
147 (file-write-date full-src-file))) |
|
|
|
148 (ensure-directories-exist "temp/data/") |
|
|
|
149 (template "%IN" src-file) |
|
|
|
150 (template "%OUT" dst-file) |
|
|
|
151 (format t "~a~%" output) |
|
|
|
152 (uiop:run-program output)))) |
|
|
|
153 |
|
|
|
154 ;; format the date |
|
|
|
155 (defun date-format(format date) |
|
|
|
156 (let ((output format)) |
|
|
|
157 (template "%DayName" (getf date :dayname)) |
|
|
|
158 (template "%DayNumber" (format nil "~2,'0d" (getf date :daynumber))) |
|
|
|
159 (template "%MonthName" (getf date :monthname)) |
|
|
|
160 (template "%MonthNumber" (format nil "~2,'0d" (getf date :monthnumber))) |
|
|
|
161 (template "%Year" (write-to-string (getf date :year ))) |
|
|
|
162 output)) |
|
|
|
163 |
|
|
|
164 ;; simplify the declaration of a new page type |
|
|
|
165 (defmacro prepare(template &body code) |
|
|
|
166 `(progn |
|
|
|
167 (let ((output (load-file ,template))) |
|
|
|
168 ,@code |
|
|
|
169 output))) |
|
|
|
170 |
|
|
|
171 ;; simplify the file saving by using the layout |
|
|
|
172 (defmacro generate(name &body data) |
|
|
|
173 `(progn |
|
|
|
174 (save-file ,name (generate-layout ,@data)))) |
|
|
|
175 |
|
|
|
176 ;; generate a gemini index file |
|
|
|
177 (defun generate-gemini-index(articles) |
|
|
|
178 (let ((output (load-file "templates/gemini_head.tpl"))) |
|
|
|
179 (dolist (article articles) |
|
|
|
180 (setf output |
|
|
|
181 (string |
|
|
|
182 (concatenate 'string output |
|
|
|
183 (format nil "=> ~a/articles/~a.gmi ~a-~2,'0d-~2,'0d ~a~%" |
|
|
|
184 (getf *config* :gemini-path) |
|
|
|
185 (article-id article) |
|
|
|
186 (getf (article-date article) :year) |
|
|
|
187 (getf (article-date article) :monthnumber) |
|
|
|
188 (getf (article-date article) :daynumber) |
|
|
|
189 (article-title article)))))) |
|
|
|
190 output)) |
|
|
|
191 |
|
|
|
192 ;; generate a gopher index file |
|
|
|
193 (defun generate-gopher-index(articles) |
|
|
|
194 (let ((output (load-file "templates/gopher_head.tpl"))) |
|
|
|
195 (dolist (article articles) |
|
|
|
196 (setf output |
|
|
|
197 (string |
|
|
|
198 (concatenate 'string output |
|
|
|
199 (format nil (getf *config* :gopher-format) |
|
|
|
200 0 ;;;; gopher type, 0 for text files |
|
|
|
201 ;; here we create a 80 width char string with title on the left |
|
|
|
202 ;; and date on the right |
|
|
|
203 ;; we truncate the article title if it's too large |
|
|
|
204 (let ((title (format nil "~80a" |
|
|
|
205 (if (< 80 (length (article-title article))) |
|
|
|
206 (subseq (article-title article) 0 80) |
|
|
|
207 (article-title article))))) |
|
|
|
208 (replace title (article-rawdate article) :start1 (- (length title) (length (article-rawdate article))))) |
|
|
|
209 (concatenate 'string |
|
|
|
210 (getf *config* :gopher-path) "/article-" (article-id article) ".txt") |
|
|
|
211 (getf *config* :gopher-server) |
|
|
|
212 (getf *config* :gopher-port) |
|
|
|
213 ))))) |
|
|
|
214 output)) |
|
|
|
215 |
|
|
|
216 ;; generate the list of tags |
|
|
|
217 (defun articles-by-tag() |
|
|
|
218 (let ((tag-list)) |
|
|
|
219 (loop for article in *articles* do |
|
|
|
220 (when (article-tag article) ;; we don't want an error if no tag |
|
|
|
221 (loop for tag in (split-str (article-tag article)) do ;; for each word in tag keyword |
|
|
|
222 (setf (getf tag-list (intern tag "KEYWORD")) ;; we create the keyword is inexistent and add ID to :value |
|
|
|
223 (list |
|
|
|
224 :name tag |
|
|
|
225 :value (push (article-id article) (getf (getf tag-list (intern tag "KEYWORD")) :value))))))) |
|
|
|
226 (loop for i from 1 to (length tag-list) by 2 collect ;; removing the keywords |
|
|
|
227 (nth i tag-list)))) |
|
|
|
228 |
|
|
|
229 ;; generates the html of the list of tags for an article |
|
|
|
230 (defun get-tag-list-article(&optional article) |
|
|
|
231 (apply #'concatenate 'string |
|
|
|
232 (mapcar #'(lambda (item) |
|
|
|
233 (prepare "templates/one-tag.tpl" (template "%%Name%%" item))) |
|
|
|
234 (split-str (article-tag article))))) |
|
|
|
235 |
|
|
|
236 ;; generates the html of the whole list of tags |
|
|
|
237 (defun get-tag-list() |
|
|
|
238 (apply #'concatenate 'string |
|
|
|
239 (mapcar #'(lambda (item) |
|
|
|
240 (prepare "templates/one-tag.tpl" |
|
|
|
241 (template "%%Name%%" (getf item :name)))) |
|
|
|
242 (articles-by-tag)))) |
|
|
|
243 |
|
|
|
244 |
|
|
|
245 ;; generates the html of only one article |
|
|
|
246 ;; this is called in a loop to produce the homepage |
|
|
|
247 (defun create-article(article &optional &key (tiny t) (no-text nil)) |
|
|
|
248 (prepare "templates/article.tpl" |
|
|
|
249 (template "%%Author%%" (let ((author (article-author article))) |
|
|
|
250 (or author (getf *config* :webmaster)))) |
|
|
|
251 (template "%%Date%%" (date-format (getf *config* :date-format) |
|
|
|
252 (article-date article))) |
|
|
|
253 (template "%%Raw-Date%%" (article-rawdate article)) |
|
|
|
254 (template "%%Title%%" (article-title article)) |
|
|
|
255 (template "%%Id%%" (article-id article)) |
|
|
|
256 (template "%%Tags%%" (get-tag-list-article article)) |
|
|
|
257 (template "%%Date-Url%%" (date-format "%Year-%MonthNumber-%DayNumber" |
|
|
|
258 (article-date article))) |
|
|
|
259 (template "%%Text%%" (if no-text |
|
|
|
260 "" |
|
|
|
261 (if (and tiny (article-tiny article)) |
|
|
|
262 (format nil "<p>~a</p>" (article-tiny article)) |
|
|
|
263 (load-file (format nil "temp/data/~d.html" (article-id article)))))))) |
|
|
|
264 |
|
|
|
265 ;; return a html string |
|
|
|
266 ;; produce the code of a whole page with title+layout with the parameter as the content |
|
|
|
267 (defun generate-layout(body &optional &key (title nil)) |
|
|
|
268 (prepare "templates/layout.tpl" |
|
|
|
269 (template "%%Title%%" (if title title (getf *config* :title))) |
|
|
|
270 (template "%%Tags%%" (get-tag-list)) |
|
|
|
271 (template "%%Body%%" body) |
|
|
|
272 output)) |
|
|
|
273 |
|
|
|
274 |
|
|
|
275 ;; html generation of index homepage |
|
|
|
276 (defun generate-semi-mainpage(&key (tiny t) (no-text nil)) |
|
|
|
277 (apply #'concatenate 'string |
|
|
|
278 (loop for article in *articles* collect |
|
|
|
279 (create-article article :tiny tiny :no-text no-text)))) |
|
|
|
280 |
|
|
|
281 ;; html generation of a tag homepage |
|
|
|
282 (defun generate-tag-mainpage(articles-in-tag) |
|
|
|
283 (apply #'concatenate 'string |
|
|
|
284 (loop for article in *articles* |
|
|
|
285 when (member (article-id article) articles-in-tag :test #'equal) |
|
|
|
286 collect (create-article article :tiny t)))) |
|
|
|
287 |
|
|
|
288 ;; xml generation of the items for the rss |
|
|
|
289 (defun generate-rss-item(&key (gopher nil)) |
|
|
|
290 (apply #'concatenate 'string |
|
|
|
291 (loop for article in *articles* |
|
|
|
292 for i from 1 to (min (length *articles*) (getf *config* :rss-item-number)) |
|
|
|
293 collect |
|
|
|
294 (prepare "templates/rss-item.tpl" |
|
|
|
295 (template "%%Title%%" (article-title article)) |
|
|
|
296 (template "%%Description%%" (load-file (format nil "temp/data/~d.html" (article-id article)))) |
|
|
|
297 (template "%%Date%%" (format nil |
|
|
|
298 (date-format "~a, %DayNumber ~a %Year 00:00:00 GMT" |
|
|
|
299 (article-date article)) |
|
|
|
300 (subseq (getf (article-date article) :dayname) 0 3) |
|
|
|
301 (subseq (getf (article-date article) :monthname) 0 3))) |
|
|
|
302 (template "%%Url%%" |
|
|
|
303 (if gopher |
|
|
|
304 (format nil "gopher://~a:~d/0~a/article-~a.txt" |
|
|
|
305 (getf *config* :gopher-server) |
|
|
|
306 (getf *config* :gopher-port) |
|
|
|
307 (getf *config* :gopher-path) |
|
|
|
308 (article-id article)) |
|
|
|
309 (format nil "~d~d-~d.html" |
|
|
|
310 (getf *config* :url) |
|
|
|
311 (date-format "%Year-%MonthNumber-%DayNumber" |
|
|
|
312 (article-date article)) |
|
|
|
313 (article-id article)))))))) |
|
|
|
314 |
|
|
|
315 |
|
|
|
316 ;; Generate the rss xml data |
|
|
|
317 (defun generate-rss(&key (gopher nil)) |
|
|
|
318 (prepare "templates/rss.tpl" |
|
|
|
319 (template "%%Description%%" (getf *config* :description)) |
|
|
|
320 (template "%%Title%%" (getf *config* :title)) |
|
|
|
321 (template "%%Url%%" (getf *config* :url)) |
|
|
|
322 (template "%%Items%%" (generate-rss-item :gopher gopher)))) |
|
|
|
323 |
|
|
|
324 ;; We do all the website |
|
|
|
325 (defun create-html-site() |
|
|
|
326 |
|
|
|
327 ;; produce each article file |
|
|
|
328 (loop for article in *articles* |
|
|
|
329 do |
|
|
|
330 ;; use the article's converter to get html code of it |
|
|
|
331 (use-converter-to-html (article-id article) (article-converter article)) |
|
|
|
332 |
|
|
|
333 (generate (format nil "output/html/~d-~d.html" |
|
|
|
334 (date-format "%Year-%MonthNumber-%DayNumber" |
|
|
|
335 (article-date article)) |
|
|
|
336 (article-id article)) |
|
|
|
337 (create-article article :tiny nil) |
|
|
|
338 :title (concatenate 'string (getf *config* :title) " : " (article-title article)))) |
|
|
|
339 |
|
|
|
340 ;; produce index.html |
|
|
|
341 (generate "output/html/index.html" (generate-semi-mainpage)) |
|
|
|
342 |
|
|
|
343 ;; produce index-titles.html where there are only articles titles |
|
|
|
344 (generate "output/html/index-titles.html" (generate-semi-mainpage :no-text t)) |
|
|
|
345 |
|
|
|
346 ;; produce index file for each tag |
|
|
|
347 (loop for tag in (articles-by-tag) do |
|
|
|
348 (generate (format nil "output/html/tag-~d.html" (getf tag :NAME)) |
|
|
|
349 (generate-tag-mainpage (getf tag :VALUE)))) |
|
|
|
350 |
|
|
|
351 ;; generate rss gopher in html folder if gopher is t |
|
|
|
352 (when (getf *config* :gopher) |
|
|
|
353 (save-file "output/html/rss-gopher.xml" (generate-rss :gopher t))) |
|
|
|
354 |
|
|
|
355 ;;(generate-file-rss) |
|
|
|
356 (save-file "output/html/rss.xml" (generate-rss))) |
|
|
|
357 |
|
|
|
358 ;; we do all the gemini capsule |
|
|
|
359 (defun create-gemini-capsule() |
|
|
|
360 |
|
|
|
361 ;; produce the index.gmi file |
|
|
|
362 (save-file (concatenate 'string "output/gemini/" (getf *config* :gemini-index)) |
|
|
|
363 (generate-gemini-index *articles*)) |
|
|
|
364 |
|
|
|
365 ;; produce a tag list menu |
|
|
|
366 (let* ((directory-path "output/gemini/_tags_/") |
|
|
|
367 (index-path (concatenate 'string directory-path (getf *config* :gemini-index)))) |
|
|
|
368 (ensure-directories-exist directory-path) |
|
|
|
369 (save-file index-path |
|
|
|
370 (let ((output (load-file "templates/gemini_head.tpl"))) |
|
|
|
371 (loop for tag in |
|
|
|
372 ;; sort tags per articles in it |
|
|
|
373 (sort (articles-by-tag) #'> |
|
|
|
374 :key #'(lambda (x) (length (getf x :value)))) |
|
|
|
375 do |
|
|
|
376 (setf output |
|
|
|
377 (string |
|
|
|
378 (concatenate |
|
|
|
379 'string output |
|
|
|
380 (format nil "=> ~a/~a/index.gmi ~a ~d~%" |
|
|
|
381 (getf *config* :gemini-path) |
|
|
|
382 (getf tag :name) |
|
|
|
383 (getf tag :name) |
|
|
|
384 (length (getf tag :value))))))) |
|
|
|
385 output))) |
|
|
|
386 |
|
|
|
387 ;; produce each tag gemini index |
|
|
|
388 (loop for tag in (articles-by-tag) do |
|
|
|
389 (let* ((directory-path (concatenate 'string "output/gemini/" (getf tag :NAME) "/")) |
|
|
|
390 (index-path (concatenate 'string directory-path (getf *config* :gemini-index))) |
|
|
|
391 (articles-with-tag (loop for article in *articles* |
|
|
|
392 when (member (article-id article) (getf tag :VALUE) :test #'equal) |
|
|
|
393 collect article))) |
|
|
|
394 (ensure-directories-exist directory-path) |
|
|
|
395 (save-file index-path (generate-gemini-index articles-with-tag)))) |
|
|
|
396 |
|
|
|
397 ;; produce each article file (adding some headers) |
|
|
|
398 (loop for article in *articles* |
|
|
|
399 do |
|
|
|
400 (with-converter |
|
|
|
401 (let ((id (article-id article))) |
|
|
|
402 (save-file (format nil "output/gemini/articles/~a.gmi" id) |
|
|
|
403 (format nil "~{~a~}" |
|
|
|
404 (list |
|
|
|
405 "Title : " (article-title article) #\Newline |
|
|
|
406 "Author: " (article-author article) #\Newline |
|
|
|
407 "Date : " (date-format (getf *config* :date-format) (article-date article)) #\Newline |
|
|
|
408 "Tags : " (article-tag article) #\Newline #\Newline |
|
|
|
409 (load-file (format nil "data/~d~d" id (converter-extension converter-object)))))))))) |
|
|
|
410 |
|
|
|
411 ;; we do all the gopher hole |
|
|
|
412 (defun create-gopher-hole() |
|
|
|
413 |
|
|
|
414 ;;(generate-file-rss) |
|
|
|
415 (save-file "output/gopher/rss.xml" (generate-rss :gopher t)) |
|
|
|
416 |
|
|
|
417 ;; produce the gophermap file |
|
|
|
418 (save-file (concatenate 'string "output/gopher/" (getf *config* :gopher-index)) |
|
|
|
419 (generate-gopher-index *articles*)) |
|
|
|
420 |
|
|
|
421 ;; produce a tag list menu |
|
|
|
422 (let* ((directory-path "output/gopher/_tags_/") |
|
|
|
423 (index-path (concatenate 'string directory-path (getf *config* :gopher-index)))) |
|
|
|
424 (ensure-directories-exist directory-path) |
|
|
|
425 (save-file index-path |
|
|
|
426 (let ((output (load-file "templates/gopher_head.tpl"))) |
|
|
|
427 (loop for tag in |
|
|
|
428 ;; sort tags per articles in it |
|
|
|
429 (sort (articles-by-tag) #'> |
|
|
|
430 :key #'(lambda (x) (length (getf x :value)))) |
|
|
|
431 do |
|
|
|
432 (setf output |
|
|
|
433 (string |
|
|
|
434 (concatenate |
|
|
|
435 'string output |
|
|
|
436 (format nil (getf *config* :gopher-format) |
|
|
|
437 1 ;; gopher type, 1 for menus |
|
|
|
438 ;; here we create a 72 width char string with title on the left |
|
|
|
439 ;; and number of articles on the right |
|
|
|
440 ;; we truncate the article title if it's too large |
|
|
|
441 (let ((title (format nil "~72a" |
|
|
|
442 (if (< 72 (length (getf tag :NAME))) |
|
|
|
443 (subseq (getf tag :NAME) 0 80) |
|
|
|
444 (getf tag :NAME)))) |
|
|
|
445 (article-number (format nil "~d article~p" (length (getf tag :value)) (length (getf tag :value))))) |
|
|
|
446 (replace title article-number :start1 (- (length title) (length article-number)))) |
|
|
|
447 (concatenate 'string |
|
|
|
448 (getf *config* :gopher-path) "/" (getf tag :NAME) "/") |
|
|
|
449 (getf *config* :gopher-server) |
|
|
|
450 (getf *config* :gopher-port) |
|
|
|
451 ))))) |
|
|
|
452 output))) |
|
|
|
453 |
|
|
|
454 ;; produce each tag gophermap index |
|
|
|
455 (loop for tag in (articles-by-tag) do |
|
|
|
456 (let* ((directory-path (concatenate 'string "output/gopher/" (getf tag :NAME) "/")) |
|
|
|
457 (index-path (concatenate 'string directory-path (getf *config* :gopher-index))) |
|
|
|
458 (articles-with-tag (loop for article in *articles* |
|
|
|
459 when (member (article-id article) (getf tag :VALUE) :test #'equal) |
|
|
|
460 collect article))) |
|
|
|
461 (ensure-directories-exist directory-path) |
|
|
|
462 (save-file index-path (generate-gopher-index articles-with-tag)))) |
|
|
|
463 |
|
|
|
464 ;; produce each article file (adding some headers) |
|
|
|
465 (loop for article in *articles* |
|
|
|
466 do |
|
|
|
467 (with-converter |
|
|
|
468 (let ((id (article-id article))) |
|
|
|
469 (save-file (format nil "output/gopher/article-~d.txt" id) |
|
|
|
470 (format nil "Title: ~a~%Author: ~a~%Date: ~a~%Tags: ~a~%============~%~%~a" |
|
|
|
471 (article-title article) |
|
|
|
472 (article-author article) |
|
|
|
473 (date-format (getf *config* :date-format) (article-date article)) |
|
|
|
474 (article-tag article) |
|
|
|
475 (load-file (format nil "data/~d~d" id (converter-extension converter-object))))))))) |
|
|
|
476 |
|
|
|
477 |
|
|
|
478 ;; This is function called when running the tool |
|
|
|
479 (defun generate-site() |
|
|
|
480 (if (getf *config* :html) |
|
|
|
481 (create-html-site)) |
|
|
|
482 (if (getf *config* :gemini) |
|
|
|
483 (create-gemini-capsule)) |
|
|
|
484 (if (getf *config* :gopher) |
|
|
|
485 (create-gopher-hole))) |
|
|
|
486 |
|
|
|
487 ;;;; EXECUTION |
|
|
|
488 |
|
|
|
489 (generate-site) |
|
|
|
490 |
|
|
|
491 (quit) |
|