s-dot2.lisp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. ;;;;;
  2. ;;;;; File s-dot.lisp
  3. ;;;;;
  4. ;;;;; A Common Lisp language binding for the graphviz 'dot' tool.
  5. ;;;;; See http://www.martin-loetzsch.de/S-DOT for details
  6. ;;;;;
  7. ;;;;; Copyright (c) 2006-2010 Martin Loetzsch. All rights reserved.
  8. ;;;;;
  9. ;;;;; Redistribution and use in source and binary forms, with or without
  10. ;;;;; modification, are permitted provided that the following conditions
  11. ;;;;; are met:
  12. ;;;;;
  13. ;;;;; 1. Redistributions of source code must retain the above copyright
  14. ;;;;; notice, this list of conditions and the following disclaimer.
  15. ;;;;;
  16. ;;;;; 2. Redistributions in binary form must reproduce the above
  17. ;;;;; copyright notice, this list of conditions and the following
  18. ;;;;; disclaimer in the documentation and/or other materials provided
  19. ;;;;; with the distribution.
  20. ;;;;;
  21. ;;;;; 3. The end-user documentation included with the redistribution, if
  22. ;;;;; any, must include the following acknowledgment:
  23. ;;;;; "This product includes S-DOT developed by Martin Loetzsch
  24. ;;;;; (http://www.martin-loetzsch.de/S-DOT)."
  25. ;;;;; Alternately, this acknowledgment may appear in the software
  26. ;;;;; itself, if and wherever such third-party acknowledgments
  27. ;;;;; normally appear.
  28. ;;;;;
  29. ;;;;; THIS SOFTWARE IS PROVIDED BY MARTIN LOETZSCH ``AS IS'' AND ANY
  30. ;;;;; EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  31. ;;;;; IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  32. ;;;;; PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL MARTIN LOETZSCH BE LIABLE
  33. ;;;;; FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  34. ;;;;; CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
  35. ;;;;; OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
  36. ;;;;; BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  37. ;;;;; LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  38. ;;;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  39. ;;;;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  40. (defpackage :s-dot
  41. (:use :common-lisp
  42. :uiop)
  43. (:documentation "See http://martin-loetzsch.de/S-DOT")
  44. (:export :check-syntax
  45. :s-dot->dot
  46. :*dot-exe*
  47. :render-s-dot
  48. :render-s-dot-sbcl))
  49. (in-package :s-dot)
  50. (defparameter *dot-exe* "dot"
  51. "the path (or name) name of the dot executable")
  52. (defparameter *dot-output-format-switch* "-T"
  53. "the command line option for dot needed to specify the output format")
  54. ;; the pure syntax of s-dot.
  55. ;; http://martin-loetzsch.de/S-DOT contains the complete documentation
  56. (defparameter s-dot-syntax
  57. `((:graph (:cluster :sub-graph :node :record :edge) ;; children
  58. nil ;; required attributes
  59. (:bgcolor :fontcolor :fontname ;; optional attributes, including the required ones
  60. :fontsize :label :margin :nodesep :rankdir :ranksep :ratio :size))
  61. (:cluster (:cluster :sub-graph :node :record :edge)
  62. (:id)
  63. (:bgcolor :color :fillcolor :fontcolor :fontname :fontsize :id :label :labeljust
  64. :labelloc :style))
  65. (:sub-graph (:node :record)
  66. (:rank)
  67. (:rank))
  68. (:node nil
  69. (:id)
  70. (:color :fillcolor :fixedsize :fontcolor :fontname :fontsize :height :id
  71. :label :shape :style :width))
  72. (:record (:record :node)
  73. nil
  74. (:color :fillcolor :fixedsize :fontcolor :fontname :fontsize :height
  75. :label :shape :style :width))
  76. (:edge nil
  77. (:from :to)
  78. (:arrowhead :arrowsize :arrowtail :color :constraint :decorate :dir
  79. :fontcolor :fontname :fontsize :from :headlabel :headport
  80. :label :labeldistance :labelfloat :labelfontcolor
  81. :labelfontname :labelfontsize :lhead :ltail :minlen :style
  82. :samehead :sametail :taillabel :tailport :to))))
  83. (defun check-syntax (graph &key (level 0))
  84. "Checks whether the s-expression 'graph' is syntactically correct.
  85. Note that it does not check whether the attributes contain useful values."
  86. (unless (and graph (listp graph)) (error "graph should be a list"))
  87. (let* ((element (first graph))
  88. (element-spec (find element s-dot-syntax :key #'first)))
  89. (when (and (= level 0) (not (equal element :graph)))
  90. (error "an s-dot expression should start with ~s. Instead, ~s was passed"
  91. :graph element))
  92. (unless (listp (second graph)) (error "~s should be a list of parameters, context: ~a"
  93. (second graph) graph))
  94. (loop for attribute in (second graph)
  95. do (when (or (not (listp attribute)) (not (= (length attribute) 2))
  96. (not (stringp (second attribute))))
  97. (error "~s attribute ~s should be of the form (attribute \"value\"), context: "
  98. element attribute))
  99. (unless (find (first attribute) (fourth element-spec))
  100. (error "attribute ~s is not allowed in ~s elements, context: ~s"
  101. (first attribute) element graph)))
  102. (loop for required-attribute in (third element-spec)
  103. do (unless (find required-attribute (second graph) :key #'first)
  104. (error "required attribute ~s not found in element ~s, context: ~a"
  105. required-attribute element graph)))
  106. (loop for child in (cddr graph)
  107. do (unless (listp child)
  108. (error "child ~s of element ~s should be a alist, context: ~a" child element graph))
  109. (unless (symbolp (first child))
  110. (error "~s should be the name of an element, context: ~a" (first child) child))
  111. (unless (find (first child) s-dot-syntax :key #'first)
  112. (error "element ~s not known, context: ~a" (first child) child))
  113. (unless (find (first child) (second element-spec))
  114. (error "element ~s not allowed inside ~s, context: ~a"
  115. (first child) element child))
  116. (check-syntax child :level (+ 1 level)))))
  117. (defun escape-record-label (raw &key (escape-bag '(#\| #\{ #\} #\< #\> #\Space)))
  118. (if (string= raw "")
  119. ""
  120. (let ((examinated (elt raw 0)))
  121. (concatenate 'string
  122. (if (find examinated escape-bag :test #'char=)
  123. (format nil "\\~a" examinated)
  124. (string examinated))
  125. (escape-record-label (subseq raw 1))))))
  126. (defun s-dot->dot (stream graph &key (check-syntax t))
  127. "Generates dot syntax from a s-dot expression and writes the result to 'stream'.
  128. This code looks indeed ugly. If you really want to understand what's going on, then it might
  129. be easier to look in the the XSLT stylesheet at
  130. http://www.martin-loetzsch.de/DOTML/dotml-1.3/dotml2dot.xsl . This code does exactly the same."
  131. (let ((sub-graph-counter 0)
  132. (records nil))
  133. (labels ((attribute-value (attribute element)
  134. (second (find attribute (second element) :key #'first)))
  135. (generate-record-label (element)
  136. (if (equal (first element) :record)
  137. (format nil "{~{~a~^ | ~}}" (mapcar #'generate-record-label (cddr element)))
  138. (format nil "<~a> ~a" (attribute-value :id element)
  139. (if (attribute-value :label element)
  140. (escape-record-label (attribute-value :label element))
  141. (attribute-value :id element)))))
  142. (write-attributes (element separator exclude-attributes)
  143. (let ((element-spec (find (first element) s-dot-syntax :key #'first)))
  144. (loop for attribute in (fourth element-spec)
  145. for attribute-value = (attribute-value attribute element)
  146. do (unless (find attribute exclude-attributes)
  147. (format stream "~a=\"~a\"~a"
  148. (string-downcase (symbol-name attribute))
  149. (if attribute-value attribute-value "")
  150. separator)))))
  151. (write-element
  152. (element)
  153. (macrolet ((write-children ()
  154. `(loop for e in (cddr element) do (write-element e))))
  155. (cond ((equal (first element) :sub-graph)
  156. (format stream "subgraph sub_graph_~a{rank=\"~a\";"
  157. (incf sub-graph-counter) (attribute-value :rank element))
  158. (write-children)
  159. (format stream "}"))
  160. ((equal (first element) :cluster)
  161. (format stream "subgraph cluster_~a{" (attribute-value :id element))
  162. (write-attributes element ";" '(:id))
  163. (write-children)
  164. (format stream "}"))
  165. ((equal (first element) :node)
  166. (format stream "node[label=\"~a\", "
  167. (if (attribute-value :label element)
  168. (attribute-value :label element)
  169. (attribute-value :id element)))
  170. (write-attributes element "," '(:label :id))
  171. (format stream "] {~s};" (attribute-value :id element)))
  172. ((equal (first element) :edge)
  173. (format stream "edge[")
  174. (write-attributes element "," '(:from :to :lhead :ltail))
  175. (format stream "lhead=\"~a\", ltail=\"~a\"]"
  176. (if (attribute-value :lhead element)
  177. (concatenate 'string "cluster_"
  178. (attribute-value :lhead element)) "")
  179. (if (attribute-value :ltail element)
  180. (concatenate 'string "cluster_"
  181. (attribute-value :ltail element)) ""))
  182. (let ((struct-from (loop for r in records
  183. when (find (attribute-value :from element)
  184. (third r) :test #'equal)
  185. return (second r)))
  186. (struct-to (loop for r in records
  187. when (find (attribute-value :to element)
  188. (third r) :test #'equal)
  189. return (second r))))
  190. (when struct-from (format stream "struct~a:" struct-from))
  191. (format stream "~s -> " (attribute-value :from element))
  192. (when struct-to (format stream "struct~a:" struct-to))
  193. (format stream "~s;" (attribute-value :to element))))
  194. ((equal (first element) :record)
  195. (format stream "node[shape=\"record\",label=\"~a\""
  196. (format nil "~{~a~^ | ~}"
  197. (mapcar #'generate-record-label (cddr element))))
  198. (write-attributes element "," '(:id :label :shape))
  199. (format stream "]{struct~a};"
  200. (second (find element records :key #'first :test #'equal)))))))
  201. (store-record-nodes (graph)
  202. (if (equal (first graph) :node)
  203. (list (second (find :id (second graph) :key #'first)))
  204. (loop for element in (cddr graph)
  205. append (store-record-nodes element))))
  206. (store-records (graph)
  207. (if (equal (first graph) :record)
  208. (push (list graph (length records)
  209. (store-record-nodes graph)) records)
  210. (loop for element in (cddr graph)
  211. do (store-records element)))))
  212. (store-records graph)
  213. (when check-syntax (check-syntax graph))
  214. (format stream "digraph g {compound=\"true\";")
  215. (write-attributes graph ";" nil)
  216. (loop for element in (cddr graph) do (write-element element))
  217. (format stream "}~c" #\linefeed))))
  218. (defun render-s-dot (file-name format graph &key (check-syntax t))
  219. "Renders a s-dot graph into a graphic file. 'file-name' should be a pathname.
  220. If the file-name is /foo/bar.png, the dot file /foo/bar.dot is created
  221. and then rendered. Format should be one out of
  222. http://www.graphviz.org/doc/info/output.html, for example svg, ps,
  223. gif, png, or jpg. The uiop:run-program is used to launch dot. If that
  224. does not work for you, write a similar function that uses (s-dot->dot)
  225. for dot generation and then runs dot on it.
  226. SBCL users may take advantage of sb-ext:run-program, opening a pipe to
  227. 'dot' instead of writing a file and reading that in again."
  228. #+sbcl (render-s-dot-sbcl file-name format graph :check-syntax check-syntax)
  229. #-sbcl (let ((dot-file-name (make-pathname :directory (pathname-directory file-name)
  230. :name (pathname-name file-name) :type "dot")))
  231. (with-open-file (stream dot-file-name :direction :output :if-exists :supersede
  232. :if-does-not-exist :create)
  233. (s-dot->dot stream graph :check-syntax check-syntax))
  234. (uiop:run-program (format nil "~a -o ~a ~a~a ~a"
  235. *dot-exe* file-name *dot-output-format-switch*
  236. format dot-file-name)
  237. :ignore-error-status t)))
  238. #+sbcl (defun render-s-dot-sbcl (file-name format graph &key (check-syntax t))
  239. (with-open-file (stream file-name
  240. :direction :output
  241. :if-exists :supersede
  242. :if-does-not-exist :create
  243. :element-type '(unsigned-byte 8))
  244. (let ((process (sb-ext:run-program *dot-exe*
  245. (list (format nil
  246. "~a~a"
  247. *dot-output-format-switch* format))
  248. :output :stream
  249. :input :stream
  250. :error t
  251. :wait nil
  252. :search t)))
  253. (when process
  254. (let ((process-input-stream (sb-ext:process-input process))
  255. (buffer (make-array 256 :element-type '(unsigned-byte 8))))
  256. (s-dot->dot process-input-stream graph :check-syntax check-syntax)
  257. (finish-output process-input-stream)
  258. (close process-input-stream)
  259. (do ((read (read-sequence buffer (sb-ext:process-output process))
  260. (read-sequence buffer (sb-ext:process-output process))))
  261. ((/= read (length buffer))
  262. (write-sequence buffer stream :start 0 :end read))
  263. (finish-output (sb-ext:process-output process))
  264. (write-sequence buffer stream :start 0 :end read)))
  265. (sb-ext:process-exit-code (sb-ext:process-close process))))))
  266. (defun test-s-dot ()
  267. "Generates a few charts in the system temporary directory"
  268. (let ((directory (uiop:default-temporary-directory))
  269. (graph1 '(:graph ()
  270. (:node ((:id "a") (:label "a")))
  271. (:cluster ((:id "c1") (:label "c1") (:style "filled") (:fillcolor "#EEEEFF")
  272. (:fontcolor "#900000") (:fontname "Arial bold") (:fontsize "15"))
  273. (:node ((:id "b"))) (:node ((:id "c")))
  274. (:edge ((:from "b") (:to "c"))))
  275. (:cluster ((:id "c2") (:label "c2") (:fontname "Courier") (:style "dashed"))
  276. (:node ((:id "d"))) (:node ((:id "e"))) (:node ((:id "f")))
  277. (:edge ((:from "d") (:to "e")))
  278. (:edge ((:from "e") (:to "f")))
  279. (:edge ((:from "d") (:to "f"))))
  280. (:node ((:id "g")))
  281. (:edge ((:from "a") (:to "b")))
  282. (:edge ((:from "a") (:to "d") (:lhead "c2") (:label "lhead='c2'")))
  283. (:edge ((:from "b") (:to "e") (:ltail "c1")))
  284. (:edge ((:from "d") (:to "c") (:ltail "c2")))
  285. (:edge ((:from "a") (:to "g")))
  286. (:edge ((:from "c") (:to "g") (:ltail "c1") (:label "ltail='c1'")))
  287. (:edge ((:from "f") (:to "g") ))))
  288. (graph2 '(:graph ()
  289. (:record ()
  290. (:node ((:shape "ignored") (:id "10") (:label "left")))
  291. (:node ((:id "(#(0 1 2 3 4) 5)") (:label "(#(0 1 2 3 4) 5)")))
  292. (:node ((:id "12") (:label "right"))))
  293. (:record ()
  294. (:node ((:id "20") (:label "one")))
  295. (:node ((:id "21") (:label "two"))))
  296. (:record ()
  297. (:node ((:id "30") (:label "{hello hello\\nworld}")))
  298. (:record ()
  299. (:node ((:id "311") (:label "b")))
  300. (:record ()
  301. (:node ((:id "3120") (:label "c")))
  302. (:node ((:id "3121") (:label "d")))
  303. (:node ((:id "3122") (:label "e"))))
  304. (:node ((:id "313") (:label "f"))))
  305. (:node ((:id "32") (:label "g")))
  306. (:node ((:id "33") (:label "h"))))
  307. (:edge ((:from "(#(0 1 2 3 4) 5)") (:to "20")))
  308. (:edge ((:from "12") (:to "3121")))))
  309. (graph3 '(:graph ()
  310. (:node ((:id "#(0 1)") (:label "#(0 1 2)")))
  311. (:node ((:id "#(0 2)") (:label "#(0 2 2)")))
  312. (:edge ((:from "#(0 2)") (:to "#(0 1)"))))))
  313. (flet ((make-filepath (name extension)
  314. (uiop:merge-pathnames* directory
  315. (make-pathname :name name :type extension))))
  316. (render-s-dot (make-filepath "test1" "gif") "gif" graph1)
  317. (render-s-dot (make-filepath "test2" "ps") "ps" graph1)
  318. (render-s-dot (make-filepath "test3" "jpg") "jpg" graph2)
  319. (render-s-dot (make-filepath "test4" "png") "png" graph2)
  320. (render-s-dot (make-filepath "test5" "svg") "svg" graph2)
  321. (render-s-dot (make-filepath "test6" "ps") "ps" graph3))))
  322. ;;(test-s-dot)