123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526 |
- ;; web.lisp -- Website to allow users to make truth tables
- ;; Copyright (C) 2024 Alexander Rosenberg
- ;;
- ;; This program is free software: you can redistribute it and/or modify
- ;; it under the terms of the GNU General Public License as published by
- ;; the Free Software Foundation, either version 3 of the License, or
- ;; (at your option) any later version.
- ;;
- ;; This program is distributed in the hope that it will be useful,
- ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
- ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ;; GNU General Public License for more details.
- ;;
- ;; You should have received a copy of the GNU General Public License
- ;; along with this program. If not, see <https://www.gnu.org/licenses/>.
- (defpackage #:truth-table/web
- (:use #:common-lisp #:truth-table/base
- #:truth-table/args)
- (:export #:toplevel #:main)
- (:import-from #:reblocks/app
- #:defapp)
- (:import-from #:reblocks/html
- #:with-html)
- (:import-from #:reblocks/widget
- #:defwidget
- #:update
- #:render)
- (:import-from #:reblocks-ui/form
- #:with-html-form)
- (:import-from #:reblocks/actions
- #:make-js-action)
- (:import-from #:reblocks/dependencies
- #:get-dependencies))
- (in-package :truth-table/web)
- (defparameter *default-port* 8000)
- (defparameter *default-address* "127.0.0.1")
- (defparameter *default-prefix* "/")
- (defwidget help-overlay ()
- ()
- (:documentation "Simple class to handle holding the help overlay."))
- (defmethod render ((overlay help-overlay))
- (with-html
- (:div :id "help-table-wrapper"
- (:div :id "help-header-wrapper"
- (:span :id "help-header" "Help")
- (:span :id "help-close-button"
- :onclick "document.querySelector(\".help-overlay\").style.display = \"none\""
- "Close"))
- (:table ;:style "margin-bottom: 10px;"
- (:tr (:th "Operator") (:th "Syntax"))
- (loop for ((sym (name . nics) desc (examples)) . rest-desc)
- = *operator-descriptions* then rest-desc
- for ((_sym . syntax) . rest-st)
- = *operator-symbol-table* then rest-st
- while sym
- do
- (:tr
- (:td name)
- (:td (format nil "~{~a~^, ~}" (sort (copy-list syntax)
- 'string<)))))
- (:tr (:th "Operand") (:th "Syntax"))
- (loop for (sym . syntax) in *operand-symbol-table* do
- (:tr
- (:td (string-downcase (symbol-name sym)))
- (:td (format nil "~{~a~^, ~}" (sort (copy-list syntax)
- 'string<))))))
- (:p "You can input multiple propositions by separating them with"
- "commas (,):"
- (:br)
- (:code "ab,cd"))
- (:p "Two operands next to each other is treated as an 'implicit and'"
- "(unless this feature is disabled):"
- (:br)
- (:code (:raw "abc|d = a ∧ b ∧ c ∨ d"))))))
- (defmethod get-dependencies ((overlay help-overlay))
- (append
- (list
- (reblocks-lass:make-dependency
- '(.help-overlay
- :display "none"
- :position "fixed"
- :top "0px"
- :left "0px"
- :width "100%"
- :height "100%"
- :z-index "100" ;; be above EVERYTHING
- (|#help-table-wrapper|
- :background "#ffffff"
- :border-width "2px"
- :border-style "solid"
- :border-color "black"
- :padding "10px"
- :width "fit-content"
- :height "fit-content"
- :position "fixed"
- :top "40%"
- :left "50%"
- :transform translate "-50%" "-50%"
- (|#help-header-wrapper|
- :margin-bottom "3px"
- :position "relative"
- (|#help-header|
- :font-size "x-large"
- :font-weight "bold"
- :display "block"
- :text-align "center")
- (|#help-close-button|
- :user-select "none"
- :text-decoration-line "underline"
- :cursor "pointer"
- :position "absolute"
- :top "0"
- :right "0"))
- (table
- :border-collapse "collapse"
- :border-spacing "0px"
- :margin "auto"
- ((:or th td)
- :padding "3px"
- :padding-left "10px"
- :padding-right "10px"
- :text-align "left"
- :border-style "solid"
- :border-width "1px"
- :border-color "black"))
- (code
- :padding-left "1em")))))
- (call-next-method)))
- (defwidget truth-table ()
- ((data :initform nil
- :accessor truth-table-data)
- (format :initform "html"
- :accessor truth-table-format)
- (pretty-print :initform t
- :accessor truth-table-pretty-print)
- (latin-truths :initform nil
- :accessor truth-table-latin-truths)
- (output-visible :initform t
- :accessor truth-table-output-visible))
- (:documentation "Class to hold the generated table."))
- (defmethod truth-table-toggle-output ((table truth-table))
- "Toggle the visibility of the output box of TABLE."
- (with-slots (output-visible) table
- (setf output-visible (not output-visible))))
- (defparameter *blank-hash-table* (make-hash-table)
- "Blank hash table to pass to make-js-action because of what seems to be a
- reblocks bug.")
- (defmethod render ((table truth-table))
- "Render TABLE."
- (with-slots (data format pretty-print latin-truths output-visible) table
- (let* ((html-text (convert-truth-table-to-html data
- :pretty-print pretty-print
- :latin-truths latin-truths))
- (other-text
- (when output-visible
- (if (equal format "html")
- html-text
- (typeset-table-to-format data format
- :pretty-print pretty-print
- :latin-truths latin-truths)))))
- (when data
- (with-html
- (:div :class "label" "Output:")
- (:span :id "output-span"
- ;; there seems to be a bug in reblocks that means you have to pass
- ;; the second argument to `make-js-action'
- (:button :onclick (make-js-action
- (lambda (&key &allow-other-keys)
- (truth-table-toggle-output table)
- (update table))
- :args *blank-hash-table*)
- :id "output-expander-button"
- (if output-visible
- "⏷"
- "⏵"))
- (if (or pretty-print
- (equal format "ascii")
- (equal format "unicode"))
- (:pre :id "output-area" :hidden (not output-visible)
- other-text)
- (:code :id "output-area" :hidden (not output-visible)
- other-text)))
- (:div :class "label" "HTML Preview:"))
- (princ html-text reblocks/html:*stream*))))
- nil)
- (defmethod get-dependencies ((widget truth-table))
- (append
- (list
- (reblocks-lass:make-dependency
- '(.truth-table
- (.label
- :font-size "large"
- :font-weight "bold"
- :margin-top "5px"
- :margin-bottom "5px")
- (|#output-span|
- :display "flex"
- (button :margin-right "10px"))
- (|#output-expander-button|
- :margin-bottom "auto"
- :font-size "xx-large"
- :background "none"
- :border "none"
- :cursor "pointer")
- (|#output-area|
- :background "lightgrey"
- :flex-grow "1"
- :padding "5px"
- :border-style "solid"
- :border-color "black"
- :border-width "1px"
- :max-height "25vh"
- :overflow-y "scroll")
- (table
- :margin "auto"
- :border-collapse "collapse"
- :border-spacing "0px"
- ((:or th td)
- :padding "3px"
- :text-align "center"
- :border-style "solid"
- :border-width "1px"
- :border-color "black")))))
- (call-next-method)))
- (defwidget error-box ()
- ((message :initform nil
- :accessor error-box-message))
- (:documentation "Class to hold various error messages."))
- (defmethod render ((box error-box))
- "Render BOX."
- (with-html
- (with-slots (message) box
- (when message
- (:div
- (:pre message))))))
- (defmethod get-dependencies ((box error-box))
- (append
- (list
- (reblocks-lass:make-dependency
- `(.error-box
- (div
- :border-width "1px"
- :border-style "solid"
- :border-color "black"
- (pre
- :margin "0px"
- :padding-top "5px"
- :font-size "large"
- :border-left-style "solid"
- :border-left-color "red"
- :border-left-width "10px"
- :padding-left "5px")))))
- (call-next-method)))
- (defwidget page ()
- ((table :initform (make-instance 'truth-table)
- :accessor page-table)
- (error-box :initform (make-instance 'error-box)
- :accessor page-error-box)
- (help-overlay :initform (make-instance 'help-overlay)
- :accessor page-help-overlay))
- (:documentation "The root of the whole page"))
- (defun parse-and-eval-propositions (input-str &key implicit-and
- multi-char-names
- include-vars
- include-intermediate)
- "Parse and then eval all of comma separated props in INPUT-STR."
- (let ((prop-start 0))
- (handler-case
- (loop
- for prop-str in (uiop:split-string input-str :separator '(#\,))
- for (parsed-exp vars) = (multiple-value-list
- (parse-proposition-string
- prop-str
- :implicit-and implicit-and
- :multi-char-names multi-char-names))
- when parsed-exp
- append vars into all-vars
- and
- collect parsed-exp into parsed-exps
- and
- do (incf prop-start (1+ (length prop-str)))
- finally
- (return (create-combined-truth-table
- parsed-exps
- (remove-duplicates all-vars :test 'equal
- :from-end t)
- :include-intermediate include-intermediate
- :include-vars include-vars)))
- (proposition-parse-error (e)
- ;; adjust the position and proposition string
- (error 'proposition-parse-error
- :message (parse-error-message e)
- :proposition input-str
- :position (+ (parse-error-position e)
- prop-start))))))
- (defun blank-prop-string-p (str)
- "Return t if STR would produce a blank proposition table."
- (not (find-if-not (lambda (c)
- (or (eq c #\,)
- (whitespace-p c)))
- str)))
- (defmethod handle-generate-request ((page page)
- &key prop-str implicit-and multi-char-names
- format include-vars subexps latin pretty)
- "Handler for requests to generate truth tables."
- (with-slots (table error-box) page
- (setf (truth-table-format table) format
- (error-box-message error-box) nil)
- (if (not (blank-prop-string-p prop-str))
- (handler-case
- (setf (truth-table-data table)
- (parse-and-eval-propositions
- prop-str
- :implicit-and implicit-and
- :multi-char-names multi-char-names
- :include-vars include-vars
- :include-intermediate subexps)
- (truth-table-latin-truths table) latin
- (truth-table-pretty-print table) pretty)
- ((or proposition-parse-error proposition-eval-error) (e)
- (setf (error-box-message error-box) (princ-to-string e))))
- (setf (truth-table-data table) nil))
- (update table)
- (update error-box)))
- (defmethod render ((page page))
- "Render PAGE."
- (with-html
- (:body
- (with-slots (table error-box help-overlay) page
- (render help-overlay)
- (:h1 "Truth Table Generator")
- (with-html-form (:POST (lambda (&key prop-str implicit-and
- multi-char-names format
- include-vars subexps latin
- pretty
- &allow-other-keys)
- (handle-generate-request
- page :prop-str prop-str
- :implicit-and implicit-and
- :multi-char-names multi-char-names
- :format format
- :include-vars include-vars
- :subexps subexps
- :pretty pretty
- :latin latin)))
- (:div :id "main-controls-wrapper"
- (:input :id "prop-input-field"
- :type "text"
- :name "prop-str"
- :placeholder "Proposition string...")
- (:input :id "submit-button"
- :type "submit"
- :value "Generate")
- (:button :id "help-button"
- :onclick
- "document.querySelector(\".help-overlay\").style.display = \"initial\""
- "Help"))
- (:div :id "extra-controls-wrapper"
- (:input :type "checkbox"
- :name "implicit-and"
- :checked t)
- (:label :for "implicit-and" "Implicit And")
- (:input :type "checkbox"
- :name "multi-char-names"
- :style "margin-left: 10px;")
- (:label :for "multi-char-names" "Multi-character Variables")
- (:input :type "checkbox"
- :name "include-vars"
- :checked t
- :style "margin-left: 10px;")
- (:label :for "include-vars" "Include Variables")
- (:input :type "checkbox"
- :name "subexps"
- :checked t
- :style "margin-left: 10px;")
- (:label :for "subexps" "Include Sub-expressions")
- (:input :type "checkbox"
- :name "pretty"
- :checked t
- :style "margin-left: 10px;")
- (:label :for "pretty" "Pretty Print")
- (:input :type "checkbox"
- :name "latin"
- :checked nil
- :style "margin-left: 10px;")
- (:label :for "latin" "Latin Truth Values")
- (:select :name "format" :style "margin-left: 10px;"
- (:option :value "html" "HTML")
- (:option :value "latex" "LaTeX")
- (:option :value "ascii" "ASCII")
- (:option :value "unicode" "Unicode"))))
- (render error-box)
- (render table)
- (:div :id "info-text"
- (:span "This website is free software under the terms of the AGPL"
- "license version 3. You can find a copy of the license ")
- (:a :href "https://www.gnu.org/licenses/agpl-3.0.html"
- "here")
- (:span ". You can find the source of this website ")
- (:a :href "https://git.zander.im/Zander671/truth-table"
- "here")
- (:span "."))))))
- (defmethod get-dependencies ((page page))
- (append
- (list
- (reblocks-lass:make-dependency
- '(.page
- :width "70%"
- :margin "auto"
- (h1 :text-align "center")
- (form
- :margin-bottom "5px"
- (|#main-controls-wrapper|
- :display flex
- :margin-bottom "5px"
- (|#prop-input-field|
- :flex-grow "1"
- :margin-right "5px"
- :font-size "large")
- ((:or |#submit-button| |#help-button|)
- :font-size "large")
- (|#help-button|
- :margin-left "5px"))
- (|#extra-controls-wrapper|
- :display "flex"
- :justify-content "center"
- :align-items "center"))
- (|#info-text|
- :text-align "center"
- :margin-top "10px"
- :font-size "small"))))
- (call-next-method)))
- (defapp truth-table-app
- :prefix *default-prefix*
- :name "Truth Table Generator")
- (defmethod reblocks/page:init-page ((app truth-table-app) (url-path string)
- expire-at)
- "Main entry point for webpage."
- (declare (ignorable app url-path expire-at))
- (setf (reblocks/page:get-title) "Truth Table Generator")
- (make-instance 'page))
- (defparameter *command-line-spec*
- `((#\h "help" help nil "print this message, then exit")
- (#\d "debug" debug nil "enable debug output")
- (#\p "port" port t
- ,(format nil "specify port to use (default: ~d)" *default-port*))
- (#\a "address" address t
- ,(format nil "specify address to bind to (default: ~a)"
- *default-address*)))
- "Spec for use in `parse-command-line.")
- (defun determine-port (opts)
- "Get port from the command line option array OPTS, or use a default if port
- was not specified."
- (let ((raw-value (option-value 'port opts)))
- (if raw-value
- (handler-case
- (let ((value (parse-integer raw-value :junk-allowed nil)))
- (if (< value 1)
- (error 'parse-error)
- value))
- (parse-error ()
- (cerror "Use *default-port*" 'command-line-error
- :message (format nil "invalid port: ~a" raw-value))
- *default-port*))
- *default-port*)))
- (defun main (argv)
- "The main entry point to the program. ARGV is the list of command line
- arguments."
- (let ((cmdline-error nil))
- (handler-bind
- ((command-line-error
- (lambda (c)
- (format *error-output* "~a~%" c)
- (setq cmdline-error t)
- (continue))))
- (destructuring-bind ((&rest norm-args) &rest opts)
- (parse-command-line *command-line-spec* argv)
- ;; parse the options here so that continue still exits properly
- (let ((port (determine-port opts))
- (address (or (option-value 'address opts) *default-address*)))
- (when norm-args
- (cerror "Ignore the extra arguments." 'command-line-error
- :message "extra non-option arguments"))
- (when (option-value 'help opts)
- (print-usage t *command-line-spec* "truth-table-web-wrapper"
- :print-astrisk nil)
- (if cmdline-error
- (uiop:quit 1)
- (uiop:quit 0)))
- (when cmdline-error
- (uiop:quit 1))
- (reblocks/server:start :apps '(truth-table-app)
- :port port
- :interface address
- :debug (option-value 'debug opts)
- :disable-welcome-app t))))))
- (defun toplevel ()
- "Top-level function to be passed to `save-lisp-and-die'."
- #+sbcl (sb-ext:disable-debugger)
- (main (uiop:command-line-arguments)))
|