| 1 |
(in-package :bos.web) |
|---|
| 2 |
|
|---|
| 3 |
(defvar *dictionary* (make-hash-table :test #'equal)) |
|---|
| 4 |
(defvar *dictionary-last-reads-alist* nil) |
|---|
| 5 |
(defvar *dictionary-keys* nil |
|---|
| 6 |
"An alist of key compile-file-pathnames pairs that is |
|---|
| 7 |
automatically build at compile-time from all occurences of |
|---|
| 8 |
DICTIONARY-ENTRY in user-code.") |
|---|
| 9 |
|
|---|
| 10 |
(defmacro dictionary-directory () |
|---|
| 11 |
"The xml files containing dictionary definitions for a |
|---|
| 12 |
particular language are stored under |
|---|
| 13 |
(dictionary-directory)/<language>/dictionary.xml." |
|---|
| 14 |
'*website-directory*) |
|---|
| 15 |
|
|---|
| 16 |
(deftype dictionary-language () |
|---|
| 17 |
'keyword) |
|---|
| 18 |
|
|---|
| 19 |
(deftype dictionary-language-designator () |
|---|
| 20 |
'(or string dictionary-language)) |
|---|
| 21 |
|
|---|
| 22 |
(defun dictionary-language (language-designator) |
|---|
| 23 |
(declare (dictionary-language-designator language-designator)) |
|---|
| 24 |
(typecase language-designator |
|---|
| 25 |
(string (intern (string-upcase language-designator) #.(find-package "KEYWORD"))) |
|---|
| 26 |
(keyword language-designator))) |
|---|
| 27 |
|
|---|
| 28 |
(defun dictionary-key-occurences (key) |
|---|
| 29 |
(cdr (assoc key *dictionary-keys* :test #'string=))) |
|---|
| 30 |
|
|---|
| 31 |
(defun (setf dictionary-key-occurences) (value key) |
|---|
| 32 |
(let ((cons (assoc key *dictionary-keys* :test #'string=))) |
|---|
| 33 |
(if cons |
|---|
| 34 |
(rplacd cons value) |
|---|
| 35 |
(push (cons key value) *dictionary-keys*)))) |
|---|
| 36 |
|
|---|
| 37 |
(defun %dictionary-entry (key language) |
|---|
| 38 |
(let ((language (dictionary-language language))) |
|---|
| 39 |
(load-dictionary-if-needed language) |
|---|
| 40 |
(or (cdr (assoc language (gethash key *dictionary*))) |
|---|
| 41 |
key))) |
|---|
| 42 |
|
|---|
| 43 |
(defun (setf %dictionary-entry) (value key language) |
|---|
| 44 |
(let* ((language (dictionary-language language)) |
|---|
| 45 |
(it (assoc language (gethash key *dictionary*)))) |
|---|
| 46 |
(if it |
|---|
| 47 |
(rplacd it value) |
|---|
| 48 |
(push (cons language value) (gethash key *dictionary*))))) |
|---|
| 49 |
|
|---|
| 50 |
(defmacro dictionary-entry (key language) |
|---|
| 51 |
(flet ((pathname-equal (a b) |
|---|
| 52 |
(equal (namestring a) (namestring b)))) |
|---|
| 53 |
(when (constantp key) |
|---|
| 54 |
(check-type key string) |
|---|
| 55 |
(when *compile-file-pathname* |
|---|
| 56 |
(pushnew *compile-file-pathname* (dictionary-key-occurences key) |
|---|
| 57 |
:test #'pathname-equal))) |
|---|
| 58 |
`(%dictionary-entry ,key ,language))) |
|---|
| 59 |
|
|---|
| 60 |
(defun dictionary-clear-entries-by-language (language) |
|---|
| 61 |
(declare (dictionary-language language)) |
|---|
| 62 |
(maphash (lambda (key value) |
|---|
| 63 |
;; (setf gethash) with key explicitly allowed by ANSI CL |
|---|
| 64 |
(setf (gethash key *dictionary*) |
|---|
| 65 |
(remove language value :key #'car))) |
|---|
| 66 |
*dictionary*)) |
|---|
| 67 |
|
|---|
| 68 |
(defun dictionary-last-read (language) |
|---|
| 69 |
(declare (dictionary-language language)) |
|---|
| 70 |
(let ((time (cdr (assoc language *dictionary-last-reads-alist*)))) |
|---|
| 71 |
(if time |
|---|
| 72 |
time |
|---|
| 73 |
0))) |
|---|
| 74 |
|
|---|
| 75 |
(defun (setf dictionary-last-read) (value language) |
|---|
| 76 |
(declare (dictionary-language language)) |
|---|
| 77 |
(let ((cons (assoc language *dictionary-last-reads-alist*))) |
|---|
| 78 |
(if cons |
|---|
| 79 |
(rplacd cons value) |
|---|
| 80 |
(push (cons language value) *dictionary-last-reads-alist*)))) |
|---|
| 81 |
|
|---|
| 82 |
(defun dictionary-xml-path (language) |
|---|
| 83 |
(declare (dictionary-language language)) |
|---|
| 84 |
(merge-pathnames (make-pathname :name "dictionary" |
|---|
| 85 |
:type "xml" |
|---|
| 86 |
:directory (list :relative "templates" (string-downcase (string language)))) |
|---|
| 87 |
(dictionary-directory))) |
|---|
| 88 |
|
|---|
| 89 |
(defun load-dictionary (language xml-path) |
|---|
| 90 |
(declare (dictionary-language language)) |
|---|
| 91 |
(labels ((trim-whitespace (string) |
|---|
| 92 |
(string-trim '(#\space #\newline #\tab) string)) |
|---|
| 93 |
(load-language (language xml-path) |
|---|
| 94 |
(handler-case |
|---|
| 95 |
(let ((xmls (cxml:parse-file (truename xml-path) (cxml-xmls:make-xmls-builder)))) |
|---|
| 96 |
(assert (equal "dictionary" (cxml-xmls:node-name xmls)) nil |
|---|
| 97 |
"root element should be \"dictionary\"") |
|---|
| 98 |
(dolist (element (cxml-xmls:node-children xmls)) |
|---|
| 99 |
(when (consp element) |
|---|
| 100 |
(assert (equal "entry" (cxml-xmls:node-name element)) nil |
|---|
| 101 |
"expected element \"entry\"") |
|---|
| 102 |
(let ((key-value (remove-if #'atom (cxml-xmls:node-children element)))) |
|---|
| 103 |
(assert (equal "key" (cxml-xmls:node-name (first key-value))) nil |
|---|
| 104 |
"expected element \"key\"") |
|---|
| 105 |
(assert (equal "value" (cxml-xmls:node-name (second key-value))) nil |
|---|
| 106 |
"expected element \"value\"") |
|---|
| 107 |
(let ((key (first (cxml-xmls:node-children (first key-value)))) |
|---|
| 108 |
(value (first (cxml-xmls:node-children (second key-value))))) |
|---|
| 109 |
(when value |
|---|
| 110 |
(let ((key (trim-whitespace key)) |
|---|
| 111 |
(value (trim-whitespace value))) |
|---|
| 112 |
(assert (stringp key)) |
|---|
| 113 |
(assert (stringp value)) |
|---|
| 114 |
(unless (zerop (length value)) |
|---|
| 115 |
(setf (%dictionary-entry key language) value))))))))) |
|---|
| 116 |
(error (c) |
|---|
| 117 |
(error "Error while loading ~a:~%~a" |
|---|
| 118 |
(enough-namestring xml-path (dictionary-directory)) c))))) |
|---|
| 119 |
(dictionary-clear-entries-by-language language) |
|---|
| 120 |
(load-language language xml-path) |
|---|
| 121 |
(setf (dictionary-last-read language) (get-universal-time)) |
|---|
| 122 |
*dictionary*)) |
|---|
| 123 |
|
|---|
| 124 |
(defun load-dictionary-if-needed (language) |
|---|
| 125 |
(declare (dictionary-language language)) |
|---|
| 126 |
(let ((xml-path (dictionary-xml-path language))) |
|---|
| 127 |
(when (> (file-write-date xml-path) |
|---|
| 128 |
(dictionary-last-read language)) |
|---|
| 129 |
(load-dictionary language xml-path)))) |
|---|
| 130 |
|
|---|
| 131 |
(defun dictionary-write-template (&optional (stream *standard-output*)) |
|---|
| 132 |
(cxml:with-xml-output (make-character-stream-sink stream :canonical nil :indentation 2) |
|---|
| 133 |
(with-element "dictionary" |
|---|
| 134 |
(loop for (key . paths) in (sort (copy-list *dictionary-keys*) #'string< :key #'car) |
|---|
| 135 |
do (cxml:comment (format nil " in ~A ~{~%~8T~A ~}" (first paths) (rest paths))) |
|---|
| 136 |
do (with-element "entry" |
|---|
| 137 |
(with-element "key" (text key)) |
|---|
| 138 |
(with-element "value" (text ""))))))) |
|---|