1/*
2* Copyright (c) 2018 (https://github.com/phase1geo/Minder)
3*
4* This program is free software; you can redistribute it and/or
5* modify it under the terms of the GNU General Public
6* License as published by the Free Software Foundation; either
7* version 2 of the License, or (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 GNU
12* General Public License for more details.
13*
14* You should have received a copy of the GNU General Public
15* License along with this program; if not, write to the
16* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17* Boston, MA 02110-1301 USA
18*
19* Authored by: Trevor Williams <phase1geo@gmail.com>
20*/
21
22using GLib;
23using Gdk;
24using Gee;
25
26public class ExportFreemind : Export {
27
28  public ExportFreemind() {
29    base( "freemind", _( "Freemind" ), { ".mm" }, true, true );
30  }
31
32  /* Exports the given drawing area to the file of the given name */
33  public override bool export( string fname, DrawArea da ) {
34    Xml.Doc*  doc  = new Xml.Doc( "1.0" );
35    doc->set_root_element( export_map( da ) );
36    doc->save_format_file( fname, 1 );
37    delete doc;
38    return( false );
39  }
40
41  /* Generates the header for the document */
42  private Xml.Node* export_map( DrawArea da ) {
43    Xml.Node* map = new Xml.Node( null, "map" );
44    map->new_prop( "version", "1.0.1" );
45    map->add_child( new Xml.Node.comment( _( "Generated by Minder" ) + " " + Minder.version ) );
46    var nodes = da.get_nodes();
47    for( int i=0; i<nodes.length; i++ ) {
48      map->add_child( export_node( nodes.index( i ), da ) );
49    }
50    return( map );
51  }
52
53  /* Exports the given node information */
54  private Xml.Node* export_node( Node node, DrawArea da ) {
55
56    Xml.Node* n = new Xml.Node( null, "node" );
57
58    n->new_prop( "ID", "id_" + node.id().to_string() );
59    n->new_prop( "TEXT", node.name.text.text );
60    if( node.linked_node != null ) {
61      n->new_prop( "LINK", "#id_" + node.linked_node.id().to_string() );
62    }
63    n->new_prop( "FOLDED", node.folded.to_string() );
64    n->new_prop( "COLOR", Utils.color_from_rgba( node.link_color ) );
65    n->new_prop( "POSITION", ((node.side == NodeSide.LEFT) ? "left" : "right") );
66
67    if( node.group ) {
68      n->add_child( export_cloud( node, da ) );
69    }
70
71    n->add_child( export_edge( node, da ) );
72    n->add_child( export_font( node, da ) );
73
74    if( node.note != "" ) {
75      n->add_child( export_note( node ) );
76    }
77
78    /* Add arrowlinks */
79    int         index = 0;
80    Connection? conn  = null;
81    while( (conn = da.get_connections().get_attached_connection( node, index++ )) != null ) {
82      if( conn.from_node == node ) {
83        n->add_child( export_arrowlink( conn, da ) );
84      }
85    }
86
87    /* Add nodes */
88    for( int i=0; i<node.children().length; i++ ) {
89      n->add_child( export_node( node.children().index( i ), da ) );
90    }
91
92    return( n );
93
94  }
95
96  /* Exports the cloud information */
97  private Xml.Node* export_cloud( Node node, DrawArea da ) {
98    Xml.Node* n = new Xml.Node( null, "cloud" );
99    return( n );
100  }
101
102  /* Exports the given node link as an edge */
103  private Xml.Node* export_edge( Node node, DrawArea da ) {
104    Xml.Node* n = new Xml.Node( null, "edge" );
105    n->new_prop( "STYLE", (node.style.link_type.name() == "curved") ? "bezier" : "linear" );
106    n->new_prop( "COLOR", Utils.color_from_rgba( node.link_color ) );
107    n->new_prop( "WIDTH", node.style.link_width.to_string() );
108    return( n );
109  }
110
111  /* Exports the given node font */
112  private Xml.Node* export_font( Node node, DrawArea da ) {
113    Xml.Node* n = new Xml.Node( null, "font" );
114    n->new_prop( "NAME",   node.style.node_font.get_family() );
115    n->new_prop( "SIZE",   (node.style.node_font.get_size() / Pango.SCALE).to_string() );
116    n->new_prop( "BOLD",   ((node.name.text.text.substring( 0, 3 ) == "<b>") || (node.name.text.text.substring( 0, 6 ) == "<i><b>")).to_string() );
117    n->new_prop( "ITALIC", ((node.name.text.text.substring( 0, 3 ) == "<i>") || (node.name.text.text.substring( 0, 6 ) == "<b><i>")).to_string() );
118    return( n );
119  }
120
121  private Xml.Node* export_note( Node node ) {
122
123    Xml.Node* rc   = new Xml.Node( null, "richcontent" );
124    Xml.Node* html = new Xml.Node( null, "html" );
125    Xml.Node* head = new Xml.Node( null, "head" );
126
127    var note_html = Utils.markdown_to_html( node.note, "body" );
128    var note_doc  = Xml.Parser.parse_memory( note_html, note_html.length );
129    var body      = note_doc->get_root_element()->copy( 1 );
130
131    html->add_child( head );
132    html->add_child( body );
133
134    rc->new_prop( "TYPE", "NOTE" );
135    rc->add_child( html );
136
137    delete note_doc;
138
139    return( rc );
140
141  }
142
143  /* Exports the given connection as an arrowlink */
144  private Xml.Node* export_arrowlink( Connection conn, DrawArea da ) {
145    Xml.Node* n = new Xml.Node( null, "arrowlink" );
146    if( conn.color != null ) {
147      n->new_prop( "COLOR", Utils.color_from_rgba( conn.color ) );
148    } else {
149      n->new_prop( "COLOR", Utils.color_from_rgba( da.get_theme().get_color( "connection_background" ) ) );
150    }
151    n->new_prop( "DESTINATION", "id_" + conn.to_node.id().to_string() );
152    n->new_prop( "STARTARROW",  ((conn.style.connection_arrow == "none") || (conn.style.connection_arrow == "fromto")) ? "None" : "Default" );
153    n->new_prop( "ENDARROW",    ((conn.style.connection_arrow == "none") || (conn.style.connection_arrow == "tofrom")) ? "None" : "Default" );
154    return( n );
155  }
156
157  /*
158   Reads the contents of an OPML file and creates a new document based on
159   the stored information.
160  */
161  public override bool import( string fname, DrawArea da ) {
162
163    /* Read in the contents of the Freemind file */
164    var doc = Xml.Parser.read_file( fname, null, (Xml.ParserOption.HUGE | Xml.ParserOption.RECOVER) );
165    if( doc == null ) {
166      return( false );
167    }
168
169    /* Load the contents of the file */
170    import_map( da, doc->get_root_element() );
171
172    /* Update the drawing area */
173    da.queue_draw();
174
175    /* Delete the OPML document */
176    delete doc;
177
178    return( true );
179
180  }
181
182  /* Parses the OPML head block for information that we will use */
183  private void import_map( DrawArea da, Xml.Node* n ) {
184
185    var color_map = new HashMap<string,RGBA?>();
186    var id_map    = new HashMap<string,int>();
187    var link_ids  = new Array<NodeLinkInfo?>();
188    var to_nodes  = new Array<string>();
189
190    /* Not sure what to do with the version information */
191    string? v = n->get_prop( "version" );
192    if( v != null ) {
193      /* Not sure what to do with this value */
194    }
195
196    for( Xml.Node* it = n->children; it != null; it = it->next ) {
197      if( it->type == Xml.ElementType.ELEMENT_NODE ) {
198        if( it->name == "node" ) {
199          var root = import_node( it, da, null, color_map, id_map, link_ids, to_nodes );
200          da.position_root_node( root );
201          da.get_nodes().append_val( root );
202        }
203      }
204    }
205
206    /* Connect linked nodes */
207    for( int i=0; i<link_ids.length; i++ ) {
208      link_ids.index( i ).node.linked_node = da.get_node( da.get_nodes(), id_map.get( link_ids.index( i ).id_str ) );
209    }
210
211    /* Finish up the connections */
212    for( int i=0; i<to_nodes.length; i++ ) {
213      if( id_map.has_key( to_nodes.index( i ) ) ) {
214        var to_node = da.get_node( da.get_nodes(), id_map.get( to_nodes.index( i ) ) );
215        if( to_node != null ) {
216          da.get_connections().complete_connection( i, to_node );
217        }
218      }
219    }
220
221  }
222
223  /* Parses the given Freemind node */
224  public Node import_node( Xml.Node* n, DrawArea da, Node? parent, HashMap<string,RGBA?> color_map, HashMap<string,int> id_map, Array<NodeLinkInfo?> link_ids, Array<string> to_nodes ) {
225
226    var node = new Node( da, da.layouts.get_default() );
227
228    /* Make sure the style has a default value */
229    node.style = StyleInspector.styles.get_style_for_level( ((parent == null) ? 0 : 1), null );
230
231    string? i = n->get_prop( "ID" );
232    if( i != null ) {
233      id_map.set( i, node.id() );
234    }
235
236    string? t = n->get_prop( "TEXT" );
237    if( t != null ) {
238      node.name.text.insert_text( 0, t );
239    }
240
241    string? f = n->get_prop( "FOLDED" );
242    if( f != null ) {
243      node.folded = bool.parse( f );
244    }
245
246    string? l = n->get_prop( "LINK" );
247    if( (l != null) && (l.substring( 0, 1 ) == "#") ) {
248      link_ids.append_val( NodeLinkInfo( l.substring( 1 ), node ) );
249    }
250
251    string? c = n->get_prop( "COLOR" );
252    if( c != null ) {
253      if( color_map.has_key( c ) ) {
254        node.link_color = color_map.get( c );
255      } else {
256        node.link_color = da.get_theme().next_color();
257        color_map.set( c, node.link_color );
258      }
259    }
260
261    string? p = n->get_prop( "POSITION" );
262    if( p != null ) {
263      node.side = (p == "left") ? NodeSide.LEFT : NodeSide.RIGHT;
264    }
265
266    /* Parse the child nodes */
267    for( Xml.Node* it = n->children; it != null; it = it->next ) {
268      if( it->type == Xml.ElementType.ELEMENT_NODE ) {
269        switch( it->name ) {
270          case "node"        :  import_node( it, da, node, color_map, id_map, link_ids, to_nodes );  break;
271          case "edge"        :  import_edge( it, node );  break;
272          case "font"        :  import_font( it, node );  break;
273          case "icon"        :  break;  // Not implemented
274          case "cloud"       :  import_cloud( it, node );  break;
275          case "arrowlink"   :  import_arrowlink( it, da, node, to_nodes );  break;
276          case "richcontent" :  import_richcontent( it, da, node );  break;
277        }
278      }
279    }
280
281    /* Attach the new node to its parent */
282    if( parent != null ) {
283      node.attach( parent, -1, da.get_theme() );
284    }
285
286    return( node );
287
288  }
289
290  private void import_edge( Xml.Node* n, Node node ) {
291
292    string? s = n->get_prop( "STYLE" );
293    if( s != null ) {
294      switch( s ) {
295        case "bezier" :  node.style.link_type = new LinkTypeCurved();    break;
296        case "linear" :  node.style.link_type = new LinkTypeStraight();  break;
297      }
298    }
299
300    string? c = n->get_prop( "COLOR" );
301    if( c != null ) {
302      /* Not implemented - link color and node color must be the same */
303    }
304
305    string? w = n->get_prop( "WIDTH" );
306    if( w != null ) {
307      node.style.link_width = int.parse( w );
308    }
309
310  }
311
312  private void import_font( Xml.Node* n, Node node ) {
313
314    string? f = n->get_prop( "NAME" );
315    if( f != null ) {
316      node.style.node_font.set_family( f );
317    }
318
319    string? s = n->get_prop( "SIZE" );
320    if( s != null ) {
321      node.style.node_font.set_size( int.parse( s ) * Pango.SCALE );
322    }
323
324    string? b = n->get_prop( "BOLD" );
325    if( b != null ) {
326      if( bool.parse( b ) ) {
327        node.name.text.insert_text( 0, "<b>" + node.name.text.text + "</b>" );
328      }
329    }
330
331    string? i = n->get_prop( "ITALIC" );
332    if( i != null ) {
333      if( bool.parse( i ) ) {
334        node.name.text.insert_text( 0, "<i>" + node.name.text.text + "</i>" );
335      }
336    }
337
338  }
339
340  private void import_cloud( Xml.Node* n, Node node ) {
341
342    node.group = true;
343
344  }
345
346  private void import_arrowlink( Xml.Node* n, DrawArea da, Node from_node, Array<string> to_nodes ) {
347
348    var conn        = new Connection( da, from_node );
349    var start_arrow = "None";
350    var end_arrow   = "None";
351
352    string? c = n->get_prop( "COLOR" );
353    if( c != null ) {
354      /* Not implemented */
355    }
356
357    string? d = n->get_prop( "DESTINATION" );
358    if( d != null ) {
359      to_nodes.append_val( d );
360    }
361
362    string? sa = n->get_prop( "STARTARROW" );
363    if( sa != null ) {
364      start_arrow = sa;
365    }
366
367    string? ea = n->get_prop( "ENDARROW" );
368    if( ea != null ) {
369      end_arrow = ea;
370    }
371
372    /* Stylize the arrow */
373    switch( start_arrow + end_arrow ) {
374      case "NoneNone"       :  conn.style.connection_arrow = "none";    break;
375      case "NoneDefault"    :  conn.style.connection_arrow = "fromto";  break;
376      case "DefaultNone"    :  conn.style.connection_arrow = "tofrom";  break;
377      case "DefaultDefault" :  conn.style.connection_arrow = "both";    break;
378    }
379
380    /* Add the connection to the connections list */
381    da.get_connections().add_connection( conn );
382
383  }
384
385  /* Parses richcontent for a note.  If found parses the note */
386  private void import_richcontent( Xml.Node* n, DrawArea da, Node node ) {
387
388    string? t = n->get_prop( "TYPE" );
389    if( (t != null) && (t == "NOTE") ) {
390      for( Xml.Node* it = n->children; it != null; it = it->next ) {
391        if( (it->type == Xml.ElementType.ELEMENT_NODE) && (it->name.down() == "html") ) {
392          HtmlToMarkdown.reset();
393          node.note = HtmlToMarkdown.parse_xml( it ).strip();
394        }
395      }
396    }
397
398  }
399
400}
401