340 lines
13 KiB
Common Lisp
340 lines
13 KiB
Common Lisp
;; typeset.lisp -- Typeset proposition 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/>.
|
||
(in-package :truth-table/base)
|
||
|
||
(define-condition table-format-error (error)
|
||
((format :initarg :format
|
||
:accessor table-format-error-format))
|
||
(:report
|
||
(lambda (con stream)
|
||
(format stream "unknown table format: ~a"
|
||
(table-format-error-format con)))))
|
||
|
||
(defparameter *operator-ascii-lookup-alist*
|
||
'((and . "&")
|
||
(nand . "nand")
|
||
(or . "|")
|
||
(nor . "nor")
|
||
(xor . "^")
|
||
(not . "~")
|
||
(implies . "->")
|
||
(converse . "<-")
|
||
(iff . "<->")
|
||
(open-paren . "(")
|
||
(close-paren . ")")
|
||
(true . "T")
|
||
(false . "F"))
|
||
"Lookup table mapping operators to their ASCII representation.")
|
||
|
||
(defparameter *operator-unicode-lookup-alist*
|
||
'((and . "∧")
|
||
(nand . "⊼")
|
||
(or . "∨")
|
||
(nor . "⊽")
|
||
(xor . "⊕")
|
||
(not . "¬")
|
||
(implies . "→")
|
||
(converse . "←")
|
||
(iff . "↔")
|
||
(open-paren . "(")
|
||
(close-paren . ")")
|
||
(true . "⊤")
|
||
(false . "⊥"))
|
||
"Lookup table mapping operators to their Unicode representation.")
|
||
|
||
(defparameter *operator-latex-lookup-alist*
|
||
'((and . "\\land")
|
||
(nand . "\\uparrow")
|
||
(or . "\\lor")
|
||
(nor . "\\downarrow")
|
||
(xor . "\\oplus")
|
||
(not . "\\lnot ")
|
||
(implies . "\\to")
|
||
(converse . "\\gets")
|
||
(iff . "\\leftrightarrow")
|
||
(open-paren . "\\left(")
|
||
(close-paren . "\\right)")
|
||
(true . "\\top")
|
||
(false . "\\bot"))
|
||
"Lookup table mapping operators to their LaTeX representation.")
|
||
|
||
(defparameter *operator-html-lookup-alist*
|
||
'((and . "∧")
|
||
(nand . "↑")
|
||
(or . "∨")
|
||
(nor . "↓")
|
||
(xor . "⊕")
|
||
(not . "¬")
|
||
(implies . "→")
|
||
(converse . "←")
|
||
(iff . "↔")
|
||
(open-paren . "(")
|
||
(close-paren . ")")
|
||
(true . "⊤")
|
||
(false . "⊥"))
|
||
"Lookup table mapping operators to their HTML representation.")
|
||
|
||
(defun latex-var-name-transform (name)
|
||
"Transform NAME so that it is escaped for use in LaTeX."
|
||
(format nil "{~{~A~}}" (loop for char across name
|
||
if (eq char #\\)
|
||
collect "\\backslash "
|
||
else if (eq char #\_)
|
||
collect "\\_"
|
||
else if (eq char #\$)
|
||
collect "\\$"
|
||
else
|
||
collect char)))
|
||
|
||
(defun html-var-name-transform (name)
|
||
"Transform NAME so that it is escaped for use in HTML."
|
||
(format nil "~{~A~}" (loop for char across name
|
||
if (eq char #\<)
|
||
collect "<"
|
||
else if (eq char #\>)
|
||
collect ">"
|
||
else
|
||
collect char)))
|
||
|
||
(defun typeset-proposition (expr &key
|
||
(lookup-table *operator-ascii-lookup-alist*)
|
||
var-name-transform
|
||
(parent-prec most-positive-fixnum))
|
||
"Typeset the propositional expression EXPR to plain text. LOOKUP-TABLE should
|
||
be a table mapping operators to their textual representation. VAR-NAME-TRANSFORM
|
||
(if non-nil) should take a single string argument which is a variable name and
|
||
escape it for use in the target typesetting system. PARENT-PERC is for internal
|
||
use (it controls when parentheses are applied.)"
|
||
(cond
|
||
;; expr is empty
|
||
((null expr)
|
||
"")
|
||
;; expr is a variable name
|
||
((stringp expr)
|
||
(if var-name-transform
|
||
(funcall var-name-transform expr)
|
||
expr))
|
||
;; expr is true or false
|
||
((or (eq expr 'true)
|
||
(eq expr 'false))
|
||
(cdr (assoc expr lookup-table)))
|
||
;; expr is a compound expression
|
||
(t
|
||
(destructuring-bind (oper first-arg &rest args) expr
|
||
(let* ((our-prec (operator-precedence oper))
|
||
(oper-ascii (cdr (assoc oper lookup-table)))
|
||
(prefix-suffix (if (< parent-prec our-prec)
|
||
(cons (cdr (assoc 'open-paren lookup-table))
|
||
(cdr (assoc 'close-paren lookup-table)))
|
||
'("" . ""))))
|
||
(if (null args)
|
||
;; we have one argument
|
||
(format nil "~A~A~A~A" (car prefix-suffix) oper-ascii
|
||
(typeset-proposition first-arg
|
||
:lookup-table lookup-table
|
||
:var-name-transform var-name-transform
|
||
:parent-prec our-prec)
|
||
(cdr prefix-suffix))
|
||
;; we have many arguments
|
||
(loop for arg in args
|
||
collect oper-ascii into output
|
||
collect
|
||
(typeset-proposition arg
|
||
:lookup-table lookup-table
|
||
:var-name-transform var-name-transform
|
||
:parent-prec our-prec)
|
||
into output
|
||
finally
|
||
(push (typeset-proposition first-arg
|
||
:lookup-table lookup-table
|
||
:var-name-transform var-name-transform
|
||
:parent-prec our-prec)
|
||
output)
|
||
(return (format nil "~A~{~A~^ ~}~A" (car prefix-suffix)
|
||
output (cdr prefix-suffix))))))))))
|
||
|
||
|
||
(defun convert-truth-table-to-latex (table)
|
||
"Convert TABLE, which should be a truth table as returned by
|
||
`create-truth-table' to latex.
|
||
NOTE: though the overall order does not matter, the order must be the same
|
||
between each row."
|
||
(let ((typeset-exprs (mapcar (lambda (expr)
|
||
(typeset-proposition
|
||
expr :lookup-table *operator-latex-lookup-alist*
|
||
:var-name-transform 'latex-var-name-transform))
|
||
(extract-truth-table-expressions table))))
|
||
(format nil "~
|
||
\\begin{tabular}{~{~*|c~}|}~
|
||
\\hline~
|
||
~{ $ ~A $~^ &~} \\\\ ~
|
||
\\hline~
|
||
~{~{ $ ~:[\\bot~;\\top~] $~^ &~} \\\\ ~}~
|
||
\\hline~
|
||
\\end{tabular}"
|
||
typeset-exprs
|
||
typeset-exprs
|
||
(extract-truth-table-values table))))
|
||
|
||
(defun format-html-properties-alist (props)
|
||
"Format PROPS, a list of conses, as a list of HTML properties."
|
||
(loop for (name . value) in props
|
||
when (eq value t)
|
||
collect (format nil "~A=\"\"" name)
|
||
else when value
|
||
collect (format nil "~A=~S" name (princ-to-string value))))
|
||
|
||
|
||
(defun convert-truth-table-to-html (table &key class id more-props)
|
||
"Convert TABLE, which should be a truth table as returned by
|
||
`create-truth-table' to HTML. CLASS and ID are their respective HTML
|
||
properties. MORE-PROPS is an alist mapping properties to values.
|
||
NOTE: though the overall order does not matter, the order must be the same
|
||
between each row."
|
||
(let ((typeset-exprs (mapcar (lambda (expr)
|
||
(typeset-proposition
|
||
expr :lookup-table *operator-html-lookup-alist*
|
||
:var-name-transform 'html-var-name-transform))
|
||
(extract-truth-table-expressions table))))
|
||
(format nil "~
|
||
<table~@[ class=~s~]~@[ id=~s~]~{ ~A~}>~
|
||
<tr>~{<th>~A</th>~}</tr>~
|
||
~{<tr>~{<td>~:[⊥~;⊤~]</td>~}</tr>~}~
|
||
</table>"
|
||
class id (format-html-properties-alist more-props)
|
||
typeset-exprs
|
||
(extract-truth-table-values table))))
|
||
|
||
(defparameter *table-border-ascii-alist*
|
||
'((vertical . #\|)
|
||
(horizontal . #\-)
|
||
(right . #\|)
|
||
(left . #\|)
|
||
(up . #\-)
|
||
(down . #\-)
|
||
(cross . #\+)
|
||
(top-left . #\+)
|
||
(top-right . #\+)
|
||
(bottom-left . #\+)
|
||
(bottom-right . #\+))
|
||
"Characters used to draw ASCII table borders.")
|
||
|
||
(defparameter *table-border-unicode-alist*
|
||
'((vertical . #\│)
|
||
(horizontal . #\─)
|
||
(right . #\├)
|
||
(left . #\┤)
|
||
(up . #\┴)
|
||
(down . #\┬)
|
||
(cross . #\┼)
|
||
(top-left . #\┌)
|
||
(top-right . #\┐)
|
||
(bottom-left . #\└)
|
||
(bottom-right . #\┘))
|
||
"Characters used to draw Unicode table borders.")
|
||
|
||
(defun typeset-table-break (stream lengths horiz start column end)
|
||
"Typeset the first row, the last row, or a break to STREAM. The proper box
|
||
character will be placed at each intersection. LENGTHS is a list of column
|
||
lengths. HORIZ, START, COLUMN, and END are the box characters to use when
|
||
drawing."
|
||
(format stream "~c" start)
|
||
(loop for (length . rest) = lengths then rest
|
||
while length
|
||
do
|
||
(format stream "~a"
|
||
(make-string length :initial-element horiz))
|
||
when rest do
|
||
(format stream "~c" column))
|
||
(format stream "~c" end))
|
||
|
||
(defun typeset-table-row (stream lengths row vert)
|
||
"Typeset ROW to STREAM. VERT is the vertical separator. LENGTHS should be the
|
||
length of each column."
|
||
(loop for col in row
|
||
for length in lengths
|
||
do
|
||
(format stream "~c~v:@<~a~>" vert length col))
|
||
(format stream "~c" vert))
|
||
|
||
(defun typeset-truth-table (table &optional
|
||
(expr-lookup-table
|
||
*operator-ascii-lookup-alist*)
|
||
(box-lookup-table
|
||
*table-border-ascii-alist*))
|
||
"Convert TABLE, which should be a truth table as returned by
|
||
`create-truth-table' to text.
|
||
NOTE: though the overall order does not matter, the order must be the same
|
||
between each row."
|
||
(let* ((typeset-exprs (mapcar (lambda (expr)
|
||
(typeset-proposition
|
||
expr :lookup-table expr-lookup-table))
|
||
(extract-truth-table-expressions table)))
|
||
(col-widths (mapcar (lambda (expr)
|
||
(+ (length expr) 2))
|
||
typeset-exprs)))
|
||
(with-output-to-string (str)
|
||
(typeset-table-break str col-widths
|
||
(cdr (assoc 'horizontal box-lookup-table))
|
||
(cdr (assoc 'top-left box-lookup-table))
|
||
(cdr (assoc 'down box-lookup-table))
|
||
(cdr (assoc 'top-right box-lookup-table)))
|
||
(terpri str)
|
||
(typeset-table-row str col-widths typeset-exprs
|
||
(cdr (assoc 'vertical box-lookup-table)))
|
||
(terpri str)
|
||
(typeset-table-break str col-widths
|
||
(cdr (assoc 'horizontal box-lookup-table))
|
||
(cdr (assoc 'right box-lookup-table))
|
||
(cdr (assoc 'cross box-lookup-table))
|
||
(cdr (assoc 'left box-lookup-table)))
|
||
(terpri str)
|
||
(dolist (row (extract-truth-table-values table))
|
||
(typeset-table-row str col-widths
|
||
;; convert t or nil to strings
|
||
(mapcar (lambda (entry)
|
||
(cdr (assoc (if entry
|
||
'true
|
||
'false)
|
||
expr-lookup-table)))
|
||
row)
|
||
(cdr (assoc 'vertical box-lookup-table)))
|
||
(terpri str))
|
||
(typeset-table-break str col-widths
|
||
(cdr (assoc 'horizontal box-lookup-table))
|
||
(cdr (assoc 'bottom-left box-lookup-table))
|
||
(cdr (assoc 'up box-lookup-table))
|
||
(cdr (assoc 'bottom-right box-lookup-table))))))
|
||
|
||
(defparameter *known-formats*
|
||
'("unicode" "ascii" "latex" "html")
|
||
"The known formats that `typeset-table-to-format' can take.")
|
||
|
||
(defun typeset-table-to-format (table format)
|
||
"Typeset TABLE into FORMAT, or error if FORMAT is not a know format."
|
||
(cond
|
||
((equal format "unicode")
|
||
(typeset-truth-table table *operator-unicode-lookup-alist*
|
||
*table-border-unicode-alist*))
|
||
((equal format "ascii")
|
||
(typeset-truth-table table *operator-ascii-lookup-alist*
|
||
*table-border-ascii-alist*))
|
||
((equal format "latex")
|
||
(convert-truth-table-to-latex table))
|
||
((equal format "html")
|
||
(convert-truth-table-to-html table))
|
||
(t (error 'table-format-error :format format))))
|