/
org-mind-map.el
897 lines (799 loc) · 36 KB
/
org-mind-map.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
;;; org-mind-map.el --- Creates a directed graph from org-mode files
;; Author: Ted Wiles <theodore.wiles@gmail.com>
;; Keywords: orgmode, extensions, graphviz, dot
;; Version: 0.4
;; URL: https://github.com/theodorewiles/org-mind-map
;; Package-Requires: ((emacs "24") (dash "1.8.0") (org "8.2.10"))
;; This file is NOT part of GNU Emacs.
;; 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, 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 GNU Emacs; see the file LICENSE. If not, write to the
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.
;;; Commentary:
;; This package takes an org-mode tree and converts it into a
;; file that can be read into graphviz in order to visually show the
;; tree as a directed graph. Mail to <theodore.wiles@gmail.com> to discuss
;; features and additions. All suggestions are more than welcome.
;;; Commands:
;;
;; Below is a complete list of commands:
;;
;; `org-mind-map-write'
;; Create a digraph based on all org trees in the current buffer.
;; Keybinding: M-x org-mind-map-write
;; `org-mind-map-write-current-branch'
;; Create a directed graph output based on just the current org tree branch.
;; Keybinding: M-x org-mind-map-write-current-branch
;; `org-mind-map-write-current-tree'
;; Create a directed graph output based on the whole current org tree.
;; Keybinding: M-x org-mind-map-write-current-tree
;;
;;; Customizable Options:
;;
;; Below is a list of customizable options:
;;
;; `org-mind-map-wrap-line-length'
;; Line length within graphviz nodes.
;; default = 30
;; `org-mind-map-wrap-legend-line-length'
;; Line length of the graphviz legend.
;; default = 45
;; `org-mind-map-unflatten-command'
;; Shell executable command for running the UNFLATTEN command.
;; default = "unflatten -l3"
;; `org-mind-map-dot-command'
;; Shell executable command for running the DOT command.
;; default = "dot"
;; `org-mind-map-dot-output'
;; Format of the DOT output. Defaults to PDF.
;; default = "pdf"
;; `org-mind-map-engine'
;; Sets the layout engine used in your graphs.
;; default = "dot"
;; `org-mind-map-default-node-attribs'
;; Alist of default node attributes and values.
;; default = '(("shape" . "plaintext"))
;; `org-mind-map-default-edge-attribs'
;; Alist of default edge attributes and values.
;; default = nil
;; `org-mind-map-default-graph-attribs'
;; Alist of default graph attributes and values.
;; default = '(("autosize" . "false") ("size" . "9,12") ("resolution" . "100") ...))
;; `org-mind-map-node-formats'
;; Assoc list of (NAME . FN) pairs where NAME is a value for the :OMM-NODE-FMT property
;; See also `org-mind-map-make-node-fn'
;; default = nil
;; `org-mind-map-edge-formats'
;; Assoc list of (NAME . FN) pairs where NAME is a value for the :OMM-EDGE-FMT property
;; See also `org-mind-map-make-edge-fn'
;; default = nil
;; `org-mind-map-edge-format-default'
;; Default format string for graph edges, e.g. "[style=dotted]".
;; default = ""
;; `org-mind-map-reserved-colors'
;; List of colors that will not be used for coloring tags.
;; default = nil
;; `org-mind-map-tag-colors'
;; An alist of (TAG . COLOR) pairs for choosing colors for tags.
;; default = nil
;; `org-mind-map-include-text'
;; A boolean indicating whether our not to include paragraph text in body of nodes.
;; default = t
;; `org-mind-map-include-images'
;; A boolean indicating whether our not to include images in body of nodes.
;; default = t
;; The headings of the org-mode file are treated as node text in the resulting tree.
;; Org-mode heading tags are included in the resulting tree as additional cells
;; within the node.
;; The tags are color-coded to be consistent across the tree.
;; Tree interleaving is also possible by naming multiple org-mode headings
;; with the same heading.
;; NOTE: this requires the GRAPHVIZ software. This is installable on
;; windows using cygwin.
;; To install, add this code to your .emacs:
;; (load "org-mind-map.el")
;; If on linux, customize the values of `org-mind-map-unflatten-command'
;; and `org-mind-map-dot-command' to have the values corresponding to
;; the executables in your system.
;; Then, run "M-x org-mind-map-write" to create a graph of all trees in the current buffer,
;; You can customize the style of the graph by adding :OMM-NODE-FMT and :OMM-EDGE-FMT properties
;; to the headlines in the tree.
;; The latest version is available at:
;;
;; https://github.com/theodorewiles/org-mind-map
;;
;;; Code:
(require 'dash)
(require 'org)
(require 'subr-x)
(defconst org-mind-map-version "0.4")
(defgroup org-mind-map nil
"Convert org-mode tree into a graphviz directed graph"
:group 'org)
(defcustom org-mind-map-wrap-line-length 30
"Line length within graphviz nodes."
:type 'integer
:group 'org-mind-map)
(defcustom org-mind-map-wrap-text-length 60
"Line length within graphviz nodes that have longer text."
:type 'integer
:group 'org-mind-map)
(defcustom org-mind-map-wrap-legend-line-length 45
"Line length of the graphviz legend."
:type 'integer
:group 'org-mind-map)
(defcustom org-mind-map-unflatten-command "unflatten -l3"
"Shell executable command for running the UNFLATTEN command."
:type 'string
:group 'org-mind-map)
(defcustom org-mind-map-dot-command "dot"
"Shell executable command for running the DOT command."
:type 'string
:group 'org-mind-map)
(defcustom org-mind-map-dot-output '("pdf" "png" "jpeg" "svg" "eps" "gif" "tiff")
"List of formats for the DOT output file.
If more than one are specified then the user will be prompted to choose one.
To find a list of available formats, on the command line enter: dot -T?"
:type '(repeat (string :tag "File type"))
:group 'org-mind-map)
(defcustom org-mind-map-display nil
"How the results should be displayed:
nil = don't display results
current = display results in current window
window = display results in new window
frame = display results in new frame"
:type '(choice (const :tag "Don't display" nil)
(const :tag "Display in current window" current)
(const :tag "Display in new window" window)
(const :tag "Display in new frame" frame))
:group 'org-mind-map)
(defcustom org-mind-map-engine "dot"
"Sets the layout engine used in your graphs.
See the graphviz user manual for description of these options."
:type '(choice
(const :tag "Directed Graph" "dot")
(const :tag "Undirected Spring Graph" "neato")
(const :tag "Radial Layout" "twopi")
(const :tag "Circular Layout" "circo")
(const :tag "Undirected Spring Force-Directed" "fdp"))
:group 'org-mind-map)
(defcustom org-mind-map-default-node-attribs '(("shape" . "plaintext"))
"Alist of default node attributes and values.
Each item in the alist should be a cons cell of the form (ATTRIB . VALUE)
where ATTRIB and VALUE are strings.
For a list of value attributes, see here: https://graphviz.gitlab.io/_pages/doc/info/attrs.html"
:type '(alist :key-type (string :tag "Attribute") :value-type (string :tag " Value"))
:group 'org-mind-map)
(defcustom org-mind-map-default-edge-attribs nil
"Alist of default edge attributes and values.
Each item in the alist should be a cons cell of the form (ATTRIB . VALUE)
where ATTRIB and VALUE are strings.
For a list of value attributes, see here: https://graphviz.gitlab.io/_pages/doc/info/attrs.html"
:type '(alist :key-type (string :tag "Attribute") :value-type (string :tag " Value"))
:group 'org-mind-map)
(defcustom org-mind-map-default-graph-attribs '(("autosize" . "false")
("size" . "9,12")
("resolution" . "100")
("nodesep" . "0.75")
("overlap" . "false")
("spline" . "true")
("rankdir" . "LR"))
"Alist of default graph attributes and values.
Each item in the alist should be a cons cell of the form (ATTRIB . VALUE)
where ATTRIB and VALUE are strings.
For a list of value attributes, see here: https://graphviz.gitlab.io/_pages/doc/info/attrs.html"
:type '(alist :key-type (string :tag "Attribute") :value-type (string :tag " Value"))
:group 'org-mind-map)
(defcustom org-mind-map-node-formats nil
"Assoc list of (NAME . FN) pairs where NAME is a value for the :OMM-NODE-FMT property
of a node/headline, and FN is a function which outputs a format string to be placed after the
node name (e.g. \"[label='Node1',color='red']\").
The function FN should take the following 5 arguments which can be used to construct the format:
TITLE = the label string for the node
TAGS = a list of org tags for the current node
COLOR = the contents of the OMM-COLOR property for the current node
HM = a hash map of colors
EL = an org element obtained from `org-element-map'
Note: the :OMM-NODE-FMT property is inherited by children of the node/headline where it is defined."
:type '(alist :key-type (string :tag " Name")
:value-type (function :tag "Format function"))
:group 'org-mind-map)
(defcustom org-mind-map-edge-formats nil
"Assoc list of (NAME . FN) pairs where NAME is a value for the :OMM-EDGE-FMT property
of a node/headline, and FN is a function which outputs a format string to be placed after an
edge (e.g. \"[style=dotted]\").
The function FN should take the following 2 arguments which can be used to construct the format:
HM = a hash map of colors
EL = an org element obtained from `org-element-map'
Note: the :OMM-EDGE-FMT property affects edges leading to the node at which it is defined, and
is inherited by children of that node/headline."
:type '(alist :key-type (string :tag " Name")
:value-type (function :tag "Format function"))
:group 'org-mind-map)
(defcustom org-mind-map-edge-format-default ""
"Default format string for graph edges, e.g. \"[style=dotted]\"."
:type 'string
:group 'org-mind-map)
(defcustom org-mind-map-reserved-colors nil
"List of colors that will not be used for coloring tags.
These colors will be excluded when random tag colors are chosen by `org-mind-map-rgb'
so that you can use them for other things.
Each color should be in hexadecimal form, e.g: \"#e3cfbc\", where the consecutive pairs
of hexdigits indicate levels of red, green and blue respectively.
It is not necessary to include any colors with levels below 7d, as these are not used
for creating random tag colors."
:type '(repeat string)
:group 'org-mind-map)
(defcustom org-mind-map-tag-colors nil
"An alist of (TAG . COLOR) pairs for choosing colors for tags.
Any tags not listed here will be colored with randomly selected colors that dont
clash with those in `org-mind-map-reserved-colors'.
Each color should be in hexadecimal form, e.g: \"#e3cfbc\", where the consecutive pairs
of hexdigits indicate levels of red, green and blue respectively.
Note: you can also set tag colors by altering the hashmap passed as an argument to functions
defined in `org-mind-map-node-formats'."
:type '(alist :key-type (string :tag " Tag") :value-type (string :tag "Color"))
:group 'org-mind-map)
(defcustom org-mind-map-include-text t
"A boolean indicating whether our not to include paragraph text in body of nodes.
default = t"
:type 'boolean
:group 'org-mind-map
)
(defcustom org-mind-map-include-images t
"A boolean indicating whether our not to include paragraph text in body of nodes.
default = t"
:type 'boolean
:group 'org-mind-map
)
(defun org-mind-map-do-wrap (words width)
"Create lines of maximum width WIDTH (in characters) from word list WORDS."
(let (lines line)
(while words
(setq line (pop words))
(while (and words (< (+ (length line) (length (car words))) width))
(setq line (concat line " " (pop words))))
(setq lines (push line lines)))
(nreverse lines)))
(defun org-mind-map-wrap (s l)
(let* ((s2 (org-mind-map-do-wrap (split-string s " ") l)))
(mapconcat 'identity s2 "<br></br>")))
(defun org-mind-map-wrap-lines (s)
"Wraps a string S so that it can never be more than ORG-MIND-MAP-WRAP-LINE-LENGTH characters long."
(org-mind-map-wrap s org-mind-map-wrap-line-length))
(defun org-mind-map-wrap-text (s)
"Wraps a string S so that it can never be more than ORG-MIND-MAP-WRAP-TEXT-LENGTH characters long."
(org-mind-map-wrap s org-mind-map-wrap-text-length))
(defun org-mind-map-wrap-legend-lines (s)
"Wraps a string S so that it can never be more than ORG-MIND-MAP-WRAP-LEGEND-LINE-LENGTH characters long."
(let* ((s2 (org-mind-map-do-wrap (split-string s " ") org-mind-map-wrap-legend-line-length)))
(mapconcat 'identity s2 "<br></br>")))
(defun org-mind-map-dot-node-name (s)
"Make string S formatted to be usable within dot node names."
(concat "\""
(replace-regexp-in-string
"</?\\(table\\|tr\\|td\\)[^<>]*>" ""
(replace-regexp-in-string "label=\\(\"[^\"]+\"\\|[^,]+\\).*" "\\1" s))
"\""))
(defun org-mind-map-add-color (hm tag &optional colspan)
"Create data element containing TAG with associated color found in hashmap HM."
(let* ((color (gethash tag hm)))
(concat "<td"
(if colspan (concat " colspan=\"" (int-to-string colspan) "\""))
(if color (concat " bgcolor=\"" color "\"")) ">" tag "</td>")))
(defun org-mind-map-write-tags-default (title tags color hm el &optional content images)
"Default function for writing nodes.
Label node with TITLE and background COLOR, and write TAGS (a list of tag names)
into boxes underneath, using associated colors in hashmap HM.
The EL argument is not used, but is needed for compatibility."
(concat "[label=<<table>"
(if (> (length tags) 0)
(concat "<tr><td colspan=\"" (int-to-string (length tags)) "\" ")
"<tr><td")
(if color (concat " bgcolor=\"" color "\" "))
">" title "</td></tr>"
(if (> (length tags) 0)
(concat
"<tr>" (mapconcat (-partial 'org-mind-map-add-color hm) tags "") "</tr>"))
(if (> (length content) 0)
(concat
"<tr><td BALIGN=\"LEFT\" ALIGN=\"LEFT\">" content "</td></tr>")
)
(if (> (length images) 0)
images ""
)
"</table>>];"))
(defun org-mind-map-get-property (prop el &optional inheritp)
"Get property PROP from an org element EL, using inheritance if INHERITP is non-nil.
PROP can be either the property symbol (beginning with :), or the name of the property (with or without :).
If there is a column summary value for the property that has recently be calculated it will be used."
(let* ((node el)
(propstr (if (stringp prop)
(upcase (if (string-match "^:" prop)
(substring prop 1)
prop))
(substring (symbol-name prop) 1)))
(prop (if (stringp prop) (intern (concat ":" propstr)) prop))
(val (or (cdr (cl-find propstr (get-text-property
(org-element-property :begin el)
'org-summaries)
:test (lambda (x y) (equal (caar y) x))))
(org-element-property prop el))))
(while (and inheritp
(not val)
(not (eq (org-element-type node) 'org-data)))
(setq node (org-element-property :parent node)
val (org-element-property prop node)))
val))
(defun org-mind-map-narrow-to-heading-content (b)
"Narrow to the region until the next headline, if applicable"
(let* ((new-end
(org-element-map (org-element-parse-buffer 'object 'true)
'headline
(lambda (x)
(if (not
(= (org-element-property :begin x) b))
b nil))
nil 'true)))
(if new-end
(progn
(widen)
(narrow-to-region b new-end)))))
(defun org-mind-map-write-tags (hm el &optional edgep)
"Use HM as the hash-map of colors and takes an element EL and extracts the title and tags.
Then, formats the titles and tags so as to be usable within DOT's graphviz language."
(let* ((ts (org-element-property :title el))
(wrapped-title (org-mind-map-wrap-lines (if (listp ts) (first ts) ts)))
(title (replace-regexp-in-string "&" "&" wrapped-title nil t))
(color (org-element-property :OMM-COLOR el))
(tags (org-element-property :tags el))
(fmt (org-mind-map-get-property (if edgep :OMM-EDGE-FMT :OMM-NODE-FMT) el))
(b (org-element-property :begin el))
(e (org-element-property :end el))
(images
(if org-mind-map-include-images
(save-restriction
(narrow-to-region b e)
(org-mind-map-narrow-to-heading-content b)
(mapconcat 'identity
(org-element-map (org-element-parse-buffer 'object 'true)
'(link)
(lambda (x)
(message "Inline image: %s" (org-export-inline-image-p x))
(if (org-export-inline-image-p x)
(concat
"<tr><td fixedsize='TRUE' height='100' width='100'>" "<IMG src='"
(org-element-property :path x)
"'/>"
"</td></tr>")
"")))
""))))
(content
(if org-mind-map-include-text
(save-restriction
(narrow-to-region b e)
(org-mind-map-narrow-to-heading-content b)
(mapconcat 'identity
(org-element-map (org-element-parse-buffer 'object 'true)
'(paragraph)
(lambda (x)
(org-mind-map-wrap-text
(string-trim
(substring-no-properties
(car (org-element-contents x)))))))
"<br></br><br></br>"))
nil))
)
(if edgep (funcall (or (cdr (assoc fmt org-mind-map-edge-formats))
(lambda (a b) org-mind-map-edge-format-default))
hm el)
(funcall (or (cdr (assoc fmt org-mind-map-node-formats))
'org-mind-map-write-tags-default)
title tags color hm el content images))))
(defun org-mind-map-first-headline (e)
"Figure out the first headline within element E."
(let* ((parent (org-element-property :parent e)))
(if parent
(if (eq (org-element-type parent) 'headline)
parent
(org-mind-map-first-headline parent))
nil)))
(defun org-mind-map-valid-link? (e)
"Is E at a valid link?"
(condition-case ex
(let* ((org-link-search-inhibit-query t)
(type (org-element-property :type e))
(l (org-element-property :path e)))
(if (string= type "fuzzy")
(save-excursion
(org-link-search l) t)
nil))
('error nil)))
(defun org-mind-map-destination-headline (e)
"Figure out where the link in E is pointing to."
(let* ((l (org-element-property :path e))
(org-link-search-inhibit-query t))
(save-excursion
(org-open-link-from-string (concat "[[" l "]]"))
(org-element-at-point))))
(defun org-mind-map-get-links (hm)
"Make a list of links with the headline they are within and
their destination. Pass hashmap arg HM mapping tags to colors
in order to keep the tag colors consistent across calls."
(org-element-map (org-element-parse-buffer 'object)
'link
(lambda (l)
(if (org-mind-map-valid-link? l)
(let* ((origin
(org-mind-map-write-tags hm
(org-mind-map-first-headline l)))
(h (org-mind-map-destination-headline l))
(destination
(org-mind-map-write-tags hm h)))
(list origin destination))))))
(defun org-mind-map-make-legend (h)
"Make a legend using the hash-map HM."
(let ((res '()))
(maphash (lambda (k v) (push k res)) h)
(if (> (length res) 0)
(concat
"{
Legend [shape=none, margin=0, label=<
<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"4\">
<TR>
<TD COLSPAN=\"2\"><B>Legend</B></TD>
</TR>"
(mapconcat 'identity
(let* (result)
(maphash
(lambda (name color)
(push (concat "<tr><td>" (org-mind-map-wrap-legend-lines name)
"</td><td bgcolor=\"" color "\"> </td></tr>")
result))
h)
(reverse result))
"")
"</TABLE>>];}"))))
(defun org-mind-map-rgb (&optional exceptions)
"Make a random pastel-like RGB color.
Dont return any of the colors listed in the optional arg EXCEPTIONS."
(let* ((fn (lambda nil
(concat "#"
(format "%x" (+ 125 (random (- 255 125))))
(format "%x" (+ 125 (random (- 255 125))))
(format "%x" (+ 125 (random (- 255 125)))))))
(color (funcall fn)))
(while (member color exceptions)
(setq color (funcall fn)))
color))
(defun org-mind-map-tags (&optional exceptions)
"Return a hash map of tags in the org file mapped to random colors.
Dont return any of the colors listed in the optional arg EXCEPTIONS."
(let* ((hm (make-hash-table :test 'equal)))
(org-element-map (org-element-parse-buffer 'headline) 'headline
(lambda (hl)
(let ((tags (mapcar 'substring-no-properties (org-element-property :tags hl)))
(legend (org-element-property :OMM-LEGEND hl))
(color (org-element-property :OMM-COLOR hl)))
(if legend (puthash legend color hm))
(if tags (mapcar (lambda (x)
(puthash x (--if-let (assoc x org-mind-map-tag-colors)
(cdr it)
(org-mind-map-rgb
(append exceptions
(mapcar 'cdr org-mind-map-tag-colors))))
hm))
tags)))))
hm))
(defun org-mind-map-data (&optional linksp)
"Create graph & tag legend of all directed pairs of headlines for constructing the digraph.
If LINKSP is non-nil include graph edges for org links."
(let* ((hm (org-mind-map-tags org-mind-map-reserved-colors))
(output
(org-element-map (org-element-parse-buffer 'headline) 'headline
(lambda (hl)
(let ((parent (org-element-property :parent hl)))
(and (eq (org-element-type parent) 'headline)
(list (org-mind-map-write-tags hm parent)
(org-mind-map-write-tags hm hl)
(org-mind-map-write-tags hm hl t))))))))
(list (append output (if linksp (org-mind-map-get-links hm))) hm)))
(defun org-mind-map-make-dot (data)
"Create the dot file from DATA."
(let ((table (nth 0 data))
(legend (nth 1 data)))
(concat "digraph structs {\n // DEFAULT OPTIONS\n"
(if org-mind-map-default-graph-attribs
(concat " graph ["
(mapconcat #'(lambda (x) (concat (car x) "=\"" (cdr x) "\""))
org-mind-map-default-graph-attribs ", ")
"];\n"))
(if org-mind-map-default-node-attribs
(concat
" node [" (mapconcat #'(lambda (x) (concat (car x) "=\"" (cdr x) "\""))
org-mind-map-default-node-attribs ", ")
"];\n"))
(if org-mind-map-default-edge-attribs
(concat
" edge [" (mapconcat #'(lambda (x) (concat (car x) "=\"" (cdr x) "\""))
org-mind-map-default-edge-attribs ", ")
"];\n"))
" // NODES\n"
(mapconcat
#'(lambda (x) (concat " " (org-mind-map-dot-node-name x) " " x))
(-distinct (-flatten (mapcar (lambda (x) (list (nth 0 x) (nth 1 x))) table)))
"\n")
"\n // EDGES\n"
(mapconcat #'(lambda (x) (format " %s -> %s;"
(org-mind-map-dot-node-name (nth 0 x))
(org-mind-map-dot-node-name (nth 1 x))
(nth 2 x)))
table "\n")
(org-mind-map-make-legend legend)
"}")))
(defun org-mind-map-command (name outputtype)
"Return the shell script that will create the correct file NAME of type OUTPUTTYPE.
The output file will be in the same location as the org file."
(concat org-mind-map-unflatten-command " | "
org-mind-map-dot-command " -T"
(shell-quote-argument outputtype) " -K"
(shell-quote-argument org-mind-map-engine) " -o"
(shell-quote-argument (concat name "." outputtype ""))))
(defun org-mind-map-update-message (filename process event)
"Write an update message on the output of running org-mind-map based on PROCESS and EVENT.
Open FILENAME according to value of `org-mind-map-display'."
(let* ((e (with-current-buffer "*org-mind-map-errors*"
(buffer-string))))
(if (string= e "")
(princ (format "Org mind map %s" event))
(princ (format "Org mind map %sErrors: %s" event e)))
(if (string= event "finished\n")
(progn
(cl-case org-mind-map-display
(nil nil)
(current (find-file filename))
(window (find-file-other-window filename))
(frame (switch-to-buffer-other-frame (find-file-noselect filename))))
(cl-case major-mode
(pdf-view-mode (pdf-view-fit-page-to-window))
(doc-view-mode (doc-view-fit-page-to-window)))))))
(defun org-mind-map-write-named (name &optional debug linksp)
"Create a directed graph output based on the org tree in the current buffer, with name NAME.
To customize, see the org-mind-map group.
If DEBUG is non-nil, then print the dot command to the *Messages* buffer,
and print the dotfile to the *Messages* buffer or to a file if DEBUG is a filename.
If LINKSP is non-nil include graph edges for org links."
(let ((dot (org-mind-map-make-dot (org-mind-map-data linksp)))
a (outputtype (if (> (length org-mind-map-dot-output) 1)
(completing-read "Output file type: " org-mind-map-dot-output)
(car org-mind-map-dot-output))))
(if debug
(progn (message (org-mind-map-command name outputtype))
(if (stringp debug)
(with-temp-file debug (insert dot))
(message dot "%s"))))
(if (get-buffer "*org-mind-map-errors*")
(kill-buffer "*org-mind-map-errors*"))
(let* ((p (start-process-shell-command
"org-mind-map-s" "*org-mind-map-errors*"
(org-mind-map-command name outputtype)))
(filename (concat name "." outputtype "")))
(process-send-string p dot)
(process-send-string p "\n")
(process-send-eof p)
(set-process-sentinel p (-partial 'org-mind-map-update-message filename))
filename)))
;;;###autoload
(defun org-mind-map-write-with-prompt nil
"Prompt for an output FILENAME (without extension) to write your output and .dot files."
(let ((filename (read-file-name "What is the file name you would like to save to?")))
(org-mind-map-write-named filename (concat filename ".dot")
(y-or-n-p "Include org links? "))))
(defun org-mind-map-default-filename (treenamep)
"Return a default filename for saving the tree diagram.
If TREENAMEP is non-nil include in the filename the name of the top level header of the tree."
(concat (file-name-sans-extension (buffer-name))
"_diagram"
(if treenamep
(concat "-"
(replace-regexp-in-string " +" "_" (nth 4 (org-heading-components)))))))
;;;###autoload
(defun org-mind-map-write (&optional promptp)
"Create a digraph based on all org trees in the current buffer.
The digraph will be named the same name as the current buffer.
To customize, see the org-mind-map group.
If called with prefix arg (or PROMPTP is non-nil), then call `org-mind-map-write-with-prompt'."
(interactive "P")
(if promptp (org-mind-map-write-with-prompt)
(org-mind-map-write-named (org-mind-map-default-filename nil))))
;;;###autoload
(defun org-mind-map-write-current-branch (&optional promptp)
"Create a directed graph output based on just the current org tree branch.
To customize, see the org-mind-map group.
If called with prefix arg (or PROMPTP is non-nil), then call `org-mind-map-write-with-prompt'."
(interactive "P")
(org-narrow-to-subtree)
(let ((filename (if promptp (org-mind-map-write-with-prompt)
(org-mind-map-write-named (org-mind-map-default-filename t)))))
(widen)
filename))
;;;###autoload
(defun org-mind-map-write-current-tree (&optional promptp)
"Create a directed graph output based on the whole current org tree.
If called with prefix arg (or PROMPTP is non-nil), then call `org-mind-map-write-with-prompt'."
(interactive "P")
(save-restriction
(ignore-errors (outline-up-heading 100))
(org-mind-map-write-current-branch promptp)))
;;;###autoload
(defmacro org-mind-map-make-node-fn (name doc props &optional shape color other)
"Create a function org-mind-map-NAME-node for use with :OMM-NODE-FMT writing node properties.
The created function should be added to `org-mind-map-node-formats' and the associated string
can be used as the :OMM-NODE-FMT for a tree.
Document the function with the DOC arg.
PROPS is a list of either property & format string pairs, or individual property names,
which will be placed in each node, e.g: ((\"PROB\" \"probability=%s\") \"COST\").
For property names with no format string, \"%s=%s\" will be used with the property name and value.
The node shape and background color can be specified with the optional SHAPE and COLOR arguments,
and any other attributes (e.g. \"fontsize=30\") can be specified with the OTHER argument.
Each of these arguments can be either a string or a form which is evaluated for each node,
and returns a string.
Example: (org-mind-map-make-node-fn decisiontree \"Draw decision tree\" (\"COST\" (\"NOTES\" \"Notes: %s\")) nil
(cond ((equal (org-mind-map-get-property :todo-keyword el) \"ACTION\") \"red\")
((equal (org-mind-map-get-property :todo-keyword el) \"STATE\") \"yellow\")
((equal (org-mind-map-get-property :todo-keyword el) \"DECISION\") \"green\")))
You could put this code in your emacs startup file (e.g. ~/.emacs) and then add to `org-mind-map-node-formats'
the pair '(\"decisiontree\" . org-mind-map-decisiontree-node), and use \":OMM-NODE-FMT: decisiontree\" as a
tree property."
`(defun ,(intern (concat "org-mind-map-" (symbol-name name) "-node"))
(title tags color hm el)
,doc
(let* ((numtags (if tags (length tags)))
(colspan (if tags (int-to-string numtags)))
(propstxt
(cl-remove
nil (list ,@(mapcar
(lambda (p)
(cond ((stringp p)
`(--if-let (org-mind-map-get-property ,p el)
(concat ,(upcase p) "=" it)))
((consp p)
`(--if-let (org-mind-map-get-property ,(car p) el)
(format ,(nth 1 p) it)))
(t (error "Invalid props value"))))
props))))
(shape ,shape)
(color (or color ,color))
(other ,other))
(concat "[label=<<table" (if shape " border=\"0\"") ">"
(if numtags (concat "<tr><td colspan=\"" colspan "\" ") "<tr><td")
(if (and color (not shape)) (concat " bgcolor=\"" color "\" "))
">" title "</td></tr>"
(mapconcat (lambda (p)
(concat "<tr>" (org-mind-map-add-color hm p numtags) "</tr>"))
propstxt "")
(if numtags
(concat "<tr>"
(mapconcat (-partial 'org-mind-map-add-color hm) tags "")
"</tr>"))
"</table>>"
(if shape (concat ",shape=" shape (if color (concat ",style=filled,color=" color))))
(if other (concat "," other)) "];"))))
;;;###autoload
(defmacro org-mind-map-make-edge-fn (name doc props &optional style color other)
"Create a function org-mind-map-write-NAME for writing edge properties which can be used for :OMM-EDGE-FMT.
Document the function with the DOC arg.
PROPS is a list of either property & format string pairs, or individual property names,
which will concatenated and used to label the edges, e.g: ((\"PROB\" \"probability=%s\") \"COST\").
For property names with no format string \"%s=%s\" will be used with the property name and value.
The edge style and color can be specified with the optional STYLE and COLOR arguments,
and any other attributes (e.g. \"fontsize=30\") can be specified with the OTHER argument.
Each of these arguments can be either a string or a form which is evaluated for each node,
and returns a string.
Example: (org-mind-map-make-edge-fn decisiontree \"Draw decision tree\" (\"PROB\"))
You could put this code in your emacs startup file (e.g. ~/.emacs) and then add to `org-mind-map-edge-formats'
the pair '(\"decisiontree\" . org-mind-map-decisiontree-edge), and use \":OMM-EDGE-FMT: decisiontree\" as a
tree property."
`(defun ,(intern (concat "org-mind-map-" (symbol-name name) "-edge"))
(hm el)
,doc
(let* ((propstxt (cl-remove
nil (list ,@(mapcar (lambda (p)
(cond ((stringp p)
`(--if-let (org-mind-map-get-property ,p el)
(concat ,(upcase p) "=" it)))
((consp p)
`(--if-let (org-mind-map-get-property ,(car p) el)
(format ,(nth 1 p) it)))
(t (error "Invalid props value"))))
props))))
(style ,style)
(color ,color)
(other ,other))
(concat "[label=\"" (mapconcat 'identity propstxt ",") "\""
(if color (concat ",color=\"" color "\" "))
(if style (concat ",style=\"" style "\""))
(if other (concat "," other)) "]"))))
(defun ox-graphviz-export (&optional async subtreep visible-only body-only info)
"Export the current buffer to a graphviz diagram.
Optional argument ASYNC to asynchronously export.
Optional argument SUBTREEP to export current subtree.
Optional argument VISIBLE-ONLY to only export visible content.
Optional argument BODY-ONLY export only the body.
Optional argument INFO is a plist of options."
(let ((org-mind-map-display nil))
(if subtreep (org-mind-map-write-current-branch)
(org-mind-map-write))))
(defun ox-graphviz-export-and-open (&optional async subtreep visible-only body-only info)
"Export the current buffer to a graphviz diagram, and open the output file.
Optional argument ASYNC to asynchronously export.
Optional argument SUBTREEP to export current subtree.
Optional argument VISIBLE-ONLY to only export visible content.
Optional argument BODY-ONLY export only the body.
Optional argument INFO is a plist of options."
(let ((org-mind-map-display (or org-mind-map-display 'current)))
(if subtreep (org-mind-map-write-current-branch)
(org-mind-map-write))))
(defun ox-graphviz-export-dot (&optional async subtreep visible-only body-only info)
"Export the current buffer to a graphviz diagram, and create and open a dot file.
Optional argument ASYNC to asynchronously export.
Optional argument SUBTREEP to export current subtree.
Optional argument VISIBLE-ONLY to only export visible content.
Optional argument BODY-ONLY export only the body.
Optional argument INFO is a plist of options."
(let ((org-mind-map-display nil)
(filename (org-mind-map-default-filename subtreep))
(linksp (y-or-n-p "Include org links? ")))
(if subtreep (org-narrow-to-subtree))
(org-mind-map-write-named filename (concat filename ".dot") linksp)
(widen)))
(defun ox-graphviz-export-dot-and-open (&optional async subtreep visible-only body-only info)
"Export the current buffer to a graphviz diagram and a dot file, and open the output file.
Optional argument ASYNC to asynchronously export.
Optional argument SUBTREEP to export current subtree.
Optional argument VISIBLE-ONLY to only export visible content.
Optional argument BODY-ONLY export only the body.
Optional argument INFO is a plist of options."
(let ((org-mind-map-display (or org-mind-map-display 'current))
(filename (org-mind-map-default-filename subtreep))
(linksp (y-or-n-p "Include org links? ")))
(if subtreep (org-narrow-to-subtree))
(org-mind-map-write-named filename (concat filename ".dot") linksp)
(widen)))
(defun org-mind-map-export-message nil
"Message string for `org-export-dispatch' buffer."
(if (> (length org-mind-map-dot-output) 1)
"Select output file format"
(concat "As " (car org-mind-map-dot-output) " file")))
(org-export-define-derived-backend 'graphviz 'org
:menu-entry
'(?g "Export to graphviz diagram"
((?f "Create graph" ox-graphviz-export)
(?o "Create graph and open" ox-graphviz-export-and-open)
(?d "Create graph & dot file" ox-graphviz-export-dot)
(?O "Create graph & dot file, and open graph" ox-graphviz-export-dot-and-open))))
;; Add a tool bar icon
;; (define-key org-mode-map [tool-bar org-button]
;; '(menu-item "Write the org-mode file mind map to disk." org-mind-map-write-with-prompt
;; :image (image :type xpm :file "info.xpm")
;; ))
;; Add menu items
;; (define-key org-mode-map [menu-bar Org Diagram]
;; (cons "Graphviz diagram" (make-sparse-keymap "Graphviz diagram")))
;; (define-key org-mode-map [menu-bar Org Diagram all]
;; '("Diagram of whole buffer" . org-mind-map-write))
;; (define-key org-mode-map [menu-bar Org Diagram current]
;; '("Diagram of current tree" . org-mind-map-write-current-tree))
;; (define-key org-mode-map [menu-bar Org Diagram branch]
;; '("Diagram of current branch" . org-mind-map-write-current-branch))
;; (global-set-key (kbd "<f4>") 'org-mind-map-write)
(provide 'org-mind-map)
;;; org-mind-map.el ends here