;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; 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 "~&
") (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 | ~%" (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 "~&
|---|