526 lines
19 KiB
Common Lisp
526 lines
19 KiB
Common Lisp
;; 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)))
|