dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

ox-freemind.el (19176B)


      1 ;;; ox-freemind.el --- Freemind Mindmap Back-End for Org Export Engine
      2 
      3 ;; Copyright (C) 2013-2021  Free Software Foundation, Inc.
      4 
      5 ;; Author: Jambunathan K <kjambunathan at gmail dot com>
      6 ;; Keywords: outlines, hypermedia, calendar, wp
      7 
      8 ;; This file is not part of GNU Emacs.
      9 
     10 ;; This program is free software: you can redistribute it and/or modify
     11 ;; it under the terms of the GNU General Public License as published by
     12 ;; the Free Software Foundation, either version 3 of the License, or
     13 ;; (at your option) any later version.
     14 
     15 ;; This program is distributed in the hope that it will be useful,
     16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18 ;; GNU General Public License for more details.
     19 
     20 ;; You should have received a copy of the GNU General Public License
     21 ;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
     22 
     23 ;;; Commentary:
     24 
     25 ;; This library implements a Freemind Mindmap back-end for Org generic
     26 ;; exporter.
     27 
     28 ;; To test it, run:
     29 ;;
     30 ;;   M-x org-freemind-export-to-freemind
     31 ;;
     32 ;; in an Org mode buffer.  See ox.el for more details on how this
     33 ;; exporter works.
     34 
     35 ;;; Code:
     36 
     37 ;;; Dependencies
     38 
     39 (require 'ox-html)
     40 
     41 
     42 
     43 ;;; Define Back-End
     44 
     45 (org-export-define-derived-backend 'freemind 'html
     46   :menu-entry
     47   '(?f "Export to Freemind Mindmap"
     48        ((?f "As Freemind Mindmap file" org-freemind-export-to-freemind)
     49 	(?o "As Freemind Mindmap file and open"
     50 	    (lambda (a s v b)
     51 	      (if a (org-freemind-export-to-freemind t s v b)
     52 		(org-open-file (org-freemind-export-to-freemind nil s v b)))))))
     53   :translate-alist '((headline . org-freemind-headline)
     54 		     (template . org-freemind-template)
     55 		     (inner-template . org-freemind-inner-template)
     56 		     (section . org-freemind-section)
     57 		     (entity . org-freemind-entity))
     58   :filters-alist '((:filter-options . org-freemind-options-function)
     59 		   (:filter-final-output . org-freemind-final-function)))
     60 
     61 
     62 
     63 ;;; User Configuration Variables
     64 
     65 (defgroup org-export-freemind nil
     66   "Options for exporting Org mode files to Freemind Mindmap."
     67   :tag "Org Export Freemind Mindmap"
     68   :group 'org-export)
     69 
     70 (defcustom org-freemind-styles
     71   '((default . "<node>\n</node>")
     72     (0 . "<node COLOR=\"#000000\">\n<font NAME=\"SansSerif\" SIZE=\"20\"/>\n</node>")
     73     (1 . "<node COLOR=\"#0033ff\">\n<edge STYLE=\"sharp_bezier\" WIDTH=\"8\"/>\n<font NAME=\"SansSerif\" SIZE=\"18\"/>\n</node>")
     74     (2 . "<node COLOR=\"#00b439\">\n<edge STYLE=\"bezier\" WIDTH=\"thin\"/>\n<font NAME=\"SansSerif\" SIZE=\"16\"/>\n</node>")
     75     (3 . "<node COLOR=\"#990000\" FOLDED=\"true\">\n<font NAME=\"SansSerif\" SIZE=\"14\"/>\n</node>")
     76     (4 . "<node COLOR=\"#111111\">\n</node>"))
     77   "List of Freemind node styles.
     78 Each entry is of the form (STYLE-NAME . STYLE-SPEC).  STYLE-NAME
     79 can be one of an integer (signifying an outline level), a string
     80 or the symbol `default'.  STYLE-SPEC, a string, is a Freemind
     81 node style."
     82   :type '(alist :options (default 0 1 2 3)
     83                 :key-type (choice :tag "Style tag"
     84 				  (integer :tag "Outline level")
     85 				  (const :tag "Default value" default)
     86 				  (string :tag "Node style"))
     87                 :value-type (string :tag "Style spec"))
     88   :group 'org-export-freemind)
     89 
     90 (defcustom org-freemind-style-map-function 'org-freemind-style-map--automatic
     91   "Function to map an Org element to it's node style.
     92 The mapping function takes two arguments an Org ELEMENT and INFO.
     93 ELEMENT can be one of the following types - `org-data',
     94 `headline' or `section'.  INFO is a plist holding contextual
     95 information during export.  The function must return a STYLE-SPEC
     96 to be applied to ELEMENT.
     97 
     98 See `org-freemind-style-map--automatic' for a sample style
     99 function.  See `org-freemind-styles' for a list of named styles."
    100   :type '(radio
    101 	  (function-item org-freemind-style-map--automatic)
    102 	  (function-item org-freemind-style-map--default)
    103 	  function)
    104   :group 'org-export-freemind)
    105 
    106 (defcustom org-freemind-section-format 'note
    107   "Specify how outline sections are to be formatted.
    108 If `inline', append it to the contents of it's heading node.  If
    109 `note', attach it as a note to it's heading node.  If `node',
    110 attach it as a separate node to it's heading node.
    111 
    112 Use `note', if the input Org file contains large sections.  Use
    113 `node', if the Org file contains mid-sized sections that need to
    114 stand apart.  Otherwise, use `inline'."
    115   :type '(choice
    116 	  (const :tag "Append to outline title" inline)
    117 	  (const :tag "Attach as a note" note)
    118 	  (const :tag "Create a separate node" node))
    119   :group 'org-export-freemind)
    120 
    121 ;;;; Debugging
    122 
    123 (defcustom org-freemind-pretty-output nil
    124   "Enable this to generate pretty Freemind Mindmap."
    125   :type 'boolean
    126   :group 'org-export-freemind)
    127 
    128 
    129 ;;; Internal Functions
    130 
    131 ;;;; XML Manipulation
    132 
    133 (defun org-freemind--serialize (parsed-xml &optional contents)
    134   "Convert PARSED-XML in to XML string.
    135 PARSED-XML is a parse tree as returned by
    136 `libxml-parse-xml-region'.  CONTENTS is an optional string.
    137 
    138 Ignore CONTENTS, if PARSED-XML is not a sole XML element.
    139 Otherwise, append CONTENTS to the contents of top-level element
    140 in PARSED-XML.
    141 
    142 This is an inverse function of `libxml-parse-xml-region'.
    143 
    144 For purposes of Freemind export, PARSED-XML is a node style
    145 specification - \"<node ...>...</node>\" - as a parse tree."
    146   (when contents
    147     (assert (symbolp (car parsed-xml))))
    148   (cond
    149    ((null parsed-xml) "")
    150    ((stringp parsed-xml) parsed-xml)
    151    ((symbolp (car parsed-xml))
    152     (let ((attributes (mapconcat
    153 		       (lambda (av)
    154 			 (format "%s=\"%s\"" (car av) (cdr av)))
    155 		       (cadr parsed-xml) " ")))
    156       (if (or (cddr parsed-xml) contents)
    157 	  (format "\n<%s%s>%s\n</%s>"
    158 		  (car parsed-xml)
    159 		  (if (string= attributes "") "" (concat " " attributes))
    160 		  (concat (org-freemind--serialize (cddr parsed-xml))
    161 			  contents )
    162 		  (car parsed-xml))
    163 	(format "\n<%s%s/>"
    164 		(car parsed-xml)
    165 		(if (string= attributes "") "" (concat " " attributes))))))
    166    (t (mapconcat #'org-freemind--serialize parsed-xml ""))))
    167 
    168 (defun org-freemind--parse-xml (xml-string)
    169   "Return parse tree for XML-STRING using `libxml-parse-xml-region'.
    170 For purposes of Freemind export, XML-STRING is a node style
    171 specification - \"<node ...>...</node>\" - as a string."
    172   (with-temp-buffer
    173     (insert (or xml-string ""))
    174     (libxml-parse-xml-region (point-min) (point-max))))
    175 
    176 
    177 ;;;; Style mappers :: Default and Automatic layout
    178 
    179 (defun org-freemind-style-map--automatic (element info)
    180   "Return a node style corresponding to relative outline level of ELEMENT.
    181 ELEMENT can be any of the following types - `org-data',
    182 `headline' or `section'.  See `org-freemind-styles' for style
    183 mappings of different outline levels."
    184   (let ((style-name
    185 	 (case (org-element-type element)
    186 	   (headline
    187 	    (org-export-get-relative-level element info))
    188 	   (section
    189 	    (let ((parent (org-export-get-parent-headline element)))
    190 	      (if (not parent) 1
    191 		(1+ (org-export-get-relative-level parent info)))))
    192 	   (t 0))))
    193     (or (assoc-default style-name org-freemind-styles)
    194 	(assoc-default 'default org-freemind-styles)
    195 	"<node></node>")))
    196 
    197 (defun org-freemind-style-map--default (element info)
    198   "Return the default style for all ELEMENTs.
    199 ELEMENT can be any of the following types - `org-data',
    200 `headline' or `section'.  See `org-freemind-styles' for current
    201 value of default style."
    202   (or (assoc-default 'default org-freemind-styles)
    203       "<node></node>"))
    204 
    205 
    206 ;;;; Helpers :: Retrieve, apply Freemind styles
    207 
    208 (defun org-freemind--get-node-style (element info)
    209   "Return Freemind node style applicable for HEADLINE.
    210 ELEMENT is an Org element of type `org-data', `headline' or
    211 `section'.  INFO is a plist holding contextual information."
    212   (unless (fboundp org-freemind-style-map-function)
    213     (setq org-freemind-style-map-function 'org-freemind-style-map--default))
    214   (let ((style (funcall org-freemind-style-map-function element info)))
    215     ;; Sanitize node style.
    216 
    217     ;; Loop through the attributes of node element and purge those
    218     ;; attributes that look suspicious.  This is an extra bit of work
    219     ;; that allows one to copy verbatim node styles from an existing
    220     ;; Freemind Mindmap file without messing with the exported data.
    221     (let* ((data (org-freemind--parse-xml style))
    222 	   (attributes (cadr data))
    223 	   (ignored-attrs '(POSITION FOLDED TEXT CREATED ID
    224 				     MODIFIED)))
    225       (let (attr)
    226 	(while (setq attr (pop ignored-attrs))
    227 	  (setq attributes (assq-delete-all attr attributes))))
    228       (when data (setcar (cdr data) attributes))
    229       (org-freemind--serialize data))))
    230 
    231 (defun org-freemind--build-stylized-node (style-1 style-2 &optional contents)
    232   "Build a Freemind node with style STYLE-1 + STYLE-2 and add CONTENTS to it.
    233 STYLE-1 and STYLE-2 are Freemind node styles as a string.
    234 STYLE-1 is the base node style and STYLE-2 is the overriding
    235 style that takes precedence over STYLE-1.  CONTENTS is a string.
    236 
    237 Return value is a Freemind node with following properties:
    238 
    239   1. The attributes of \"<node ...> </node>\" element is the union
    240      of corresponding attributes of STYLE-1 and STYLE-2.  When
    241      STYLE-1 and STYLE-2 specify values for the same attribute
    242      name, choose the attribute value from STYLE-2.
    243 
    244   2. The children of \"<node ...> </node>\" element is the union of
    245      top-level children of STYLE-1 and STYLE-2 with CONTENTS
    246      appended to it.  When STYLE-1 and STYLE-2 share a child
    247      element of same type, the value chosen is that from STYLE-2.
    248 
    249 For example, merging with following parameters
    250 
    251   STYLE-1  =>
    252               <node COLOR=\"#00b439\" STYLE=\"Bubble\">
    253                 <edge STYLE=\"bezier\" WIDTH=\"thin\"/>
    254                 <font NAME=\"SansSerif\" SIZE=\"16\"/>
    255               </node>
    256 
    257   STYLE-2  =>
    258               <node COLOR=\"#990000\" FOLDED=\"true\">
    259                 <font NAME=\"SansSerif\" SIZE=\"14\"/>
    260               </node>
    261 
    262   CONTENTS =>
    263                <attribute NAME=\"ORGTAG\" VALUE=\"@home\"/>
    264 
    265 will result in following node:
    266 
    267   RETURN   =>
    268                <node STYLE=\"Bubble\" COLOR=\"#990000\" FOLDED=\"true\">
    269                  <edge STYLE=\"bezier\" WIDTH=\"thin\"/>
    270                  <font NAME=\"SansSerif\" SIZE=\"14\"/>
    271                  <attribute NAME=\"ORGTAG\" VALUE=\"@home\"/>
    272                </node>."
    273   (let* ((data1 (org-freemind--parse-xml (or style-1 "")))
    274 	 (data2 (org-freemind--parse-xml (or style-2 "")))
    275 	 (attr1 (cadr data1))
    276 	 (attr2 (cadr data2))
    277 	 (merged-attr attr2)
    278 	 (children1 (cddr data1))
    279 	 (children2 (cddr data2))
    280 	 (merged-children children2))
    281     (let (attr)
    282       (while (setq attr (pop attr1))
    283 	(unless (assq (car attr) merged-attr)
    284 	  (push attr merged-attr))))
    285     (let (child)
    286       (while (setq child (pop children1))
    287 	(when (or (stringp child) (not (assq (car child) merged-children)))
    288 	  (push child merged-children))))
    289     (let ((merged-data (nconc (list 'node merged-attr) merged-children)))
    290       (org-freemind--serialize merged-data contents))))
    291 
    292 
    293 ;;;; Helpers :: Node contents
    294 
    295 (defun org-freemind--richcontent (type contents &optional css-style)
    296   (let* ((type (case type
    297 		 (note "NOTE")
    298 		 (node "NODE")
    299 		 (t "NODE")))
    300 	 (contents (org-trim contents)))
    301     (if (string= (org-trim contents) "") ""
    302       (format "\n<richcontent TYPE=\"%s\">%s\n</richcontent>"
    303 	      type
    304 	      (format "\n<html>\n<head>%s\n</head>\n%s\n</html>"
    305 		      (or css-style "")
    306 		      (format "<body>\n%s\n</body>" contents))))))
    307 
    308 (defun org-freemind--build-node-contents (element contents info)
    309   (let* ((title (case (org-element-type element)
    310 		  (headline
    311 		   (org-element-property :title element))
    312 		  (org-data
    313 		   (plist-get info :title))
    314 		  (t (error "Shouldn't come here"))))
    315 	 (element-contents (org-element-contents element))
    316 	 (section (assq 'section element-contents))
    317 	 (section-contents
    318 	  (let ((backend (org-export-create-backend
    319 			  :parent (org-export-backend-name
    320 				   (plist-get info :back-end))
    321 			  :transcoders '((section . (lambda (e c i) c))))))
    322 	    (org-export-data-with-backend section backend info)))
    323 	 (itemized-contents-p (let ((first-child-headline
    324 				     (org-element-map element-contents
    325 					 'headline 'identity info t)))
    326 				(when first-child-headline
    327 				  (org-export-low-level-p first-child-headline
    328 							  info))))
    329 	 (node-contents (concat section-contents
    330 				(when itemized-contents-p
    331 				  contents))))
    332     (concat (let ((title (org-export-data title info)))
    333 	      (case org-freemind-section-format
    334 		(inline
    335 		  (org-freemind--richcontent
    336 		   'node (concat (format "\n<h2>%s</h2>" title)
    337 				 node-contents) ))
    338 		(note
    339 		 (concat (org-freemind--richcontent
    340 			  'node (format "\n<p>%s\n</p>" title))
    341 			 (org-freemind--richcontent
    342 			  'note node-contents)))
    343 		(node
    344 		 (concat
    345 		  (org-freemind--richcontent
    346 		   'node (format "\n<p>%s\n</p>" title))
    347 		  (when section
    348 		    (org-freemind--build-stylized-node
    349 		     (org-freemind--get-node-style section info) nil
    350 		     (org-freemind--richcontent 'node node-contents)))))))
    351 	    (unless itemized-contents-p
    352 	      contents))))
    353 
    354 
    355 
    356 ;;; Template
    357 
    358 (defun org-freemind-template (contents info)
    359   "Return complete document string after Freemind Mindmap conversion.
    360 CONTENTS is the transcoded contents string.  RAW-DATA is the
    361 original parsed data.  INFO is a plist holding export options."
    362   (format
    363    "<map version=\"0.9.0\">\n%s\n</map>"
    364    (org-freemind--build-stylized-node
    365     (org-freemind--get-node-style nil info) nil
    366     (let ((org-data (plist-get info :parse-tree)))
    367       (org-freemind--build-node-contents org-data contents info)))))
    368 
    369 (defun org-freemind-inner-template (contents info)
    370   "Return body of document string after Freemind Mindmap conversion.
    371 CONTENTS is the transcoded contents string.  INFO is a plist
    372 holding export options."
    373   contents)
    374 
    375 ;;;; Tags
    376 
    377 (defun org-freemind--tags (tags)
    378   (mapconcat (lambda (tag)
    379 	       (format "\n<attribute NAME=\"%s\" VALUE=\"%s\"/>" tag ""))
    380 	     tags "\n"))
    381 
    382 
    383 
    384 ;;; Transcode Functions
    385 
    386 ;;;; Entity
    387 
    388 (defun org-freemind-entity (entity contents info)
    389   "Transcode an ENTITY object from Org to Freemind Mindmap.
    390 CONTENTS are the definition itself.  INFO is a plist holding
    391 contextual information."
    392   (org-element-property :utf-8 entity))
    393 
    394 ;;;; Headline
    395 
    396 (defun org-freemind-headline (headline contents info)
    397   "Transcode a HEADLINE element from Org to Freemind Mindmap.
    398 CONTENTS holds the contents of the headline.  INFO is a plist
    399 holding contextual information."
    400   ;; Empty contents?
    401   (setq contents (or contents ""))
    402   (let* ((numberedp (org-export-numbered-headline-p headline info))
    403 	 (level (org-export-get-relative-level headline info))
    404 	 (text (org-export-data (org-element-property :title headline) info))
    405 	 (todo (and (plist-get info :with-todo-keywords)
    406 		    (let ((todo (org-element-property :todo-keyword headline)))
    407 		      (and todo (org-export-data todo info)))))
    408 	 (todo-type (and todo (org-element-property :todo-type headline)))
    409 	 (tags (and (plist-get info :with-tags)
    410 		    (org-export-get-tags headline info)))
    411 	 (priority (and (plist-get info :with-priority)
    412 			(org-element-property :priority headline)))
    413 	 (section-number (and (not (org-export-low-level-p headline info))
    414 			      (org-export-numbered-headline-p headline info)
    415 			      (mapconcat 'number-to-string
    416 					 (org-export-get-headline-number
    417 					  headline info) ".")))
    418 	 ;; Create the headline text.
    419 	 (full-text (org-export-data (org-element-property :title headline)
    420 				     info))
    421 	 ;; Headline order (i.e, first digit of the section number)
    422 	 (headline-order (car (org-export-get-headline-number headline info))))
    423     (cond
    424      ;; Case 1: This is a footnote section: ignore it.
    425      ((org-element-property :footnote-section-p headline) nil)
    426      ;; Case 2. This is a deep sub-tree, export it as a list item.
    427      ;;         Delegate the actual export to `html' backend.
    428      ((org-export-low-level-p headline info)
    429       (org-html-headline headline contents info))
    430      ;; Case 3. Standard headline.  Export it as a section.
    431      (t
    432       (let* ((section-number (mapconcat 'number-to-string
    433 					(org-export-get-headline-number
    434 					 headline info) "-"))
    435 	     (ids (remove 'nil
    436 			  (list (org-element-property :CUSTOM_ID headline)
    437 				(concat "sec-" section-number)
    438 				(org-element-property :ID headline))))
    439 	     (preferred-id (car ids))
    440 	     (extra-ids (cdr ids))
    441 	     (left-p (zerop (% headline-order 2))))
    442 	(org-freemind--build-stylized-node
    443 	 (org-freemind--get-node-style headline info)
    444 	 (format "<node ID=\"%s\" POSITION=\"%s\" FOLDED=\"%s\">\n</node>"
    445 		 preferred-id
    446 		 (if left-p "left" "right")
    447 		 (if (= level 1) "true" "false"))
    448 	 (concat (org-freemind--build-node-contents headline contents info)
    449 		 (org-freemind--tags tags))))))))
    450 
    451 
    452 ;;;; Section
    453 
    454 (defun org-freemind-section (section contents info)
    455   "Transcode a SECTION element from Org to Freemind Mindmap.
    456 CONTENTS holds the contents of the section.  INFO is a plist
    457 holding contextual information."
    458   (let ((parent (org-export-get-parent-headline section)))
    459     (when (and parent (org-export-low-level-p parent info))
    460       contents)))
    461 
    462 
    463 
    464 ;;; Filter Functions
    465 
    466 (defun org-freemind-final-function (contents backend info)
    467   "Return CONTENTS as pretty XML using `indent-region'."
    468   (if (not org-freemind-pretty-output) contents
    469     (with-temp-buffer
    470       (nxml-mode)
    471       (insert contents)
    472       (indent-region (point-min) (point-max))
    473       (buffer-substring-no-properties (point-min) (point-max)))))
    474 
    475 (defun org-freemind-options-function (info backend)
    476   "Install script in export options when appropriate.
    477 EXP-PLIST is a plist containing export options.  BACKEND is the
    478 export back-end currently used."
    479   ;; Freemind/Freeplane doesn't seem to like named html entities in
    480   ;; richcontent.  For now, turn off smart quote processing so that
    481   ;; entities like "&rsquo;" & friends are avoided in the exported
    482   ;; output.
    483   (plist-put info :with-smart-quotes nil))
    484 
    485 
    486 
    487 ;;; End-user functions
    488 
    489 ;;;###autoload
    490 (defun org-freemind-export-to-freemind
    491   (&optional async subtreep visible-only body-only ext-plist)
    492   "Export current buffer to a Freemind Mindmap file.
    493 
    494 If narrowing is active in the current buffer, only export its
    495 narrowed part.
    496 
    497 If a region is active, export that region.
    498 
    499 A non-nil optional argument ASYNC means the process should happen
    500 asynchronously.  The resulting file should be accessible through
    501 the `org-export-stack' interface.
    502 
    503 When optional argument SUBTREEP is non-nil, export the sub-tree
    504 at point, extracting information from the headline properties
    505 first.
    506 
    507 When optional argument VISIBLE-ONLY is non-nil, don't export
    508 contents of hidden elements.
    509 
    510 When optional argument BODY-ONLY is non-nil, only write code
    511 between \"<body>\" and \"</body>\" tags.
    512 
    513 EXT-PLIST, when provided, is a property list with external
    514 parameters overriding Org default settings, but still inferior to
    515 file-local settings.
    516 
    517 Return output file's name."
    518   (interactive)
    519   (let* ((extension (concat ".mm" ))
    520 	 (file (org-export-output-file-name extension subtreep))
    521 	 (org-export-coding-system 'utf-8))
    522     (org-export-to-file 'freemind file
    523       async subtreep visible-only body-only ext-plist)))
    524 
    525 (provide 'ox-freemind)
    526 
    527 ;;; ox-freemind.el ends here