typeset.lisp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. ;; typeset.lisp -- Typeset proposition 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. (in-package :truth-table/base)
  17. (define-condition table-format-error (error)
  18. ((format :initarg :format
  19. :accessor table-format-error-format))
  20. (:report
  21. (lambda (con stream)
  22. (format stream "unknown table format: ~a"
  23. (table-format-error-format con)))))
  24. (defparameter *operator-ascii-lookup-alist*
  25. '((and . "&")
  26. (nand . "nand")
  27. (or . "|")
  28. (nor . "nor")
  29. (xor . "^")
  30. (not . "~")
  31. (implies . "->")
  32. (converse . "<-")
  33. (iff . "<->")
  34. (open-paren . "(")
  35. (close-paren . ")")
  36. (true . "T")
  37. (false . "F")
  38. (latin-true . "T")
  39. (latin-false . "F"))
  40. "Lookup table mapping operators to their ASCII representation.")
  41. (defparameter *operator-unicode-lookup-alist*
  42. '((and . "∧")
  43. (nand . "⊼")
  44. (or . "∨")
  45. (nor . "⊽")
  46. (xor . "⊕")
  47. (not . "¬")
  48. (implies . "→")
  49. (converse . "←")
  50. (iff . "↔")
  51. (open-paren . "(")
  52. (close-paren . ")")
  53. (true . "⊤")
  54. (false . "⊥")
  55. (latin-true . "T")
  56. (latin-false . "F"))
  57. "Lookup table mapping operators to their Unicode representation.")
  58. (defparameter *operator-latex-lookup-alist*
  59. '((and . "\\land")
  60. (nand . "\\uparrow")
  61. (or . "\\lor")
  62. (nor . "\\downarrow")
  63. (xor . "\\oplus")
  64. (not . "\\lnot ")
  65. (implies . "\\to")
  66. (converse . "\\gets")
  67. (iff . "\\leftrightarrow")
  68. (open-paren . "\\left(")
  69. (close-paren . "\\right)")
  70. (true . "\\top")
  71. (false . "\\bot")
  72. (latin-true . "\\textrm{T}")
  73. (latin-false . "\\textrm{F}"))
  74. "Lookup table mapping operators to their LaTeX representation.")
  75. (defparameter *operator-html-lookup-alist*
  76. '((and . "&and;")
  77. (nand . "&uarr;")
  78. (or . "&or;")
  79. (nor . "&darr;")
  80. (xor . "&oplus;")
  81. (not . "&not;")
  82. (implies . "&rarr;")
  83. (converse . "&larr;")
  84. (iff . "&harr;")
  85. (open-paren . "(")
  86. (close-paren . ")")
  87. (true . "&top;")
  88. (false . "&perp;")
  89. (latin-true . "T")
  90. (latin-false . "F"))
  91. "Lookup table mapping operators to their HTML representation.")
  92. (defun latex-var-name-transform (name)
  93. "Transform NAME so that it is escaped for use in LaTeX."
  94. (format nil "{~{~A~}}" (loop for char across name
  95. if (eq char #\\)
  96. collect "\\backslash "
  97. else if (eq char #\_)
  98. collect "\\_"
  99. else if (eq char #\$)
  100. collect "\\$"
  101. else
  102. collect char)))
  103. (defun html-var-name-transform (name)
  104. "Transform NAME so that it is escaped for use in HTML."
  105. (format nil "~{~A~}" (loop for char across name
  106. if (eq char #\<)
  107. collect "&lt;"
  108. else if (eq char #\>)
  109. collect "&gt;"
  110. else
  111. collect char)))
  112. (defun flattenable-p (oper)
  113. "Return t if OPER is able to be flattened. That is, it does not care a bout
  114. argument ordering."
  115. (multiple-value-bind (min max)
  116. (operator-argument-count oper)
  117. (declare (ignorable min))
  118. ;; currently, all unordered operators take any number of arguments max, and
  119. ;; all ordered operators have some max number of arguments (as a
  120. ;; proposition: an operator is unordered if and only if it takes any number
  121. ;; of arguments)
  122. (not max)))
  123. (defun flatten-proposition (prop)
  124. "Flatten PROP, such that adjacent operators with the same precedence and with
  125. no ordering (such as \"and\" or \"or\") are not surrounded by parenthesis when
  126. typeset."
  127. (if (consp prop)
  128. (loop with my-oper = (car prop)
  129. for raw-sub-expr in (cdr prop)
  130. for sub-expr = (flatten-proposition raw-sub-expr)
  131. when (and (flattenable-p my-oper)
  132. (consp sub-expr)
  133. (eq (car sub-expr) my-oper))
  134. append (cdr sub-expr) into out-args
  135. else
  136. collect sub-expr into out-args
  137. finally (return (cons my-oper out-args)))
  138. prop))
  139. (defun typeset-proposition (expr &key
  140. (lookup-table *operator-ascii-lookup-alist*)
  141. var-name-transform
  142. (parent-prec most-positive-fixnum)
  143. latin-truths
  144. (flatten-prop t))
  145. "Typeset the propositional expression EXPR to plain text. LOOKUP-TABLE should
  146. be a table mapping operators to their textual representation. VAR-NAME-TRANSFORM
  147. (if non-nil) should take a single string argument which is a variable name and
  148. escape it for use in the target typesetting system. PARENT-PERC is for internal
  149. use (it controls when parentheses are applied.)"
  150. (cond
  151. ;; expr is empty
  152. ((null expr)
  153. "")
  154. ;; expr is a variable name
  155. ((stringp expr)
  156. (if var-name-transform
  157. (funcall var-name-transform expr)
  158. expr))
  159. ;; expr is true or false
  160. ((eq expr 'true)
  161. (if latin-truths
  162. (cdr (assoc 'latin-true lookup-table))
  163. (cdr (assoc 'true lookup-table))))
  164. ((eq expr 'false)
  165. (if latin-truths
  166. (cdr (assoc 'latin-false lookup-table))
  167. (cdr (assoc 'false lookup-table))))
  168. ;; expr is a compound expression
  169. (t
  170. (destructuring-bind (oper first-arg &rest args)
  171. (if flatten-prop
  172. (flatten-proposition expr)
  173. expr)
  174. (let* ((our-prec (operator-precedence oper))
  175. (oper-ascii (cdr (assoc oper lookup-table)))
  176. (prefix-suffix (if (<= parent-prec our-prec)
  177. (cons (cdr (assoc 'open-paren lookup-table))
  178. (cdr (assoc 'close-paren lookup-table)))
  179. '("" . ""))))
  180. (if (null args)
  181. ;; we have one argument
  182. (format nil "~A~A~A~A" (car prefix-suffix) oper-ascii
  183. (typeset-proposition first-arg
  184. :lookup-table lookup-table
  185. :var-name-transform var-name-transform
  186. :latin-truths latin-truths
  187. :parent-prec our-prec
  188. :flatten-prop nil)
  189. (cdr prefix-suffix))
  190. ;; we have many arguments
  191. (loop for arg in args
  192. collect oper-ascii into output
  193. collect
  194. (typeset-proposition arg
  195. :lookup-table lookup-table
  196. :var-name-transform var-name-transform
  197. :latin-truths latin-truths
  198. :parent-prec our-prec
  199. :flatten-prop nil)
  200. into output
  201. finally
  202. (push (typeset-proposition first-arg
  203. :lookup-table lookup-table
  204. :var-name-transform var-name-transform
  205. :latin-truths latin-truths
  206. :parent-prec our-prec
  207. :flatten-prop nil)
  208. output)
  209. (return (format nil "~A~{~A~^ ~}~A" (car prefix-suffix)
  210. output (cdr prefix-suffix))))))))))
  211. (defun convert-truth-table-to-latex (table &key pretty-print latin-truths)
  212. "Convert TABLE, which should be a truth table as returned by
  213. `create-truth-table' to latex. If PRETTY-PRINT, add newlines to make the
  214. generated code easier to read.
  215. NOTE: though the overall order does not matter, the order must be the same
  216. between each row."
  217. (let ((typeset-exprs (mapcar (lambda (expr)
  218. (typeset-proposition
  219. expr :lookup-table *operator-latex-lookup-alist*
  220. :var-name-transform 'latex-var-name-transform
  221. :latin-truths latin-truths))
  222. (extract-truth-table-expressions table))))
  223. (with-output-to-string (str)
  224. (format str "~
  225. \\begin{tabular}{~{~*|c~}|}~@[~% ~*~]~
  226. \\hline~:[ ~;~% ~]~
  227. ~{$ ~A $~^ & ~} \\\\~:[ ~;~% ~]~
  228. \\hline~:[ ~;~% ~]"
  229. typeset-exprs
  230. pretty-print pretty-print
  231. typeset-exprs
  232. pretty-print pretty-print)
  233. (let ((format-str
  234. (if latin-truths
  235. "~{~:[F~;T~]~^ & ~} \\\\~:[ ~;~% ~]"
  236. "~{$ ~:[\\bot~;\\top~] $~^ & ~} \\\\~:[ ~;~% ~]")))
  237. (dolist (row (extract-truth-table-values table))
  238. (format str format-str row pretty-print)))
  239. (format str "\\hline~@[~%~]\\end{tabular}" pretty-print))))
  240. (defun format-html-properties-alist (props)
  241. "Format PROPS, a list of conses, as a list of HTML properties."
  242. (loop for (name . value) in props
  243. when (eq value t)
  244. collect (format nil "~A=\"\"" name)
  245. else when value
  246. collect (format nil "~A=~S" name (princ-to-string value))))
  247. (defun convert-truth-table-to-html (table &key class id more-props
  248. pretty-print latin-truths)
  249. "Convert TABLE, which should be a truth table as returned by
  250. `create-truth-table' to HTML. CLASS and ID are their respective HTML
  251. properties. MORE-PROPS is an alist mapping properties to values.
  252. NOTE: though the overall order does not matter, the order must be the same
  253. between each row."
  254. (with-output-to-string (str)
  255. (format str "<table~@[ class=~s~]~@[ id=~s~]~{ ~A~}>~@[~% ~*~]<tr>"
  256. class id (format-html-properties-alist more-props)
  257. pretty-print)
  258. (dolist (expr (extract-truth-table-expressions table))
  259. (format str "~@[~% ~*~]<th>~a</th>"
  260. pretty-print
  261. (typeset-proposition
  262. expr :lookup-table *operator-html-lookup-alist*
  263. :var-name-transform 'html-var-name-transform
  264. :latin-truths latin-truths)))
  265. (format str "~@[~% ~]</tr>" pretty-print)
  266. (dolist (row (extract-truth-table-values table))
  267. (format str "~@[~% ~*~]<tr>~@[~% ~*~]" pretty-print pretty-print)
  268. (loop with truth-str = (if latin-truths
  269. "~:[F~;T~]"
  270. "~:[&perp;~;&top;~]")
  271. for now = row then (cdr now)
  272. for value = (car now)
  273. while now do
  274. (format str "<td>~?</td>" truth-str (list value))
  275. when (and pretty-print (cdr now)) do
  276. (format str "~% "))
  277. (format str "~@[~% ~*~]</tr>" pretty-print))
  278. (format str "~@[~%~]</table>" pretty-print)))
  279. (defparameter *table-border-ascii-alist*
  280. '((vertical . #\|)
  281. (horizontal . #\-)
  282. (right . #\|)
  283. (left . #\|)
  284. (up . #\-)
  285. (down . #\-)
  286. (cross . #\+)
  287. (top-left . #\+)
  288. (top-right . #\+)
  289. (bottom-left . #\+)
  290. (bottom-right . #\+))
  291. "Characters used to draw ASCII table borders.")
  292. (defparameter *table-border-unicode-alist*
  293. '((vertical . #\│)
  294. (horizontal . #\─)
  295. (right . #\├)
  296. (left . #\┤)
  297. (up . #\┴)
  298. (down . #\┬)
  299. (cross . #\┼)
  300. (top-left . #\┌)
  301. (top-right . #\┐)
  302. (bottom-left . #\└)
  303. (bottom-right . #\┘))
  304. "Characters used to draw Unicode table borders.")
  305. (defun typeset-table-break (stream lengths horiz start column end
  306. &key (left-pad-len 0) (right-pad-len 0))
  307. "Typeset the first row, the last row, or a break to STREAM. The proper box
  308. character will be placed at each intersection. LENGTHS is a list of column
  309. lengths. HORIZ, START, COLUMN, and END are the box characters to use when
  310. drawing."
  311. (format stream "~c" start)
  312. (loop for (length . rest) = lengths then rest
  313. while length
  314. do
  315. (format stream "~a"
  316. (make-string (+ left-pad-len length right-pad-len)
  317. :initial-element horiz))
  318. when rest do
  319. (format stream "~c" column))
  320. (format stream "~c" end))
  321. (defun typeset-table-row (stream lengths row vert
  322. &key (align :center) (left-pad-str "")
  323. (right-pad-str ""))
  324. "Typeset ROW to STREAM. VERT is the vertical separator. LENGTHS should be the
  325. length of each column."
  326. (loop with format = (case align
  327. (:right
  328. "~c~a~v:<~a~>~a")
  329. (:left
  330. "~c~a~v@<~a~>~a")
  331. (t ;; :center
  332. "~c~a~v:@<~a~>~a"))
  333. for col in row
  334. for length in lengths
  335. do
  336. (format stream format
  337. vert left-pad-str length col right-pad-str))
  338. (format stream "~c" vert))
  339. (defmacro with-draw-table ((stream col-widths lookup-table
  340. &key (padding 0) (align :center))
  341. &body body)
  342. "Execute BODY with the function \=:seperator and \=:row bound. STREAM is the
  343. stream to write the table to. COL-WIDTHS is a list of column
  344. widths. LOOKUP-TABLE is the table to use to lookup characters for the table
  345. border. PADDING is the number to spaces to both append and prepend to each table
  346. cell. ALIGN is one of \=:right, \=:center, or \=:left."
  347. (let ((pad-str-var (gensym)))
  348. `(let ((,pad-str-var (make-string ,padding :initial-element #\space)))
  349. (truth-table/base::typeset-table-break
  350. ,stream ,col-widths
  351. (cdr (assoc 'horizontal ,lookup-table))
  352. (cdr (assoc 'top-left ,lookup-table))
  353. (cdr (assoc 'down ,lookup-table))
  354. (cdr (assoc 'top-right ,lookup-table))
  355. :right-pad-len ,padding
  356. :left-pad-len ,padding)
  357. (format ,stream "~%")
  358. (flet ((:seperator ()
  359. (truth-table/base::typeset-table-break
  360. ,stream ,col-widths
  361. (cdr (assoc 'horizontal ,lookup-table))
  362. (cdr (assoc 'right ,lookup-table))
  363. (cdr (assoc 'cross ,lookup-table))
  364. (cdr (assoc 'left ,lookup-table))
  365. :right-pad-len ,padding
  366. :left-pad-len ,padding)
  367. (format ,stream "~%"))
  368. (:row (row)
  369. (truth-table/base::typeset-table-row
  370. ,stream ,col-widths row
  371. (cdr (assoc 'vertical ,lookup-table))
  372. :align ,align
  373. :left-pad-str ,pad-str-var
  374. :right-pad-str ,pad-str-var)
  375. (format ,stream "~%")))
  376. ,@body)
  377. (truth-table/base::typeset-table-break
  378. ,stream ,col-widths
  379. (cdr (assoc 'horizontal ,lookup-table))
  380. (cdr (assoc 'bottom-left ,lookup-table))
  381. (cdr (assoc 'up ,lookup-table))
  382. (cdr (assoc 'bottom-right ,lookup-table))
  383. :right-pad-len ,padding
  384. :left-pad-len ,padding))))
  385. (defun typeset-truth-table (table &optional
  386. (expr-lookup-table
  387. *operator-ascii-lookup-alist*)
  388. (box-lookup-table
  389. *table-border-ascii-alist*)
  390. latin-truths)
  391. "Convert TABLE, which should be a truth table as returned by
  392. `create-truth-table' to text.
  393. NOTE: though the overall order does not matter, the order must be the same
  394. between each row."
  395. (let* ((typeset-exprs (mapcar (lambda (expr)
  396. (typeset-proposition
  397. expr :lookup-table expr-lookup-table
  398. :latin-truths latin-truths))
  399. (extract-truth-table-expressions table)))
  400. (col-widths (mapcar 'length typeset-exprs)))
  401. (with-output-to-string (str)
  402. (with-draw-table (str col-widths box-lookup-table
  403. :padding 1)
  404. (:row typeset-exprs)
  405. (:seperator)
  406. (dolist (row (extract-truth-table-values table))
  407. (:row (mapcar
  408. (lambda (entry)
  409. (cdr (assoc
  410. (if entry
  411. (if latin-truths
  412. 'latin-true
  413. 'true)
  414. (if latin-truths
  415. 'latin-false
  416. 'false))
  417. expr-lookup-table)))
  418. row)))))))
  419. (defparameter *known-formats*
  420. '("unicode" "ascii" "latex" "html")
  421. "The known formats that `typeset-table-to-format' can take.")
  422. (defun typeset-table-to-format (table format
  423. &key pretty-print latin-truths)
  424. "Typeset TABLE into FORMAT, or error if FORMAT is not a know format."
  425. (cond
  426. ((equal format "unicode")
  427. (typeset-truth-table table *operator-unicode-lookup-alist*
  428. *table-border-unicode-alist*
  429. latin-truths))
  430. ((equal format "ascii")
  431. (typeset-truth-table table *operator-ascii-lookup-alist*
  432. *table-border-ascii-alist*
  433. latin-truths))
  434. ((equal format "latex")
  435. (convert-truth-table-to-latex table
  436. :pretty-print pretty-print
  437. :latin-truths latin-truths))
  438. ((equal format "html")
  439. (convert-truth-table-to-html table
  440. :pretty-print pretty-print
  441. :latin-truths latin-truths))
  442. (t (error 'table-format-error :format format))))