lat.lisp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. ;;;; lat.lisp
  2. (in-package #:lat)
  3. ;;; "lat" goes here. Hacks and glory await!
  4. #|
  5. Задать вопрос, получить ответ.
  6. Показать результат.
  7. state: problems more-problems current-problem
  8. mode: show check
  9. request: type answer
  10. |#
  11. (defstruct (request (:conc-name))
  12. mode
  13. answer)
  14. (defun show-finish (id)
  15. (djula:render-template* "finish-exercise.html" nil :id id :menu (menu)))
  16. (defun show-mistake (problem answers id)
  17. (djula:render-template* "mistake.html" nil :id id :problem (corrected-answer-to-html problem answers) :menu (menu)))
  18. (defun show-ok (problem id)
  19. (djula:render-template* "ok.html" nil :id id :problem (answer-to-html problem) :menu (menu)))
  20. (defun prefix (problem)
  21. (getf problem :prefix))
  22. (defun suffix (problem)
  23. (getf problem :suffix))
  24. (defun show-problem (problem id)
  25. (djula:render-template* "ex-show.html" nil :problem (problem-to-html problem) :id id :menu (menu `(:exercise ,id))))
  26. ; (defvar *server* (hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 12345)))
  27. (djula:add-template-directory (asdf:system-relative-pathname "lat" "templates/"))
  28. (hunchentoot:define-easy-handler (home :uri "/lat") ()
  29. (unless hunchentoot:*session*
  30. (hunchentoot:start-session))
  31. (djula:render-template* "base.html" nil :menu (menu)))
  32. (hunchentoot:define-easy-handler serve-exercise ((mode :request-type :both) (answer :parameter-type '(list string) :request-type :post))
  33. (let* ((uri (hunchentoot:request-uri*))
  34. (exercise-id (if (and (eql (search "/lat/" uri) 0)
  35. (> (length uri) 5)) ; should always be true
  36. (subseq uri 5)
  37. nil)))
  38. (when (null exercise-id)
  39. (setf (hunchentoot:return-code*) hunchentoot:+http-not-found+)
  40. (return-from serve-exercise))
  41. (let ((problems (make-instance 'cl-containers:basic-queue)))
  42. (with-open-file (in (asdf:system-relative-pathname "lat" (format nil "exercises/~A.txt" exercise-id))
  43. :direction :input
  44. :if-does-not-exist nil)
  45. (when (null in)
  46. (setf (hunchentoot:return-code*) hunchentoot:+http-not-found+)
  47. (return-from serve-exercise))
  48. (loop for line = (read-line in nil)
  49. while line
  50. do (cl-containers:insert-item problems (parse-problem line)))
  51. (if hunchentoot:*session*
  52. (progn
  53. (let ((request (make-request :mode mode :answer answer)))
  54. (if (or (string= mode "check")
  55. (string= mode "show"))
  56. (exercise request (hunchentoot:session-value 'problems) (hunchentoot:session-value 'id))
  57. (progn
  58. (setf (hunchentoot:session-value 'problems) problems)
  59. (setf (hunchentoot:session-value 'exercise-id) "p1e1")
  60. (setf (mode request) "show")
  61. (exercise request (hunchentoot:session-value 'problems) (hunchentoot:session-value 'id))))))
  62. (hunchentoot:redirect "/lat"))))))
  63. (push (hunchentoot:create-regex-dispatcher "/lat/p\\d+e\\d*" 'serve-exercise) hunchentoot:*dispatch-table*)
  64. (push (hunchentoot:create-regex-dispatcher "/lat/e\\d+[a-z]?" 'serve-exercise) hunchentoot:*dispatch-table*)
  65. (hunchentoot:define-easy-handler (test :uri "/test") ()
  66. (hunchentoot:start-session)
  67. (hunchentoot:redirect "/"))
  68. (hunchentoot:define-easy-handler (p0 :uri "/lat/p0") ()
  69. (djula:render-template* "units/p0.html" nil :menu (menu '(:lesson 0))))
  70. (hunchentoot:define-easy-handler (p2 :uri "/lat/p2") ()
  71. (djula:render-template* "units/p2-accents.html" nil :menu (menu '(:lesson 2))))
  72. (hunchentoot:define-easy-handler (p1 :uri "/lat/p1") ()
  73. (djula:render-template* "units/p1-accents.html" nil :menu (menu '(:lesson 1))))
  74. (hunchentoot:define-easy-handler (p3 :uri "/lat/p3") ()
  75. (djula:render-template* "unit.html" nil :unit "units/p3.txt" :menu (menu '(:lesson 3))))
  76. (hunchentoot:define-easy-handler (p4 :uri "/lat/p4") ()
  77. (djula:render-template* "unit.html" nil :unit "units/p4.txt" :menu (menu '(:lesson 4))))
  78. (defun normalize-answer (string)
  79. (with-output-to-string (s)
  80. (loop for c across (string-downcase string)
  81. with remembered
  82. do (if (find c '(#\COMBINING_MACRON #\= #\_))
  83. (when remembered
  84. (write-char (case remembered
  85. (#\a #\LATIN_SMALL_LETTER_A_WITH_MACRON)
  86. (#\e #\LATIN_SMALL_LETTER_E_WITH_MACRON)
  87. (#\i #\LATIN_SMALL_LETTER_I_WITH_MACRON)
  88. (#\o #\LATIN_SMALL_LETTER_O_WITH_MACRON)
  89. (#\u #\LATIN_SMALL_LETTER_U_WITH_MACRON)
  90. (t remembered))
  91. s)
  92. (setf remembered nil))
  93. (progn
  94. (when remembered
  95. (write-char remembered s))
  96. (if (find c "aeiou")
  97. (setf remembered c)
  98. (progn
  99. (setf remembered nil)
  100. (write-char c s)))))
  101. finally (when remembered
  102. (write-char remembered s)))))
  103. (defun fails (answers problem)
  104. (let ((keys (loop for (tag . content) in problem
  105. when (eq tag :blank)
  106. collect (or (getf content :regexp) (normalize-answer (getf content :key))))))
  107. (loop for answer in answers
  108. for key in keys
  109. for i from 0
  110. unless (cl-ppcre:scan key (normalize-answer answer))
  111. collect i)))
  112. (defun exercise (request problems id)
  113. (cond ((string= (mode request) "show")
  114. (if (cl-containers:empty-p problems)
  115. (show-finish id)
  116. (show-problem (cl-containers:first-element problems) id)))
  117. ((string= (mode request) "check")
  118. (let ((problem (cl-containers:dequeue problems)))
  119. (if (fails (answer request) problem)
  120. (progn
  121. (cl-containers:enqueue problems problem)
  122. (show-mistake problem (answer request) id))
  123. (show-ok problem id))))))
  124. (defun gap-to-html (gap-content number stream)
  125. (format stream "<input type=\"text\" name=\"answer\" id=answer~D oninput=\"add_macrons(this)\">~@[ <em>(~A)</em> ~]"
  126. number
  127. (getf gap-content :hint)))
  128. (defun problem-to-html (problem)
  129. (with-output-to-string (s)
  130. (loop for (tag . content) in problem
  131. with gap-number = 0
  132. do (case tag
  133. (:text (loop for word in (split-sequence:split-sequence #\Space (first content))
  134. do (write-string (add-stress word) s)
  135. (write-char #\Space s)
  136. finally (unread-char #\Space s)))
  137. (:blank (gap-to-html content gap-number s) (incf gap-number))))))
  138. (defun answer-to-html (problem)
  139. (with-output-to-string (s)
  140. (loop for (tag . content) in problem
  141. do (case tag
  142. (:text (write-string (first content) s))
  143. (:blank (format s "<strong>~A</strong>" (getf content :key)))))))
  144. (defun corrected-answer-to-html (problem answers)
  145. (let ((fails (fails answers problem)))
  146. (with-output-to-string (s)
  147. (loop with gap-number = 0
  148. for (tag . content) in problem
  149. do (case tag
  150. (:text (write-string (first content) s))
  151. (:blank (if (member gap-number fails)
  152. (format s "<s>~A</s> <strong>~A</strong>" (elt answers gap-number) (getf content :key))
  153. (format s "<strong>~A</strong>" (getf content :key)))
  154. (incf gap-number)))))))
  155. (defun get-immediate-bracket (string start)
  156. (if (or (>= start (length string))
  157. (not (char= (char string start) #\[)))
  158. (values nil start)
  159. (let ((end (position #\] string :start start)))
  160. (when (null end)
  161. (error "Unmatched [."))
  162. (values (subseq string (1+ start) end) (1+ end)))))
  163. (defun parse-problem (string)
  164. (let ((start 0)
  165. result)
  166. (nreverse (loop
  167. (when (>= start (length string))
  168. (return result))
  169. (let ((blank (position #\_ string :start start)))
  170. (unless blank
  171. (return (cons (list :text (subseq string start))
  172. result)))
  173. (unless (= blank start)
  174. (push (list :text (subseq string start blank)) result))
  175. (multiple-value-bind (args next-start) (let ((args '())
  176. (next-start (1+ blank))
  177. arg)
  178. (loop
  179. (multiple-value-setq (arg next-start) (get-immediate-bracket string next-start))
  180. (when (null arg)
  181. (return (values (nreverse args) next-start)))
  182. (push arg args)))
  183. (setf start next-start)
  184. (cond ((null args) (error "Key not provided."))
  185. ((null (rest args)) (push (list :blank :key (first args)) result))
  186. ((null (nthcdr 2 args)) (push (list :blank
  187. :key (first args)
  188. :hint (second args))
  189. result))
  190. ((null (nthcdr 3 args)) (push (list :blank
  191. :key (first args)
  192. :hint (second args)
  193. :regexp (third args))
  194. result))
  195. (t (error "Too much arguments for a blank.")))))))))
  196. (defun add-stress (word)
  197. (if (member word '("illīc" "adhūc" "addūc" "tantōn") :test #'string-equal)
  198. (concatenate 'string
  199. (subseq word 0 (1- (length word)))
  200. (list #\COMBINING_ACUTE_ACCENT (char word (1- (length word)))))
  201. (flet ((vowelp (c) (find c "aeiouāēīōū" :test #'char-equal))
  202. (short-vowel-p (c) (find c "aeiou" :test #'char-equal))
  203. (mutap (c) (find c "bcdfgpt" :test #'char-equal))
  204. (liquidap (c) (find c "rlmn" :test #'char-equal)))
  205. (flet ((get-vowel (start)
  206. (loop for i from start downto 0
  207. when (and (vowelp (char word i))
  208. (or (zerop i)
  209. (char-not-equal (char word (1- i)) #\q)))
  210. do (return i)
  211. finally (return nil))))
  212. (let* ((vowel-1 (get-vowel (1- (length word))))
  213. (vowel-2 (and vowel-1 (get-vowel (1- vowel-1))))
  214. (vowel-3 (and vowel-2 (get-vowel (1- vowel-2)))))
  215. (if (and vowel-3
  216. (short-vowel-p (char word vowel-2))
  217. (or (and (= (- vowel-1 vowel-2) 1))
  218. (and (= (- vowel-1 vowel-2) 2)
  219. (not (find (char word (1+ vowel-2)) "jz" :test #'char-equal)))
  220. (and (= (- vowel-1 vowel-2) 3)
  221. (mutap (char word (1+ vowel-2)))
  222. (liquidap (char word (+ vowel-2 2))))))
  223. (concatenate 'string
  224. (subseq word 0 (1+ vowel-3))
  225. (list #\COMBINING_ACUTE_ACCENT)
  226. (subseq word (1+ vowel-3)))
  227. word))))))
  228. (defun add-stresses (text)
  229. (flet ((latin-letter-p (c) (string= (cl-unicode:script c) "Latin"))
  230. )
  231. (with-output-to-string (s)
  232. (loop with start = 0
  233. for word-start = (position-if #'latin-letter-p text :start start)
  234. for word-end = (and word-start (position-if (complement #'latin-letter-p) text :start word-start))
  235. do (write-string (subseq text start word-start) s)
  236. (when word-start
  237. (write-string (add-stress (subseq text word-start word-end)) s))
  238. (setf start word-end)
  239. while (and word-start word-end)
  240. finally (when start
  241. (write-string (subseq text start) s))))))
  242. (defun emit-html (parsed-markup &optional stream)
  243. (if (atom parsed-markup)
  244. (princ parsed-markup stream)
  245. (let ((head (first parsed-markup))
  246. (body (rest parsed-markup)))
  247. (macrolet ((do-body ()
  248. '(dolist (item body)
  249. (emit-html item stream))))
  250. (ecase head
  251. (:body (do-body))
  252. (:p (write-line "<p>" stream) (do-body) (write-line "</p>" stream))
  253. (:e (write-string "<em>" stream) (do-body) (write-string "</em>" stream))
  254. (:b (write-string "<strong>" stream) (do-body) (write-string "</strong>" stream))
  255. (:h1 (write-line "<h1>" stream) (do-body) (write-line "</h1>" stream))
  256. (:h2 (write-line "<h2>" stream) (do-body) (write-line "</h2>" stream))
  257. (:h3 (write-line "<h3>" stream) (do-body) (write-line "</h3>" stream))
  258. (:la (write-string (add-stresses (with-output-to-string (s)
  259. (emit-html (first body) s)))
  260. stream)))))))
  261. (defun unit2html (file)
  262. (let ((path (merge-pathnames file (asdf:system-source-directory "lat"))))
  263. (with-output-to-string (s)
  264. (emit-html (com.gigamonkeys.markup:parse-file path) s))))
  265. (djula::def-filter :unit2html (file)
  266. (unit2html file))
  267. (defparameter *toc* '((:type :lesson :id 0 :title "Алфавит и произношение")
  268. (:type :lesson :id 1 :title "Quid facis?")
  269. (:type :exercise :lesson 1 :uri "/lat/p1e1")
  270. (:type :lesson :id 2 :title "Quid vīs?")
  271. (:type :exercise :lesson 2 :uri "/lat/p2e1")
  272. (:type :lesson :id 3 :title "Fac! Nōlī facere!")
  273. (:type :exercise :lesson 3 :id "3a" :uri "/lat/p3e1")
  274. (:type :exercise :lesson 3 :id "3b" :uri "/lat/p3e2")
  275. (:type :lesson :id 4 :title "Vīs facere sed nōn facis")
  276. (:type :exercise :lesson 4)
  277. ))
  278. (defun toc-item-type (description)
  279. (getf description :type))
  280. (defun toc-exercise-lesson (exercise)
  281. (getf exercise :lesson))
  282. (defun toc-item-id (item)
  283. (or (getf item :id )
  284. (and (eql (toc-item-type item) :exercise)
  285. (toc-exercise-lesson item))))
  286. (defun toc-item-default-uri (item)
  287. (ecase (toc-item-type item)
  288. (:lesson (format nil "/lat/p~D" (toc-item-id item)))
  289. (:exercise (format nil "/lat/e~A" (toc-item-id item)))))
  290. (defun toc-item-uri (item)
  291. (or (getf item :uri)
  292. (toc-item-default-uri item)))
  293. (defun toc-item-title (item)
  294. (getf item :title))
  295. (defun toc-item-full-title (item)
  296. (case (toc-item-type item)
  297. (:lesson (format nil "~:[Урок ~D.~;Вводный урок.~*~] ~A" (zerop (toc-item-id item)) (toc-item-id item) (toc-item-title item)))
  298. (:exercise (format nil "Упражнение ~A" (toc-item-id item)))))
  299. (defparameter *menu-classes*
  300. '((:lesson "menu-button-lesson")
  301. (:exercise "menu-button-exercise")))
  302. ;; current: (:lesson 1)
  303. (defun menu (&optional current)
  304. (loop for item in *toc*
  305. collect `((:href . ,(toc-item-uri item))
  306. (:title . ,(toc-item-full-title item))
  307. (:class . ,(format nil "~A~:[~; menu-button-current~]"
  308. (second (assoc (toc-item-type item) *menu-classes*))
  309. (equal (list (toc-item-type item)
  310. (toc-item-id item))
  311. current)))
  312. (:text . ,(toc-item-id item)))))
  313. #+nil (with-open-file (in (asdf:system-relative-pathname "lat" "templates/units/p2.html")
  314. :direction :input)
  315. (with-open-file (out (asdf:system-relative-pathname "lat" "templates/units/p2-accents.html")
  316. :direction :output
  317. :if-exists :supersede)
  318. (loop for line = (read-line in nil)
  319. while line
  320. do (write-line (add-stresses line) out))))