plugins/raphael.export.js
[raphael] / plugins / raphael.export.js
1 /**
2  * Raphael.Export https://github.com/ElbertF/Raphael.Export
3  *
4  * Licensed under the MIT license:
5  * http://www.opensource.org/licenses/mit-license.php
6  *
7  */
8
9 (function(R) {
10         /**
11         * Escapes string for XML interpolation
12         * @param value string or number value to escape
13         * @returns string escaped
14         */
15         function escapeXML(s) {
16                 if ( typeof s === 'number' ) return s.toString();
17
18                 var replace = { '&': 'amp', '<': 'lt', '>': 'gt', '"': 'quot', '\'': 'apos' };
19
20                 for ( var entity in replace ) {
21                         s = s.replace(new RegExp(entity, 'g'), '&' + replace[entity] + ';');
22                 }
23
24                 return s;
25         }
26
27         /**
28         * Generic map function
29         * @param iterable the array or object to be mapped
30         * @param callback the callback function(element, key)
31         * @returns array
32         */
33         function map(iterable, callback) {
34                 var mapped = new Array;
35
36                 for ( var i in iterable ) {
37                         if ( iterable.hasOwnProperty(i) ) {
38                                 var value = callback.call(this, iterable[i], i);
39
40                                 if ( value !== null ) mapped.push(value);
41                         }
42                 }
43
44                 return mapped;
45         }
46
47         /**
48         * Generic reduce function
49         * @param iterable array or object to be reduced
50         * @param callback the callback function(initial, element, i)
51         * @param initial the initial value
52         * @return the reduced value
53         */
54         function reduce(iterable, callback, initial) {
55                 for ( var i in iterable ) {
56                         if ( iterable.hasOwnProperty(i) ) {
57                                 initial = callback.call(this, initial, iterable[i], i);
58                         }
59                 }
60
61                 return initial;
62         }
63
64         /**
65         * Utility method for creating a tag
66         * @param name the tag name, e.g., 'text'
67         * @param attrs the attribute string, e.g., name1="val1" name2="val2"
68         * or attribute map, e.g., { name1 : 'val1', name2 : 'val2' }
69         * @param content the content string inside the tag
70         * @returns string of the tag
71         */
72         function tag(name, attrs, matrix, content) {
73                 if ( typeof content === 'undefined' || content === null ) {
74                         content = '';
75                 }
76
77                 if ( typeof attrs === 'object' ) {
78                         attrs = map(attrs, function(element, name) {
79                                 if ( name === 'transform') return;
80
81                                 return name + '="' + escapeXML(element) + '"';
82                         }).join(' ');
83                 }
84
85                 return '<' + name + ( matrix ? ' transform="matrix(' + matrix.toString().replace(/^matrix\(|\)$/g, '') + ')" ' : ' ' ) + attrs + '>' +
86             escapeXML(content) +
87             '</' + name + '>';
88         }
89
90         /**
91         * @return object the style object
92         */
93         function extractStyle(node) {
94                 return {
95                         font: {
96                                 family: node.attrs.font.replace(/^.*?"(\w+)".*$/, '$1'),
97                                 size:   typeof node.attrs['font-size'] === 'undefined' ? null : node.attrs['font-size']
98                                 }
99                         };
100         }
101
102         /**
103         * @param style object from style()
104         * @return string
105         */
106         function styleToString(style) {
107                 // TODO figure out what is 'normal'
108                 return 'font: normal normal normal 10px/normal ' + style.font.family + ( style.font.size === null ? '' : '; font-size: ' + style.font.size + 'px' );
109         }
110
111         /**
112         * Computes tspan dy using font size. This formula was empircally determined
113         * using a best-fit line. Works well in both VML and SVG browsers.
114         * @param fontSize number
115         * @return number
116         */
117         function computeTSpanDy(fontSize, line, lines) {
118                 if ( fontSize === null ) fontSize = 10;
119
120                 //return fontSize * 4.5 / 13
121                 return fontSize * 4.5 / 13 * ( line - .2 - lines / 2 ) * 3.5;
122         }
123
124         var serializer = {
125                 'text': function(node) {
126                         style = extractStyle(node);
127
128                         var tags = new Array;
129
130                         map(node.attrs['text'].split('\n'), function(text, iterable, line) {
131                                 line = line || 0;
132                                 tags.push(tag(
133                                         'text',
134                                         reduce(
135                                                 node.attrs,
136                                                 function(initial, value, name) {
137                                                         if ( name !== 'text' && name !== 'w' && name !== 'h' ) {
138                                                                 if ( name === 'font-size') value = value + 'px';
139
140                                                                 initial[name] = escapeXML(value.toString());
141                                                         }
142
143                                                         return initial;
144                                                 },
145                                                 { style: 'text-anchor: middle; ' + styleToString(style) + ';' }
146                                                 ),
147                                         node.matrix,
148                                         tag('tspan',
149                             {
150                                 dy: computeTSpanDy(style.font.size, line + 1, node.attrs['text'].split('\n').length)
151                             },
152                             null,
153                             text
154                     )
155                                 ));
156                         });
157
158                         return tags;
159                 },
160                 'path' : function(node) {
161                         var initial = ( node.matrix.a === 1 && node.matrix.d === 1 ) ? {} : { 'transform' : node.matrix.toString() };
162
163                         return tag(
164                                 'path',
165                                 reduce(
166                                         node.attrs,
167                                         function(initial, value, name) {
168                                                 if ( name === 'path' ) name = 'd';
169
170                                                 initial[name] = value.toString();
171
172                                                 return initial;
173                                         },
174                                         {}
175                                 ),
176                                 node.matrix
177                                 );
178                 }
179                 // Other serializers should go here
180         };
181
182         R.fn.toSVG = function() {
183                 var
184                         paper   = this,
185                         restore = { svg: R.svg, vml: R.vml },
186                         svg     = '<svg style="overflow: hidden; position: relative;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="' + paper.width + '" version="1.1" height="' + paper.height + '">'
187                         ;
188
189                 R.svg = true;
190                 R.vml = false;
191
192                 for ( var node = paper.bottom; node != null; node = node.next ) {
193                         if ( node.node.style.display === 'none' ) continue;
194
195                         var attrs = '';
196
197                         // Use serializer
198                         if ( typeof serializer[node.type] === 'function' ) {
199                                 svg += serializer[node.type](node);
200
201                                 continue;
202                         }
203
204                         switch ( node.type ) {
205                                 case 'image':
206                                         attrs += ' preserveAspectRatio="none"';
207                                         break;
208                         }
209
210                         for ( i in node.attrs ) {
211                                 var name = i;
212
213                                 switch ( i ) {
214                                         case 'src':
215                                                 name = 'xlink:href';
216
217                                                 break;
218                                         case 'transform':
219                                                 name = '';
220
221                                                 break;
222                                 }
223
224                                 if ( name ) {
225                                         attrs += ' ' + name + '="' + escapeXML(node.attrs[i].toString()) + '"';
226                                 }
227                         }
228
229                         svg += '<' + node.type + ' transform="matrix(' + node.matrix.toString().replace(/^matrix\(|\)$/g, '') + ')"' + attrs + '></' + node.type + '>';
230                 }
231
232                 svg += '</svg>';
233
234                 R.svg = restore.svg;
235                 R.vml = restore.vml;
236
237                 return svg;
238         };
239 })(window.Raphael);