;; 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 . (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") (latin-true . "T") (latin-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 . "⊥") (latin-true . "T") (latin-false . "F")) "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") (latin-true . "\\textrm{T}") (latin-false . "\\textrm{F}")) "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 . "⊥") (latin-true . "T") (latin-false . "F")) "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 flattenable-p (oper) "Return t if OPER is able to be flattened. That is, it does not care a bout argument ordering." (multiple-value-bind (min max) (operator-argument-count oper) (declare (ignorable min)) ;; currently, all unordered operators take any number of arguments max, and ;; all ordered operators have some max number of arguments (as a ;; proposition: an operator is unordered if and only if it takes any number ;; of arguments) (not max))) (defun flatten-proposition (prop) "Flatten PROP, such that adjacent operators with the same precedence and with no ordering (such as \"and\" or \"or\") are not surrounded by parenthesis when typeset." (if (consp prop) (loop with my-oper = (car prop) for raw-sub-expr in (cdr prop) for sub-expr = (flatten-proposition raw-sub-expr) when (and (flattenable-p my-oper) (consp sub-expr) (eq (car sub-expr) my-oper)) append (cdr sub-expr) into out-args else collect sub-expr into out-args finally (return (cons my-oper out-args))) prop)) (defun typeset-proposition (expr &key (lookup-table *operator-ascii-lookup-alist*) var-name-transform (parent-prec most-positive-fixnum) latin-truths (flatten-prop t)) "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 ((eq expr 'true) (if latin-truths (cdr (assoc 'latin-true lookup-table)) (cdr (assoc 'true lookup-table)))) ((eq expr 'false) (if latin-truths (cdr (assoc 'latin-false lookup-table)) (cdr (assoc 'false lookup-table)))) ;; expr is a compound expression (t (destructuring-bind (oper first-arg &rest args) (if flatten-prop (flatten-proposition expr) 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 :latin-truths latin-truths :parent-prec our-prec :flatten-prop nil) (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 :latin-truths latin-truths :parent-prec our-prec :flatten-prop nil) into output finally (push (typeset-proposition first-arg :lookup-table lookup-table :var-name-transform var-name-transform :latin-truths latin-truths :parent-prec our-prec :flatten-prop nil) output) (return (format nil "~A~{~A~^ ~}~A" (car prefix-suffix) output (cdr prefix-suffix)))))))))) (defun convert-truth-table-to-latex (table &key pretty-print latin-truths) "Convert TABLE, which should be a truth table as returned by `create-truth-table' to latex. If PRETTY-PRINT, add newlines to make the generated code easier to read. 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 :latin-truths latin-truths)) (extract-truth-table-expressions table)))) (with-output-to-string (str) (format str "~ \\begin{tabular}{~{~*|c~}|}~@[~% ~*~]~ \\hline~:[ ~;~% ~]~ ~{$ ~A $~^ & ~} \\\\~:[ ~;~% ~]~ \\hline~:[ ~;~% ~]" typeset-exprs pretty-print pretty-print typeset-exprs pretty-print pretty-print) (let ((format-str (if latin-truths "~{~:[F~;T~]~^ & ~} \\\\~:[ ~;~% ~]" "~{$ ~:[\\bot~;\\top~] $~^ & ~} \\\\~:[ ~;~% ~]"))) (dolist (row (extract-truth-table-values table)) (format str format-str row pretty-print))) (format str "\\hline~@[~%~]\\end{tabular}" pretty-print)))) (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 pretty-print latin-truths) "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." (with-output-to-string (str) (format str "~@[~% ~*~]" class id (format-html-properties-alist more-props) pretty-print) (dolist (expr (extract-truth-table-expressions table)) (format str "~@[~% ~*~]~a" pretty-print (typeset-proposition expr :lookup-table *operator-html-lookup-alist* :var-name-transform 'html-var-name-transform :latin-truths latin-truths))) (format str "~@[~% ~]" pretty-print) (dolist (row (extract-truth-table-values table)) (format str "~@[~% ~*~]~@[~% ~*~]" pretty-print pretty-print) (loop with truth-str = (if latin-truths "~:[F~;T~]" "~:[⊥~;⊤~]") for now = row then (cdr now) for value = (car now) while now do (format str "~?" truth-str (list value)) when (and pretty-print (cdr now)) do (format str "~% ")) (format str "~@[~% ~*~]" pretty-print)) (format str "~@[~%~]" pretty-print))) (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 &key (left-pad-len 0) (right-pad-len 0)) "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 (+ left-pad-len length right-pad-len) :initial-element horiz)) when rest do (format stream "~c" column)) (format stream "~c" end)) (defun typeset-table-row (stream lengths row vert &key (align :center) (left-pad-str "") (right-pad-str "")) "Typeset ROW to STREAM. VERT is the vertical separator. LENGTHS should be the length of each column." (loop with format = (case align (:right "~c~a~v:<~a~>~a") (:left "~c~a~v@<~a~>~a") (t ;; :center "~c~a~v:@<~a~>~a")) for col in row for length in lengths do (format stream format vert left-pad-str length col right-pad-str)) (format stream "~c" vert)) (defmacro with-draw-table ((stream col-widths lookup-table &key (padding 0) (align :center)) &body body) "Execute BODY with the function \=:seperator and \=:row bound. STREAM is the stream to write the table to. COL-WIDTHS is a list of column widths. LOOKUP-TABLE is the table to use to lookup characters for the table border. PADDING is the number to spaces to both append and prepend to each table cell. ALIGN is one of \=:right, \=:center, or \=:left." (let ((pad-str-var (gensym))) `(let ((,pad-str-var (make-string ,padding :initial-element #\space))) (truth-table/base::typeset-table-break ,stream ,col-widths (cdr (assoc 'horizontal ,lookup-table)) (cdr (assoc 'top-left ,lookup-table)) (cdr (assoc 'down ,lookup-table)) (cdr (assoc 'top-right ,lookup-table)) :right-pad-len ,padding :left-pad-len ,padding) (format ,stream "~%") (flet ((:seperator () (truth-table/base::typeset-table-break ,stream ,col-widths (cdr (assoc 'horizontal ,lookup-table)) (cdr (assoc 'right ,lookup-table)) (cdr (assoc 'cross ,lookup-table)) (cdr (assoc 'left ,lookup-table)) :right-pad-len ,padding :left-pad-len ,padding) (format ,stream "~%")) (:row (row) (truth-table/base::typeset-table-row ,stream ,col-widths row (cdr (assoc 'vertical ,lookup-table)) :align ,align :left-pad-str ,pad-str-var :right-pad-str ,pad-str-var) (format ,stream "~%"))) ,@body) (truth-table/base::typeset-table-break ,stream ,col-widths (cdr (assoc 'horizontal ,lookup-table)) (cdr (assoc 'bottom-left ,lookup-table)) (cdr (assoc 'up ,lookup-table)) (cdr (assoc 'bottom-right ,lookup-table)) :right-pad-len ,padding :left-pad-len ,padding)))) (defun typeset-truth-table (table &optional (expr-lookup-table *operator-ascii-lookup-alist*) (box-lookup-table *table-border-ascii-alist*) latin-truths) "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 :latin-truths latin-truths)) (extract-truth-table-expressions table))) (col-widths (mapcar 'length typeset-exprs))) (with-output-to-string (str) (with-draw-table (str col-widths box-lookup-table :padding 1) (:row typeset-exprs) (:seperator) (dolist (row (extract-truth-table-values table)) (:row (mapcar (lambda (entry) (cdr (assoc (if entry (if latin-truths 'latin-true 'true) (if latin-truths 'latin-false 'false)) expr-lookup-table))) row))))))) (defparameter *known-formats* '("unicode" "ascii" "latex" "html") "The known formats that `typeset-table-to-format' can take.") (defun typeset-table-to-format (table format &key pretty-print latin-truths) "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* latin-truths)) ((equal format "ascii") (typeset-truth-table table *operator-ascii-lookup-alist* *table-border-ascii-alist* latin-truths)) ((equal format "latex") (convert-truth-table-to-latex table :pretty-print pretty-print :latin-truths latin-truths)) ((equal format "html") (convert-truth-table-to-html table :pretty-print pretty-print :latin-truths latin-truths)) (t (error 'table-format-error :format format))))