;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; ;;;; Blogsym ;;;; ;;;; $Id: blogsym.lisp,v 1.1.1.1 2005/07/06 13:02:57 ola Exp $ ;;;; ;;;; http://ola.rinta-koski.net/blogsym/ ;;;; Copyright (c) 2004, 2005 Olli-Pekka Rinta-Koski ;;;; Permission is hereby granted, free of charge, to any person ;;;; obtaining a copy of this software and associated documentation ;;;; files (the "Software"), to deal in the Software without ;;;; restriction, including without limitation the rights to use, ;;;; copy, modify, merge, publish, distribute, sublicense, and/or sell ;;;; copies of the Software, and to permit persons to whom the ;;;; Software is furnished to do so, subject to the following ;;;; conditions: ;;;; The above copyright notice and this permission notice shall be ;;;; included in all copies or substantial portions of the Software. ;;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ;;;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES ;;;; OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ;;;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT ;;;; HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, ;;;; WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING ;;;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR ;;;; OTHER DEALINGS IN THE SOFTWARE. ;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defvar *blogsym-name* "Blogsym") (defvar *blogsym-version* "1.0") (defvar *blog* nil) (defun string+ (&rest strings) (apply #'concatenate 'string (mapcan #'(lambda (x) (typecase x (list x) (t (list x)))) strings))) (defun htmlify (string) (let* ((string-size (length string)) (temp (make-string string-size)) (i 0) s) (flet ((add-char (char) (push (subseq temp 0 i) s) (push char s) (setf temp (make-string string-size) i 0))) (loop for c across string do (case c (#\& (add-char "&")) (#\< (add-char "<")) (#\> (add-char ">")) (t (setf (elt temp i) c) (incf i))) finally (progn (push (subseq temp 0 i) s) (return (string+ (nreverse s)))))))) (defun sanitize (string &optional (quote #\")) "Modify STRING so that the Lisp reader won't choke on it. Specifically, transform a date string, eg. \"2005-02-24\" into a list of three elements, (2005 2 24), and change all colons into periods (otherwise the reader will think it's a package reference)." (cond ((= (length string) 0) "") ;; ((and (not (eql (elt string 0) quote)) (= (count #\- string) 2)) (split string #\-)) ;; (t (substitute #\. #\: string)))) (defun split (str &optional (ch #\,) (quote #\")) "Converts STR into a list of strings, splitting it at every CH. However, substrings limited by a pair of QUOTE characters won't be split even if they contain an instance of CH." (let ((len (length str))) (when (> len 0) (loop for prev-pos = 0 then (1+ next-pos) for next-pos = (cond ((>= prev-pos len) nil) ;; ((eql (elt str prev-pos) quote) (position ch str :start (1+ (position quote str :start (1+ prev-pos))))) ;; (t (position ch str :start prev-pos))) for result = (list (sanitize (subseq str 0 next-pos))) then (cons (sanitize (subseq str prev-pos next-pos)) result) while next-pos finally (return (nreverse result)))))) (defun read-from-strings-ignoring-eof (str) (etypecase str (string (read-from-string str nil)) (list (mapcar #'(lambda (s) (read-from-string s nil)) str)))) (defvar *regions* '((au "Australia" ((ns "New South Wales" "NSW") (ql "Queensland" "QLD") (sa "South Australia") (ts "Tasmania" "TAS") (vi "Victoria" "VIC") (wa "Western Australia") (ct "Australian Capital Territory" "ACT") (nt "Northern Territory"))) (fi "Finland") (us "United States" (;; Feel free to fill in the missing states! (ca "California") (nj "New Jersey") (ny "New York"))) ;; Feel free to fill in the missing countries! ) "Country and region codes (ISO3166-2) and names") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defun weather-read-line (line) "Converts LINE, assumed to be a string in the format in which the Australian Bureau of Meteorology releases daily weather data, into a list of the data converted into a Lisp-readable format." ;; ",2005-02-22,23.8,,0,7.4,,,,,27.9,56,7,NNW,6,1017.6,29.7,52,1,NE,13,1014.0" ;; (NIL (2005 02 22) 23.8 NIL 0 7.4 NIL NIL NIL NIL 27.9 56 7 NNW 6 ;; 1017.6 29.7 52 1 NE 13 1014.0) (let ((split-line (split line))) (when split-line (mapcar #'read-from-strings-ignoring-eof (mapcar #'sanitize split-line))))) (defun read-weather (file-name) "Reads weather data from FILE-NAME." (when (probe-file file-name) (format t "~&Reading weather file ~A..." file-name) (with-open-file (f file-name :direction :input) (loop for line = (read-line f nil) while line collect (weather-read-line line))))) (defun find-weather (weather year month day) "Returns the weather data specified by YEAR, MONTH and DAY by searching through the daily entries contained in WEATHER." (find-if #'(lambda (x) (and (listp x) (>= (length x) 2) (listp (second x)) (eql (first (second x)) year) (eql (second (second x)) month) (eql (third (second x)) day))) weather)) (defun format-weather (stream day-weather &optional language) (when day-weather (destructuring-bind (dummy (year month day) minimum-temperature maximum-temperature rainfall evaporation sunshine maximum-wind-gust-direction maximum-wind-gust-speed maximum-wind-gust-time temperature-9am relative-humidity-9am cloud-amount-9am wind-direction-9am wind-speed-9am msl-pressure-9am temperature-3pm relative-humidity-3pm cloud-amount-3pm wind-direction-3pm wind-speed-3pm msl-pressure-3pm) day-weather (declare (ignore dummy year month day evaporation maximum-wind-gust-direction maximum-wind-gust-speed maximum-wind-gust-time cloud-amount-9am wind-direction-9am wind-speed-9am msl-pressure-9am cloud-amount-3pm wind-direction-3pm wind-speed-3pm msl-pressure-3pm)) (format stream (if (eql language 'finnish) "9:00 ~D°C ~@[(ilmankosteus ~D%)~], ~:[~*~;15:00 ~:*~D°C ~@[(~D%)~],~] ~@[~D~]...~@[~D~]°C~@[, ~D tuntia auringonpaistetta~]~:[~*~;, ~D mm sadetta~]" "9AM ~D°C ~@[(relative humidity ~D%)~], ~:[~*~;3PM ~:*~D°C ~@[(~D%)~],~] ~@[~D~]...~@[~D~]°C~@[, ~D hours of sunshine~]~:[~*~;, ~D mm of rain~]") temperature-9am relative-humidity-9am temperature-3pm relative-humidity-3pm minimum-temperature maximum-temperature sunshine (and rainfall (> rainfall 0.0)) rainfall)))) (defvar *locations* ;; For daily weather ;; See eg. ;; http://www.bom.gov.au/climate/dwo/200502/html/IDCJDW2124.200502.shtml ;; and http://www.bom.gov.au/climate/dwo/200502/text/IDCJDW4019.200502.csv '((brisbane . 4019) (byron-bay . 2022) (nimbin . 2022) ; same as Byron Bay (sydney . 2124) (adelaide . 5002) (hawker . 5020) (coolangatta . 4036) (surfers-paradise . 4036) ; same as Coolangatta (maroochydore . 4081) (noosa . 4081) ; same as Maroochydore (kingscote . 5026)) "Australian Bureau of Meteorology location codes") (defvar *weathers* (make-hash-table :test #'equal)) (defun get-weather (location year month day directory) (let ((location-id (cdr (assoc location *locations*)))) (when location-id (unless *weathers* (setq *weathers* (make-hash-table :test #'equal))) (let ((weather (gethash (list location year month) *weathers*))) (unless weather (setq weather (read-weather (format nil "~A/IDCJDW~D.~4,'0D~2,'0D.csv" directory location-id year month))) (setf (gethash (list location year month) *weathers*) weather)) (find-weather weather year month day))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defstruct geo-location latitude longitude name state country) (defun geo-position (geo-location) (format nil "~D;~D" (geo-location-latitude geo-location) (geo-location-longitude geo-location))) (defun icbm (geo-location) (format nil "~D, ~D" (geo-location-latitude geo-location) (geo-location-longitude geo-location))) (defun state (geo-location) (let ((country (assoc (geo-location-country geo-location) *regions*))) (when country (let ((state (assoc (geo-location-state geo-location) (third country)))) (when state (or (third state) (string (first state)))))))) (defun geo-placename (geo-location) (format nil "~A~@[, ~A~]" (geo-location-name geo-location) (state geo-location))) (defun geo-region (geo-location) (format nil "~A~@[-~A~]" (geo-location-country geo-location) (geo-location-state geo-location))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defvar *start-year* 2004) (defvar *max-year* 2005) (let ((languages '((english eng (en au gb nz us) "In English" ("Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday" "Sunday") ("January" "February" "March" "April" "May" "June" "July" "August" "September" "October" "November" "December")) (finnish fin fi "Suomeksi" ("Maanantai" "Tiistai" "Keskiviikko" "Torstai" "Perjantai" "Lauantai" "Sunnuntai") ("Tammikuu" "Helmikuu" "Maaliskuu" "Huhtikuu" "Toukokuu" "Kesäkuu" "Heinäkuu" "Elokuu" "Syyskuu" "Lokakuu" "Marraskuu" "Joulukuu")) (swedish swe sv "På svenska" ("Måndag" "Tisdag" "Onsdag" "Torsdag" "Fredag" "Lördag" "Söndag") ("januari" "februari" "mars" "april" "maj" "juni" "juli" "augusti" "september" "oktober" "november" "december")) (francais fra fr "En français" ("Lundi" "Mardi" "Mercredi" "Jeudi" "Vendredi" "Samedi" "Dimanche") ("Janvier" "Février" "Mars" "Avril" "Mai" "Juin" "Juillet" "Août" "Septembre" "Octobre" "Novembre" "Décembre") )))) (defun position-language (language) (position language languages :key #'car)) (defun language-cleartext (language) (fourth (assoc language languages))) (defun language-iso639 (language &optional variant) (let ((lang (third (assoc language languages)))) (when lang (typecase lang (symbol lang) (list (if (position variant (rest lang)) (make-symbol (format nil "~A-~A" (first lang) variant)) (first lang))))))) (defun language-short-symbol (language) (second (assoc language languages))) (defun language-symbols () (mapcar #'first languages)) (defun language-short-symbols () (mapcar #'second languages)) (defun total-languages () (length languages)) (defun day-name (language day) (nth day (fifth (assoc language languages)))) (defun month-name (month language) (nth (1- month) (sixth (assoc language languages))))) (defun days-in-month (month year) (unless (and (>= month 1) (<= month 12)) (error "Invalid month ~A -- DAYS-IN-MONTH" month)) (if (and (= month 2) (= 0 (mod year 4)) (or (not (= 0 (mod year 100))) (= 0 (mod year 400)))) 29 (nth month '(nil 31 28 31 30 31 30 31 31 30 31 30 31)))) (defun rss-title (list pics pic-dir html-dir language) (values (or (nth (position-language language) list) (first list)) 'rss-title)) (defclass common () ((title :accessor title :initarg :title))) ;;;;;;;;;;;;;;;; ;;;; Class blog (defclass blog (common) ((start-year :accessor start-year :initarg :start-year :initform *start-year*) (end-year :accessor end-year :initarg :end-year :initform *max-year*) (timespan :accessor timespan) (end-entries :accessor end-entries) (contents :accessor contents) (months :accessor months) (languages :accessor languages :initarg :languages :initform '(english)) (language-variants :accessor language-variants :initarg :language-variants :initform nil) (default-language :reader default-language :initarg :default-language :initform nil) (base-url :accessor base-url :initarg :base-url :initform "http://localhost/blog/") (stylesheet :accessor stylesheet :initarg :stylesheet :initform nil) (arrows :accessor arrows :initarg :arrows :initform '((left . "bb.png") (right . "bf.png") (up . "bh.png"))) (timezone :accessor timezone :initarg :timezone :initform 0) (contact-address :accessor contact-address :initarg :contact-address :initform nil) (html-directory :accessor html-directory :initarg :html-directory :initform "/tmp/blog/") (pic-directory :accessor pic-directory :initarg :pic-directory :initform "pics/") (text :accessor text :initarg :text :initform nil) (original-text :accessor original-text :initarg :original-text :initform nil) (text-directory :accessor text-directory :initarg :text-directory :initform ".") (weather-directory :accessor weather-directory :initarg :weather-directory :initform nil) (extensions :accessor extensions :initarg :extensions :initform nil) (hooks :accessor hooks :initarg :hooks :initform nil) (geo-location :accessor geo-location :initarg :geo-location :initform nil) (rss-size :accessor rss-size :initarg :rss-size :initform 10) (last-page :initform nil) (index-suffix :reader index-suffix :initarg :index-suffix :initform ".index") (file-suffix :reader file-suffix :initarg :file-suffix :initform ".blog"))) (defmethod local-base ((blog blog)) (local-base (base-url blog))) (defmethod blog-host ((blog blog)) (if (eq 0 (search "http://" (base-url blog))) (subseq (base-url blog) 7 (position #\/ (base-url blog) :start 7)) nil)) (defmethod get-arrow ((blog blog) which) (let ((arrow (cdr (assoc which (arrows blog))))) (when arrow (string+ (local-base blog) arrow)))) (defmethod start-time ((blog blog)) (first (timespan blog))) (defmethod end-time ((blog blog)) (second (timespan blog))) (defmethod get-month ((blog blog) year month language) (aref (months blog) (- year (start-year blog)) (1- month) (position-language language))) (defmethod set-month ((blog blog) year month language value) (setf (aref (months blog) (- year (start-year blog)) (1- month) (position-language language)) value)) (defmethod years ((blog blog)) (1+ (- (end-year blog) (start-year blog)))) (defmethod add-extension ((blog blog) id extension-fn) (push (cons id extension-fn) (extensions blog))) (defmethod set-extension ((blog blog) id extension-fn) (error "Not implemented yet -- SET-EXTENSION")) (defmethod linkify ((blog blog) list pics language footnote-id &key synopsis) (let* ((img-format (string+ "")) (text (etypecase list ((or number string) (unless synopsis (format nil img-format nil list))) ;; (list (etypecase (first list) ((or number string) (let ((link-target (etypecase (first list) (number (page-name (local-base blog) (first list) (second list) (third list) (if (symbolp (fourth list)) (fourth list) language))) (string (first list)))) (link-text (or (nth (+ (etypecase (first list) (number (if (symbolp (fourth list)) 4 3)) (string 1)) (position-language language)) list) (car (last list))))) (if synopsis link-text (link-to link-target link-text)))) ;; (list (unless synopsis (format nil img-format (string+ (mapcar #'(lambda (x) (format nil "~A=\"~A\" " (first x) (second x))) (butlast list))) (car (last list))))) ;; (symbol (let ((extension (assoc (first list) (extensions blog)))) (if extension (unless synopsis (multiple-value-bind (extension-text extension-category) (funcall (cdr extension) blog (rest list) pics language) (when extension-category (setf footnote-id extension-category)) extension-text)) ;; Save footnote (prog1 (format nil "~A ~A
" footnote-id (substitute-tags blog (or (nth (+ 1 (position-language language)) list) (car (last list))) pics language)) (incf footnote-id)))))))))) (values text footnote-id))) ;;;; Built-in extensions (defmethod minipage ((blog blog) list pics language) (format nil "~&
~A
" (substitute-tags blog (or (nth (position-language language) list) (first list)) pics language))) (defmethod text-block ((blog blog) list pics language) (format nil "~&~{~A~}" (mapcar #'(lambda (x) (substitute-tags blog x pics language)) list))) (defmethod image ((blog blog) list pics language) (declare (ignore pics)) (link-to (format nil "~A~A.jpg" (string+ (local-base blog) (pic-directory blog)) (first list)) (or (nth (position-language language) (rest list)) (first (rest list))))) (defmethod cool-site ((blog blog) list pics language) (declare (ignore pics)) (let ((prologue '((english . "Site of the day") (finnish . "Päivän webbisivu")))) (values (format nil "~&~A: ~A" (cdr (assoc language prologue)) (link-to (first list) (or (nth (position-language language) (cdr list)) (second list)))) 'cool-site))) (defmethod soundtrack ((blog blog) list pics language) (destructuring-bind ((artist &optional album &rest other-albums) &optional url-text &rest rest) list (declare (ignore rest)) (flet ((sanitize-for-filename (str) (when (stringp str) (let ((s (loop for c in '(#\' #\, #\! #\? #\: #\;) with s = str do (setq s (delete c (string-downcase (substitute #\_ #\Space s)))) finally (return s)))) (loop for html-ampersand = (search "&" s) while html-ampersand do (setf s (string+ (subseq s 0 html-ampersand) "and" (subseq s (+ html-ampersand 5))))) (substitute #\e #\é s))))) (flet ((album-pic (artist album) (let* ((filename (format nil "~A~@[_-_~A~].jpg" (sanitize-for-filename artist) (sanitize-for-filename album))) (filepath (format nil "~A/kuvat/levyt/~A" (html-directory blog) filename)) (img (list (format nil "~A/levyt/~A" (string+ (local-base blog) (pic-directory blog)) filename) (format nil "~A~@[ - ~A~]" artist (when (stringp album) album)) "0" "LEFT" "soundtrack")) (link-text (format nil "~A~@[:
~{~A~^, ~}~]" artist (when album (cons album other-albums))))) (list (probe-file filepath) filename filepath img link-text)))) (let ((full (album-pic artist album)) (brief (album-pic artist nil)) (url (cond ((and (listp url-text) (eql (first url-text) 'amazon)) (format nil "http://www.amazon.com/exec/obidos/redirect?path=ASIN/~A&link_code=as2&camp=1789&tag=~A&creative=9325" (second url-text) (amazon-associate blog))) (t url-text)))) (format nil "
~@[~A ~]~ ~[Today's commute soundtrack~;Tänään työmatkan rattona~]:\
~A
" (cond ((first full) (if url (link-to url (apply #'img (fourth full))) (apply #'img (fourth full)))) ((first brief) (if url (link-to url (apply #'img (fourth brief))) (apply #'img (fourth brief)))) (t (format nil "" (third full)))) (position-language language) (if url (link-to url (fifth full)) (fifth full)))))))) ;;;; End of extensions (defmethod substitute-tags ((blog blog) text pics language &key synopsis) (let ((footnote-id 1) (trailer-hash (make-hash-table))) (loop for c below (length text) with b with buf = (make-string (length text) :initial-element #\Space) with bufp = 0 do (let ((char (elt text c))) (case char (#\@;; @0..@9, @A, @B etc. (case insensitive) (when (> bufp 0) (push (subseq buf 0 bufp) b) (setf bufp 0 buf (make-string (length text) :initial-element #\Space))) (incf c) (let* ((pic-id (elt text c)) (pic (nth (- (char-code pic-id) (if (alpha-char-p pic-id) 55 48)) pics))) (push (multiple-value-bind (text next-footnote-id) (linkify blog pic pics language footnote-id :synopsis synopsis) (cond ((and (numberp next-footnote-id) (not (= footnote-id next-footnote-id))) (prog1 (format nil "~D" footnote-id) (push text (gethash 'footnote trailer-hash)) (setf footnote-id next-footnote-id))) ((symbolp next-footnote-id) (push text (gethash next-footnote-id trailer-hash)) nil) (t text))) b))) (t (setf (elt buf bufp) char) (incf bufp)))) finally (when (> bufp 0) (push (subseq buf 0 bufp) b)) (loop for k being each hash-key of trailer-hash for v = (gethash k trailer-hash) do (push (format nil "
" k) b) (dolist (n (nreverse v)) (push n b)) (push "
" b)) (return-from substitute-tags (string+ (nreverse b)))))) (defmethod initialize-instance :after ((blog blog) &rest initargs) (declare (ignore initargs)) (setq *blog* blog) (let ((years (1+ (- (end-year blog) (start-year blog))))) (setf (timespan blog) (list (encode-universal-time 0 0 0 1 1 (start-year blog)) (encode-universal-time 0 0 0 31 12 (end-year blog))) ;; (contents blog) (make-array (list years 12 31 (total-languages)) :initial-element nil) ;; (months blog) (make-array (list years 12 (total-languages)) :initial-element nil))) (setf (end-entries blog) (list (second (timespan blog)) (first (timespan blog)))) (when t;; should have an arg for NOT adding default extensions! (add-extension blog 'rss-title #'rss-title) (add-extension blog 'soundtrack #'soundtrack) (add-extension blog 'minipage #'minipage) (add-extension blog 'block #'text-block) (add-extension blog 'image #'image) (add-extension blog 'cool-site #'cool-site)) ;; Transform hook contents from function name symbols to ;; actual functions, discarding any undefined functions (dolist (hook (hooks blog)) (setf (cdr hook) (mapcan #'(lambda (h) (when (fboundp h) (list (symbol-function h)))) (rest hook)))) (format t "~&Preparing to read indexes.") (when (html-directory blog) (dolist (file (directory (parse-namestring (string+ (html-directory blog) "/*" (index-suffix blog))))) (format t "~&Reading ~A..." file) (with-open-file (index file :direction :input) (let ((existing-page-index (read index))) (destructuring-bind (language year &rest months) existing-page-index (dolist (m months) (destructuring-bind (month &rest days) m (dolist (day days) (let ((time (encode-universal-time 0 0 0 day month year))) (when (> time (second (end-entries blog))) (setf (second (end-entries blog)) time)) (when (< time (first (end-entries blog))) (setf (first (end-entries blog)) time))) (unless (get-month blog year month language) (set-month blog year month language (list t nil))) (set-entry-value blog year month day language t))))))))) (format t "~&Preparing to read contents.") (when (text-directory blog) (dolist (file (directory (parse-namestring (string+ (text-directory blog) "/*" (file-suffix blog))))) (format t "~&Reading ~A...~%" file) (load file)))) #| (with-open-file (text file :direction :input) (loop for item = (read text nil) while item do (push item (text blog))))))) |# (defmacro defmonth (year month &rest day) (let ((y (gensym)) (m (gensym)) (days (gensym))) `(let ((,y ,year) (,m ,month) (,days ',day)) (push (list ,y ,m ,days) (text *blog*))))) (defmethod month-has-entries ((blog blog) year month language) (format t "~&MONTH-HAS-ENTRIES ~A ~A ~A" year month language) (let (entries) (loop for i from 1 upto (days-in-month month year) for entry = (get-entry blog year month i language) when entry do (setq entries t) (when (typep entry 'entry) (return-from month-has-entries 'pages))) (values entries))) ;;;;;;;;;;;;;;;; ;;;; Class entry (defclass entry (common) ((parent :accessor parent :initarg :parent) (previous :accessor previous :initform nil) (following :accessor following :initform nil) (date :accessor date :initarg :date) (language :accessor language :initarg :language) (location :accessor location :initarg :location :initform nil) (original-text :accessor original-text :initarg :original-text) (feed-text :accessor feed-text :initarg :feed-text) (plain-text :accessor plain-text :initarg :plain-text) (text :accessor text :initarg :text))) (defmethod get-arrow ((entry entry) which) (get-arrow (parent entry) which)) (defmethod year ((entry entry)) (sixth (multiple-value-list (decode-universal-time (date entry))))) (defmethod month ((entry entry)) (fifth (multiple-value-list (decode-universal-time (date entry))))) (defmethod day ((entry entry)) (fourth (multiple-value-list (decode-universal-time (date entry))))) (defmethod initialize-instance :after ((entry entry) &key year month day &allow-other-keys) (unless (and year month day) (error "Invalid date ~A ~A ~A" year month day)) (unless (get-month (parent entry) year month (language entry)) (set-month (parent entry) year month (language entry) (list t nil))) (setf (date entry) (encode-universal-time 0 0 0 day month year (timezone (parent entry))))) (defmethod get-entry ((blog blog) year month day language) ;; (format t "~&GET-ENTRY ~A ~A ~A ~A" year month day language) (aref (contents blog) (- year (start-year blog)) (1- month) (1- day) (position-language language))) (defmethod set-entry-value ((blog blog) year month day language value) (setf (aref (contents blog) (- year (start-year blog)) (1- month) (1- day) (position-language language)) value)) (defmethod set-entry ((blog blog) (entry entry)) (set-entry-value blog (year entry) (month entry) (day entry) (language entry) entry)) (defun adjacent-day (year month date which) (multiple-value-bind (second minute hour date month year) (decode-universal-time (funcall (ecase which (:previous #'-) (:following #'+)) (encode-universal-time 0 0 0 date month year) #.(* 24 60 60))) (declare (ignore second minute hour)) (values year month date))) (defmethod valid-day ((blog blog) year month date) (let ((target (encode-universal-time 0 0 0 date month year))) (and (>= target (first (end-entries blog))) (<= target (second (end-entries blog)))))) (defmethod adjacent-entry ((blog blog) (entry entry) which) (loop for (year month day) = (multiple-value-list (adjacent-day (year entry) (month entry) (day entry) which)) then (multiple-value-list (adjacent-day year month day which)) while (valid-day blog year month day) do (let ((e (get-entry blog year month day (language entry)))) (when (and e (not (eql e entry))) (return-from adjacent-entry (if (typep e 'entry) e (list year month day (language entry))))))) nil) (defmethod date-target ((entry entry)) (list (year entry) (month entry) (day entry) (language entry))) (defmethod date-target ((entry list)) entry) ;;;;;;;;;;;;;;;; (defun paragraphify (text) (let* ((par-breaks (loop for n below (length text) when (and (eql (elt text n) #\Newline) (eql (elt text (1+ n)) #\Newline)) collect n)) (breaks (if t par-breaks (loop for a in par-breaks for b = -1 then a when (> a (1+ b)) collect a))) (buffer (make-string (+ 7 (- (length text) (length breaks)) (* (length breaks) 6)))) (parbreak "

") (i 3)) (replace buffer "

") (loop for break in breaks with paragraph-start = 0 for len = (- break paragraph-start) do (when (< break paragraph-start) (error "breaks ~A ~D ~D ~S~%~S" breaks break paragraph-start text (subseq text 0 break))) (replace buffer text :start1 i :start2 paragraph-start :end2 break) (incf i len) (replace buffer parbreak :start1 i) (incf i (length parbreak)) (setf paragraph-start (+ 2 break)) finally (replace buffer text :start1 i :start2 paragraph-start) (incf i (- (length text) paragraph-start)) (replace buffer "

" :start1 i) ;;(print buffer) (return-from paragraphify buffer)))) (defun page-name (base year month day language &optional (suffix "html")) (format nil "~@[~A~]~4,'0D/~2,'0D/~4,'0D~2,'0D~@[~2,'0D~]-~A.~A" base year month year month day (language-short-symbol language) suffix)) (defmethod html-file-name ((entry entry)) (string+ (html-directory (parent entry)) (page-name nil (year entry) (month entry) (day entry) (language entry) "php"))) (defmethod link-name ((entry entry) &optional full) (page-name (if full (base-url (parent entry)) (local-base (parent entry))) (year entry) (month entry) (day entry) (language entry) "php")) (defmethod link-name ((list list) &optional full) (declare (ignore full)) (apply #'page-name list)) (defmethod local-base ((url string)) (if (eq 0 (search "http://" url)) (subseq url (position #\/ url :start 7)) url)) (defmethod page-title ((entry entry)) (format nil "~A ~A" (title (parent entry)) (iso-date entry))) (defmethod synopsis ((entry entry)) (subseq (plain-text entry) 0 (min (length (plain-text entry)) 100))) ;;;;;;;;;;;;;;;; ;;;; HTML functions (defmethod print-html-header (stream (blog blog) language title &optional h1) (format stream "~&~%~A~%~{~@[~A~]~%~}" title (list (when (stylesheet blog) (format nil "" (stylesheet blog))) (format nil "" (title blog) (base-url blog) language ))) (when (geo-location blog) (format stream "~&" (icbm (geo-location blog))) (format stream "~&" (title blog)) (format stream "~&" (geo-position (geo-location blog))) (format stream "~&" (geo-placename (geo-location blog))) (format stream "~&" (geo-region (geo-location blog)))) (format stream "~&") ;; (format stream "~&") (format stream "~&

~A

" (if h1 h1 title))) (defmethod get-hooks ((blog blog) hook-name) (rest (assoc hook-name (hooks blog)))) (defmethod html-header (stream (entry entry) &optional language) (declare (ignore language)) (print-html-header stream (parent entry) (language entry) (page-title entry) (date-name entry))) (defmethod html-header (stream (blog blog) &optional language) (print-html-header stream blog (if language language (default-language blog)) (title blog))) (defmethod html-footer (stream (blog blog) &optional language) (multiple-value-bind (second minute hour date month year day daylight-p zone) (get-decoded-time) (declare (ignore second minute hour day daylight-p zone)) (print-html-footer stream (get-hooks blog 'footer) (base-url blog) year month date (if language language (default-language blog))))) (defmethod html-footer (stream (entry entry) &optional language) (declare (ignore language)) (print-html-footer stream (get-hooks (parent entry) 'footer) (base-url (parent entry)) (year entry) (month entry) (day entry) (language entry))) (defun print-html-footer (stream hooks base-url year month day &optional language) (dolist (hook hooks) (funcall hook stream base-url year month day language)) (format stream "~&~%")) (defun link-to (url &optional link-text anchor) (unless url (error "URL has to be non-NIL -- LINK-TO")) (format nil "~A" url anchor (if link-text link-text url))) (defun img (source &optional alt border align class) (format nil "\"~@[~A~]\"~@[" source alt border align class)) (defun create-iso-date (y m d &optional (dash t)) (format nil (if dash "~@[~4,'0D~]~@[-~2,'0D~]~@[-~2,'0D~]" "~@[~4,'0D~]~@[~2,'0D~]~@[~2,'0D~]") y m d)) (defmethod iso-date ((entry entry) &optional (dash t)) (create-iso-date (year entry) (month entry) (day entry) dash)) (defmethod iso-date ((entry list) &optional (dash t)) (create-iso-date (first entry) (second entry) (third entry) dash)) (defun date-suffix (day) (if (and (>= day 10) (<= day 19)) "th" (case (mod day 10) (1 "st") (2 "nd") (3 "rd") (t "th")))) (defmethod date-name ((entry entry)) (let ((weekday (seventh (multiple-value-list (decode-universal-time (date entry)))))) (format nil (ecase (language entry) ;; (english "~A, ~D~A ~A ~D") ;; British (english "~A, ~D~* ~A ~D");; Australian (finnish "~A, ~D~*. ~(~A~)ta ~D")) (day-name (language entry) weekday) (day entry) nil ; (date-suffix (day entry) (month-name (month entry) (language entry)) (year entry)))) ;;;; (defun surrounding-months (year month) (let (previous-month previous-year following-month following-year) (cond ((= month 1) (setq previous-month 12 previous-year (1- year) following-month (1+ month) following-year year)) ((= month 12) (setq previous-month (1- month) previous-year year following-month 1 following-year (1+ year))) (t (setq previous-month (1- month) previous-year year following-month (1+ month) following-year year))) (values (list previous-month month following-month) (list previous-year year following-year)))) (defmethod output ((x t)) nil) (defmethod comment-link (stream (entry entry)) (format stream "~%" (year entry) (month entry) (day entry) (language entry))) (defmethod output ((entry entry)) (unless (or (null (text entry)) (string= "" (text entry))) (ensure-directories-exist (html-file-name entry)) (with-open-file (s (html-file-name entry) :direction :output :if-exists :supersede) (html-header s entry) ;; Surrounding month indexes (format s "~&
") (when (and (location entry) (weather-directory (parent entry))) (format s "~&~A~@[ ~A~]
" (substitute #\Space #\- (string (location entry))) (format-weather nil (get-weather (location entry) (year entry) (month entry) (day entry) (weather-directory (parent entry))) (language entry)))) (multiple-value-bind (months years) (surrounding-months (year entry) (month entry)) (loop for y in years for m in months for month = (get-month (parent entry) y m (language entry)) when (first month) do (format s "~&~A~%" (second month)))) (format s "~&
") (labels ((arrow (trigger direction) ;; prev/up/next arrows (let ((neighbour (funcall trigger entry))) (when neighbour (format s "~A" (link-to (link-name (if (listp neighbour) (cons nil neighbour) neighbour)) (img (get-arrow entry direction) (iso-date neighbour) 0)))))) (arrows () (format s "~&
") (arrow #'previous 'left) (format s "~A" (link-to (format nil "~Aindex-~A.html" (local-base (parent entry)) (language-short-symbol (language entry))) (img (get-arrow entry 'up) "index" 0))) (arrow #'following 'right) (format s "
"))) (arrows) ;; (loop for language in (language-symbols) unless (eql language (language entry)) do (let ((e (get-entry (parent entry) (year entry) (month entry) (day entry) language))) (when e (format s "~&
~A
" (link-to (link-name e) (language-cleartext language)))))) ;; (format s "~&
~A
" (text entry)) (arrows) (comment-link s entry) (html-footer s entry))))) (defmethod create-month-index ((blog blog) this-year this-month language) (multiple-value-bind (months years) (surrounding-months this-year this-month) (format t "~&CREATE-MONTH-INDEX ~A ~A" years months) (loop for y in years for m in months for month = (get-month blog y m language) do (unless (and month (stringp (second month))) (when (first month) (set-month blog y m language (list (first month) (with-output-to-string (md) (format md "~&~A ~4,'0D: " (month-name m language) y) (loop for i from 1 upto (days-in-month m y) for e = (get-entry blog y m i language) do (format md "~A " (if e (link-to (link-name (list (local-base blog) y m i language "php")) i) i)) finally (format md "~&
")))))))))) (defmethod create-entry ((blog blog) &rest key-args) (set-entry blog (apply #'make-instance 'entry :parent blog key-args))) (let (last-location) (defun get-location (pics) (let ((last-pic (car (last pics)))) (when (and last-pic (typep last-pic 'symbol)) (setq last-location last-pic)) last-location))) (defun get-title (pics language languages) (let ((title (find 'title pics :key #'(lambda (x) (typecase x (list (car x)) (t nil)))))) (when title (or (nth (position language languages) (rest title)) (first (rest title)))))) (defmethod make-entry ((blog blog) y m day-contents) (destructuring-bind (date &optional pics &rest texts) day-contents (let ((time (encode-universal-time 0 0 0 date m y))) (when (> time (second (end-entries blog))) (setf (second (end-entries blog)) time)) (when (< time (first (end-entries blog))) (setf (first (end-entries blog)) time))) (mapc #'(lambda (text language) (let ((plain (to-plain-text (substitute-tags blog text pics language :synopsis t)))) (create-entry blog :year y :month m :day date :language language :location (get-location pics) :title (get-title pics language (languages blog)) :original-text text :plain-text (string-left-trim " " (substitute #\Space #\Newline plain)) :feed-text (htmlify plain) :text (paragraphify (substitute-tags blog text pics language))))) texts (language-symbols)))) (defmethod create-pages ((blog blog)) (dolist (file-contents (text blog)) (destructuring-bind (year month days) file-contents (dolist (entry days) (make-entry blog year month entry)))) (format t "~&Pages created~%") (loop for year from (start-year blog) to (end-year blog) do (loop for month from 1 to 12 do (format t "~&Processing ~D/~D..." month year) (loop for language in (language-symbols) do (loop for day from 1 to (days-in-month month year) for entry = (get-entry blog year month day language) do ;; (format t " ~D-~2,'0D-~2,'0D." year month day) (when (typep entry 'entry) (unless (previous entry) (let ((prev (adjacent-entry blog entry :previous))) (format t "<~A<" prev) (when prev (setf (previous entry) prev)) (when (typep prev 'entry) (setf (following prev) entry)))) (unless (following entry) (let ((foll (adjacent-entry blog entry :following))) (format t ">~A>" foll) (when foll (setf (following entry) foll)) (when (typep foll 'entry) (setf (previous foll) entry))))))))) (format t "Done.~%")) (defun table-cell (contents) (format nil "~A" contents)) (defmethod output-pages ((blog blog)) (loop for language in (language-symbols) for lang in (language-short-symbols) when (position language (languages *blog*)) do (ensure-directories-exist (html-directory blog)) (with-open-file (s (format nil "~Aindex-~A.html" (html-directory blog) lang) :direction :output :if-exists :supersede) (html-header s blog language) ;; (loop for other-lang in (language-symbols) when (and (position other-lang (languages *blog*)) (not (eql other-lang language))) do (format s "~&~A" (language-short-symbol other-lang) (language-cleartext other-lang))) ;; (format s "~&
~%~A" (img (format nil "~Aindeksi.jpg" (string+ (local-base blog) (pic-directory blog))) (title blog))) ;; (loop for year from (end-year blog) downto (start-year blog) do (format s "~&") (loop for month from 12 downto 1 for entries = (month-has-entries blog year month language) when entries do (when (eql entries 'pages) (with-open-file (page-index (format nil "~A/~A-~4,'0D-~2,'0D~A" (html-directory blog) language year month (index-suffix blog)) :direction :output :if-exists :supersede) (format page-index "~&(~A ~4,'0D (~D" language year month) (loop for day from 1 upto (days-in-month month year) when (get-entry blog year month day language) do (format page-index " ~D" day)) (format page-index "))~%"))) (create-month-index blog year month language) (format s "~&~%" (month-name month language) year) (loop for day from 1 upto (days-in-month month year) for page = (get-entry blog year month day language) do (cond (page (format s (table-cell (link-to (page-name (local-base blog) year month day language "php") day))) (output page)) (t (format s (table-cell day))))) (format s "~&~%")) (format s "~&
~A ~A
")) ; year (format s "~A" (link-to (string+ (base-url blog) (string-downcase language) ".xml") (img (string+ (base-url blog) "kuvat/xml.png") "RSS Feed" 0))) (dolist (hook (get-hooks blog 'index)) (funcall hook s (base-url blog) language)) (html-footer s blog language)))) ;;;;;;;;;;;;;;;; (defun startup (&optional (init-file "blog.conf")) (when (and init-file (probe-file init-file)) (load init-file)) (unless *blog* (setq *blog* (make-instance 'blog)))) (defun meta (stream http-equiv content) (format stream "~&~%" http-equiv content)) (defun starts-with (string-1 string-2) (not (mismatch string-1 string-2 :end1 (min (length string-1) (length string-2))))) (defmethod redirect ((blog blog)) (flet ((redirect-to (entry) (page-name nil (year entry) (month entry) (day entry) (language entry) "php"))) (let ((old-file (make-pathname :directory (html-directory blog) :name "" :type "htaccess")) (input-file (pathname (string+ (text-directory blog) "htaccess"))) (new-file (pathname "/tmp/blogsym-htaccess")) (language-indexes (mapcar #'(lambda (x) (cons x (format nil "RewriteRule ^~A/ " (string-downcase x)))) (languages blog)))) (with-open-file (input input-file :direction :input) (with-open-file (htaccess new-file :direction :output :if-exists :supersede) (loop for line = (read-line input nil) while line do (cond ((starts-with line "RewriteRule ^$ ") (format htaccess "RewriteRule ^$ ~A [R]~%" (redirect-to (last-page blog)))) ((starts-with line "RewriteRule ^index.html$ ") (format htaccess "RewriteRule ^index.html$ ~A [R]~%" (redirect-to (last-page blog)))) (t (let (printed) (dolist (lang language-indexes) (when (starts-with line (cdr lang)) (setq printed t) (format htaccess "~A ~A [R]~%" (cdr lang) (redirect-to (last-page blog (car lang)))))) (unless printed (format htaccess "~A~%" line)))))))) (when (probe-file old-file) (delete-file old-file)) (rename-file new-file old-file)))) (defmethod get-pages ((blog blog) from-end language how-many) (let (ret (counter (or how-many 1))) (do ((year (if from-end (end-year blog) (start-year blog)) (if from-end (1- year) (1+ year)))) ((if from-end (< year (start-year blog)) (> year (end-year blog))) nil) (do ((month (if from-end 12 1) (if from-end (1- month) (1+ month)))) ((if from-end (< month 1) (> month 12)) nil) (do ((day (if from-end (days-in-month month year) 1) (if from-end (1- day) (1+ day)))) ((if from-end (< day 1) (> day (days-in-month month year))) nil) (let ((e (get-entry blog year month day (or language (default-language blog) (first (languages blog)))))) (when e (push e ret) (decf counter) (when (= 0 counter) (return-from get-pages (nreverse ret)))))))) (nreverse ret))) (defmethod first-pages ((blog blog) &optional language how-many) (get-pages blog nil language how-many)) (defmethod last-pages ((blog blog) &optional language how-many) (get-pages blog t language how-many)) (defmethod last-page ((blog blog) &optional language) (unless language (setq language (default-language blog))) (unless (assoc language (slot-value blog 'last-page)) (push (cons language (first (last-pages blog language))) (slot-value blog 'last-page))) (cdr (assoc language (slot-value blog 'last-page)))) (defmethod entries ((blog blog) which language &optional size) (ecase which (:first (first-pages blog language size)) (:last (last-pages blog language size)))) (defun rss-tag (stream indent tag text) (let ((tag (let ((s (split (string-downcase tag) #\-))) (string+ (first s) (mapcar #'string-capitalize (rest s)))))) (format stream "~&~A<~A>~A" (make-string indent :initial-element #\Space) tag text tag))) (defun rss-tags (stream indent &rest tags) (loop for tag on tags by #'cddr when (second tag) do (rss-tag stream indent (first tag) (second tag)))) (defun publication-date (date) (multiple-value-bind (second minute hour date month year day daylight-saving-time-p time-zone) (decode-universal-time date) (format nil "~[Mon~;Tue~;Wed~;Thu~;Fri~;Sat~;Sun~], ~D ~A ~D ~2,'0D:~2,'0D:~2,'0D UT" day date (subseq (month-name month 'english) 0 3) year hour minute second))) (defun positions (item seq &key max) (loop for c = (position item seq :start (if c (1+ c) 0)) for limit = (or (not max) (>= (decf max) 0)) while (and c limit) collect c)) (defun tags (text) (let ((open (positions #\< text)) (close (positions #\> text))) (mapcar #'cons open close))) (defun not-tags (text) (let ((tags (tags text))) (if (not tags) nil (append (loop for (open . close) in (tags text) with start = 0 for end = open collect (prog1 (cons start end) (setf start (1+ close)))) (when (< (cdar (last tags)) (1- (length text))) (list (cons (1+ (cdar (last tags))) nil))))))) (defun not-char (text &optional (ch #\Newline)) ;; doesn't work - leaves out stuff after last ch (let ((pos (positions ch text))) (loop for p in pos with s = 0 collect (prog1 (cons s p) (setf s (1+ p)))))) (defmacro cut-text (marker-fun text) (let ((txt (gensym)) (pos (gensym))) `(let* ((,txt ,text) (,pos (funcall ,marker-fun ,txt))) (if (not ,pos) ,txt (loop for (start . end) in ,pos collect (subseq ,text start end) into s finally (return (string+ s))))))) (defun to-plain-text (text) (cut-text #'not-tags text)) (defun remove-linebreaks (text) ;; doesn't work - see above (cut-text #'not-char text)) (defmethod rss-item-title ((entry entry)) (cond ((title entry) (title entry)) (t (let* ((plain-text (plain-text entry)) (period (position #\. plain-text)) (comma (position #\, plain-text)) (space (nth 5 (positions #\Space plain-text :max 6))) (len (length plain-text))) (subseq plain-text 0 (min (or period len) (or comma len) (or space len))))))) (defmethod rss (stream (blog blog) &optional language size) (format stream "~&") (format stream "~&~% ") (rss-tags stream 2 'title (title blog) 'managing-editor (contact-address blog) 'link (base-url blog) 'description (title blog) 'language (string-downcase (language-iso639 language (nth (position language (languages blog)) (language-variants blog)))) 'pub-date (publication-date (date (last-page blog language))) 'generator (string+ *blogsym-name* " " *blogsym-version*)) (dolist (entry (entries blog :last language (or size (rss-size blog)))) (format stream "~& ") (rss-tags stream 3 'title (rss-item-title entry) 'link (link-name entry t) 'pub-date (publication-date (date entry)) 'description (feed-text entry)) (format stream "~& ")) (format stream "~& ~%")) (defun create (&optional force) (unless (and *blog* (not force)) (startup)) (create-pages *blog*) (output-pages *blog*) (ensure-directories-exist (html-directory *blog*)) (redirect *blog*) (loop for language in (language-symbols) when (position language (languages *blog*)) do (format t "~&RSS: ~A..." language) (with-open-file (xml (format nil "~A/~A.xml" (html-directory *blog*) (string-downcase language)) :direction :output :if-exists :supersede) (rss xml *blog* language)))) ;; EOF