web.lisp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. ;; web.lisp -- Website to allow users to make truth tables
  2. ;; Copyright (C) 2024 Alexander Rosenberg
  3. ;;
  4. ;; This program is free software: you can redistribute it and/or modify
  5. ;; it under the terms of the GNU General Public License as published by
  6. ;; the Free Software Foundation, either version 3 of the License, or
  7. ;; (at your option) any later version.
  8. ;;
  9. ;; This program is distributed in the hope that it will be useful,
  10. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. ;; GNU General Public License for more details.
  13. ;;
  14. ;; You should have received a copy of the GNU General Public License
  15. ;; along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. (defpackage #:truth-table/web
  17. (:use #:common-lisp #:truth-table/base
  18. #:truth-table/args)
  19. (:export #:toplevel #:main)
  20. (:import-from #:reblocks/app
  21. #:defapp)
  22. (:import-from #:reblocks/html
  23. #:with-html)
  24. (:import-from #:reblocks/widget
  25. #:defwidget
  26. #:update
  27. #:render)
  28. (:import-from #:reblocks-ui/form
  29. #:with-html-form)
  30. (:import-from #:reblocks/actions
  31. #:make-js-action)
  32. (:import-from #:reblocks/dependencies
  33. #:get-dependencies))
  34. (in-package :truth-table/web)
  35. (defparameter *default-port* 8000)
  36. (defparameter *default-address* "127.0.0.1")
  37. (defparameter *default-prefix* "/")
  38. (defwidget help-overlay ()
  39. ()
  40. (:documentation "Simple class to handle holding the help overlay."))
  41. (defmethod render ((overlay help-overlay))
  42. (with-html
  43. (:div :id "help-table-wrapper"
  44. (:div :id "help-header-wrapper"
  45. (:span :id "help-header" "Help")
  46. (:span :id "help-close-button"
  47. :onclick "document.querySelector(\".help-overlay\").style.display = \"none\""
  48. "Close"))
  49. (:table ;:style "margin-bottom: 10px;"
  50. (:tr (:th "Operator") (:th "Syntax"))
  51. (loop for ((sym (name . nics) desc (examples)) . rest-desc)
  52. = *operator-descriptions* then rest-desc
  53. for ((_sym . syntax) . rest-st)
  54. = *operator-symbol-table* then rest-st
  55. while sym
  56. do
  57. (:tr
  58. (:td name)
  59. (:td (format nil "~{~a~^, ~}" (sort (copy-list syntax)
  60. 'string<)))))
  61. (:tr (:th "Operand") (:th "Syntax"))
  62. (loop for (sym . syntax) in *operand-symbol-table* do
  63. (:tr
  64. (:td (string-downcase (symbol-name sym)))
  65. (:td (format nil "~{~a~^, ~}" (sort (copy-list syntax)
  66. 'string<))))))
  67. (:p "You can input multiple propositions by separating them with"
  68. "commas (,):"
  69. (:br)
  70. (:code "ab,cd"))
  71. (:p "Two operands next to each other is treated as an 'implicit and'"
  72. "(unless this feature is disabled):"
  73. (:br)
  74. (:code (:raw "abc|d = a &and; b &and; c &or; d"))))))
  75. (defmethod get-dependencies ((overlay help-overlay))
  76. (append
  77. (list
  78. (reblocks-lass:make-dependency
  79. '(.help-overlay
  80. :display "none"
  81. :position "fixed"
  82. :top "0px"
  83. :left "0px"
  84. :width "100%"
  85. :height "100%"
  86. :z-index "100" ;; be above EVERYTHING
  87. (|#help-table-wrapper|
  88. :background "#ffffff"
  89. :border-width "2px"
  90. :border-style "solid"
  91. :border-color "black"
  92. :padding "10px"
  93. :width "fit-content"
  94. :height "fit-content"
  95. :position "fixed"
  96. :top "40%"
  97. :left "50%"
  98. :transform translate "-50%" "-50%"
  99. (|#help-header-wrapper|
  100. :margin-bottom "3px"
  101. :position "relative"
  102. (|#help-header|
  103. :font-size "x-large"
  104. :font-weight "bold"
  105. :display "block"
  106. :text-align "center")
  107. (|#help-close-button|
  108. :user-select "none"
  109. :text-decoration-line "underline"
  110. :cursor "pointer"
  111. :position "absolute"
  112. :top "0"
  113. :right "0"))
  114. (table
  115. :border-collapse "collapse"
  116. :border-spacing "0px"
  117. :margin "auto"
  118. ((:or th td)
  119. :padding "3px"
  120. :padding-left "10px"
  121. :padding-right "10px"
  122. :text-align "left"
  123. :border-style "solid"
  124. :border-width "1px"
  125. :border-color "black"))
  126. (code
  127. :padding-left "1em")))))
  128. (call-next-method)))
  129. (defwidget truth-table ()
  130. ((data :initform nil
  131. :accessor truth-table-data)
  132. (format :initform "html"
  133. :accessor truth-table-format)
  134. (pretty-print :initform t
  135. :accessor truth-table-pretty-print)
  136. (latin-truths :initform nil
  137. :accessor truth-table-latin-truths)
  138. (output-visible :initform t
  139. :accessor truth-table-output-visible))
  140. (:documentation "Class to hold the generated table."))
  141. (defmethod truth-table-toggle-output ((table truth-table))
  142. "Toggle the visibility of the output box of TABLE."
  143. (with-slots (output-visible) table
  144. (setf output-visible (not output-visible))))
  145. (defparameter *blank-hash-table* (make-hash-table)
  146. "Blank hash table to pass to make-js-action because of what seems to be a
  147. reblocks bug.")
  148. (defmethod render ((table truth-table))
  149. "Render TABLE."
  150. (with-slots (data format pretty-print latin-truths output-visible) table
  151. (let* ((html-text (convert-truth-table-to-html data
  152. :pretty-print pretty-print
  153. :latin-truths latin-truths))
  154. (other-text
  155. (when output-visible
  156. (if (equal format "html")
  157. html-text
  158. (typeset-table-to-format data format
  159. :pretty-print pretty-print
  160. :latin-truths latin-truths)))))
  161. (when data
  162. (with-html
  163. (:div :class "label" "Output:")
  164. (:span :id "output-span"
  165. ;; there seems to be a bug in reblocks that means you have to pass
  166. ;; the second argument to `make-js-action'
  167. (:button :onclick (make-js-action
  168. (lambda (&key &allow-other-keys)
  169. (truth-table-toggle-output table)
  170. (update table))
  171. :args *blank-hash-table*)
  172. :id "output-expander-button"
  173. (if output-visible
  174. "⏷"
  175. "⏵"))
  176. (if (or pretty-print
  177. (equal format "ascii")
  178. (equal format "unicode"))
  179. (:pre :id "output-area" :hidden (not output-visible)
  180. other-text)
  181. (:code :id "output-area" :hidden (not output-visible)
  182. other-text)))
  183. (:div :class "label" "HTML Preview:"))
  184. (princ html-text reblocks/html:*stream*))))
  185. nil)
  186. (defmethod get-dependencies ((widget truth-table))
  187. (append
  188. (list
  189. (reblocks-lass:make-dependency
  190. '(.truth-table
  191. (.label
  192. :font-size "large"
  193. :font-weight "bold"
  194. :margin-top "5px"
  195. :margin-bottom "5px")
  196. (|#output-span|
  197. :display "flex"
  198. (button :margin-right "10px"))
  199. (|#output-expander-button|
  200. :margin-bottom "auto"
  201. :font-size "xx-large"
  202. :background "none"
  203. :border "none"
  204. :cursor "pointer")
  205. (|#output-area|
  206. :background "lightgrey"
  207. :flex-grow "1"
  208. :padding "5px"
  209. :border-style "solid"
  210. :border-color "black"
  211. :border-width "1px"
  212. :max-height "25vh"
  213. :overflow-y "scroll")
  214. (table
  215. :margin "auto"
  216. :border-collapse "collapse"
  217. :border-spacing "0px"
  218. ((:or th td)
  219. :padding "3px"
  220. :text-align "center"
  221. :border-style "solid"
  222. :border-width "1px"
  223. :border-color "black")))))
  224. (call-next-method)))
  225. (defwidget error-box ()
  226. ((message :initform nil
  227. :accessor error-box-message))
  228. (:documentation "Class to hold various error messages."))
  229. (defmethod render ((box error-box))
  230. "Render BOX."
  231. (with-html
  232. (with-slots (message) box
  233. (when message
  234. (:div
  235. (:pre message))))))
  236. (defmethod get-dependencies ((box error-box))
  237. (append
  238. (list
  239. (reblocks-lass:make-dependency
  240. `(.error-box
  241. (div
  242. :border-width "1px"
  243. :border-style "solid"
  244. :border-color "black"
  245. (pre
  246. :margin "0px"
  247. :padding-top "5px"
  248. :font-size "large"
  249. :border-left-style "solid"
  250. :border-left-color "red"
  251. :border-left-width "10px"
  252. :padding-left "5px")))))
  253. (call-next-method)))
  254. (defwidget page ()
  255. ((table :initform (make-instance 'truth-table)
  256. :accessor page-table)
  257. (error-box :initform (make-instance 'error-box)
  258. :accessor page-error-box)
  259. (help-overlay :initform (make-instance 'help-overlay)
  260. :accessor page-help-overlay))
  261. (:documentation "The root of the whole page"))
  262. (defun parse-and-eval-propositions (input-str &key implicit-and
  263. multi-char-names
  264. include-vars
  265. include-intermediate)
  266. "Parse and then eval all of comma separated props in INPUT-STR."
  267. (let ((prop-start 0))
  268. (handler-case
  269. (loop
  270. for prop-str in (uiop:split-string input-str :separator '(#\,))
  271. for (parsed-exp vars) = (multiple-value-list
  272. (parse-proposition-string
  273. prop-str
  274. :implicit-and implicit-and
  275. :multi-char-names multi-char-names))
  276. when parsed-exp
  277. append vars into all-vars
  278. and
  279. collect parsed-exp into parsed-exps
  280. and
  281. do (incf prop-start (1+ (length prop-str)))
  282. finally
  283. (return (create-combined-truth-table
  284. parsed-exps
  285. (remove-duplicates all-vars :test 'equal
  286. :from-end t)
  287. :include-intermediate include-intermediate
  288. :include-vars include-vars)))
  289. (proposition-parse-error (e)
  290. ;; adjust the position and proposition string
  291. (error 'proposition-parse-error
  292. :message (parse-error-message e)
  293. :proposition input-str
  294. :position (+ (parse-error-position e)
  295. prop-start))))))
  296. (defun blank-prop-string-p (str)
  297. "Return t if STR would produce a blank proposition table."
  298. (not (find-if-not (lambda (c)
  299. (or (eq c #\,)
  300. (whitespace-p c)))
  301. str)))
  302. (defmethod handle-generate-request ((page page)
  303. &key prop-str implicit-and multi-char-names
  304. format include-vars subexps latin pretty)
  305. "Handler for requests to generate truth tables."
  306. (with-slots (table error-box) page
  307. (setf (truth-table-format table) format
  308. (error-box-message error-box) nil)
  309. (if (not (blank-prop-string-p prop-str))
  310. (handler-case
  311. (setf (truth-table-data table)
  312. (parse-and-eval-propositions
  313. prop-str
  314. :implicit-and implicit-and
  315. :multi-char-names multi-char-names
  316. :include-vars include-vars
  317. :include-intermediate subexps)
  318. (truth-table-latin-truths table) latin
  319. (truth-table-pretty-print table) pretty)
  320. ((or proposition-parse-error proposition-eval-error) (e)
  321. (setf (error-box-message error-box) (princ-to-string e))))
  322. (setf (truth-table-data table) nil))
  323. (update table)
  324. (update error-box)))
  325. (defmethod render ((page page))
  326. "Render PAGE."
  327. (with-html
  328. (:body
  329. (with-slots (table error-box help-overlay) page
  330. (render help-overlay)
  331. (:h1 "Truth Table Generator")
  332. (with-html-form (:POST (lambda (&key prop-str implicit-and
  333. multi-char-names format
  334. include-vars subexps latin
  335. pretty
  336. &allow-other-keys)
  337. (handle-generate-request
  338. page :prop-str prop-str
  339. :implicit-and implicit-and
  340. :multi-char-names multi-char-names
  341. :format format
  342. :include-vars include-vars
  343. :subexps subexps
  344. :pretty pretty
  345. :latin latin)))
  346. (:div :id "main-controls-wrapper"
  347. (:input :id "prop-input-field"
  348. :type "text"
  349. :name "prop-str"
  350. :placeholder "Proposition string...")
  351. (:input :id "submit-button"
  352. :type "submit"
  353. :value "Generate")
  354. (:button :id "help-button"
  355. :onclick
  356. "document.querySelector(\".help-overlay\").style.display = \"initial\""
  357. "Help"))
  358. (:div :id "extra-controls-wrapper"
  359. (:input :type "checkbox"
  360. :name "implicit-and"
  361. :checked t)
  362. (:label :for "implicit-and" "Implicit And")
  363. (:input :type "checkbox"
  364. :name "multi-char-names"
  365. :style "margin-left: 10px;")
  366. (:label :for "multi-char-names" "Multi-character Variables")
  367. (:input :type "checkbox"
  368. :name "include-vars"
  369. :checked t
  370. :style "margin-left: 10px;")
  371. (:label :for "include-vars" "Include Variables")
  372. (:input :type "checkbox"
  373. :name "subexps"
  374. :checked t
  375. :style "margin-left: 10px;")
  376. (:label :for "subexps" "Include Sub-expressions")
  377. (:input :type "checkbox"
  378. :name "pretty"
  379. :checked t
  380. :style "margin-left: 10px;")
  381. (:label :for "pretty" "Pretty Print")
  382. (:input :type "checkbox"
  383. :name "latin"
  384. :checked nil
  385. :style "margin-left: 10px;")
  386. (:label :for "latin" "Latin Truth Values")
  387. (:select :name "format" :style "margin-left: 10px;"
  388. (:option :value "html" "HTML")
  389. (:option :value "latex" "LaTeX")
  390. (:option :value "ascii" "ASCII")
  391. (:option :value "unicode" "Unicode"))))
  392. (render error-box)
  393. (render table)
  394. (:div :id "info-text"
  395. (:span "This website is free software under the terms of the AGPL"
  396. "license version 3. You can find a copy of the license ")
  397. (:a :href "https://www.gnu.org/licenses/agpl-3.0.html"
  398. "here")
  399. (:span ". You can find the source of this website ")
  400. (:a :href "https://git.zander.im/Zander671/truth-table"
  401. "here")
  402. (:span "."))))))
  403. (defmethod get-dependencies ((page page))
  404. (append
  405. (list
  406. (reblocks-lass:make-dependency
  407. '(.page
  408. :width "70%"
  409. :margin "auto"
  410. (h1 :text-align "center")
  411. (form
  412. :margin-bottom "5px"
  413. (|#main-controls-wrapper|
  414. :display flex
  415. :margin-bottom "5px"
  416. (|#prop-input-field|
  417. :flex-grow "1"
  418. :margin-right "5px"
  419. :font-size "large")
  420. ((:or |#submit-button| |#help-button|)
  421. :font-size "large")
  422. (|#help-button|
  423. :margin-left "5px"))
  424. (|#extra-controls-wrapper|
  425. :display "flex"
  426. :justify-content "center"
  427. :align-items "center"))
  428. (|#info-text|
  429. :text-align "center"
  430. :margin-top "10px"
  431. :font-size "small"))))
  432. (call-next-method)))
  433. (defapp truth-table-app
  434. :prefix *default-prefix*
  435. :name "Truth Table Generator")
  436. (defmethod reblocks/page:init-page ((app truth-table-app) (url-path string)
  437. expire-at)
  438. "Main entry point for webpage."
  439. (declare (ignorable app url-path expire-at))
  440. (setf (reblocks/page:get-title) "Truth Table Generator")
  441. (make-instance 'page))
  442. (defparameter *command-line-spec*
  443. `((#\h "help" help nil "print this message, then exit")
  444. (#\d "debug" debug nil "enable debug output")
  445. (#\p "port" port t
  446. ,(format nil "specify port to use (default: ~d)" *default-port*))
  447. (#\a "address" address t
  448. ,(format nil "specify address to bind to (default: ~a)"
  449. *default-address*)))
  450. "Spec for use in `parse-command-line.")
  451. (defun determine-port (opts)
  452. "Get port from the command line option array OPTS, or use a default if port
  453. was not specified."
  454. (let ((raw-value (option-value 'port opts)))
  455. (if raw-value
  456. (handler-case
  457. (let ((value (parse-integer raw-value :junk-allowed nil)))
  458. (if (< value 1)
  459. (error 'parse-error)
  460. value))
  461. (parse-error ()
  462. (cerror "Use *default-port*" 'command-line-error
  463. :message (format nil "invalid port: ~a" raw-value))
  464. *default-port*))
  465. *default-port*)))
  466. (defun main (argv)
  467. "The main entry point to the program. ARGV is the list of command line
  468. arguments."
  469. (let ((cmdline-error nil))
  470. (handler-bind
  471. ((command-line-error
  472. (lambda (c)
  473. (format *error-output* "~a~%" c)
  474. (setq cmdline-error t)
  475. (continue))))
  476. (destructuring-bind ((&rest norm-args) &rest opts)
  477. (parse-command-line *command-line-spec* argv)
  478. ;; parse the options here so that continue still exits properly
  479. (let ((port (determine-port opts))
  480. (address (or (option-value 'address opts) *default-address*)))
  481. (when norm-args
  482. (cerror "Ignore the extra arguments." 'command-line-error
  483. :message "extra non-option arguments"))
  484. (when (option-value 'help opts)
  485. (print-usage t *command-line-spec* "truth-table-web-wrapper"
  486. :print-astrisk nil)
  487. (if cmdline-error
  488. (uiop:quit 1)
  489. (uiop:quit 0)))
  490. (when cmdline-error
  491. (uiop:quit 1))
  492. (reblocks/server:start :apps '(truth-table-app)
  493. :port port
  494. :interface address
  495. :debug (option-value 'debug opts)
  496. :disable-welcome-app t))))))
  497. (defun toplevel ()
  498. "Top-level function to be passed to `save-lisp-and-die'."
  499. #+sbcl (sb-ext:disable-debugger)
  500. (main (uiop:command-line-arguments)))