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