1/* $Id$ $Revision$ */
2/* vim:set shiftwidth=4 ts=8: */
3
4/*************************************************************************
5 * Copyright (c) 2011 AT&T Intellectual Property
6 * All rights reserved. This program and the accompanying materials
7 * are made available under the terms of the Eclipse Public License v1.0
8 * which accompanies this distribution, and is available at
9 * http://www.eclipse.org/legal/epl-v10.html
10 *
11 * Contributors: See CVS logs. Details at http://www.graphviz.org/
12 *************************************************************************/
13
14
15#include "render.h"
16#include "htmltable.h"
17#include <limits.h>
18
19static char *strdup_and_subst_obj0 (char *str, void *obj, int escBackslash);
20
21static void storeline(GVC_t *gvc, textlabel_t *lp, char *line, char terminator)
22{
23 pointf size;
24 textspan_t *span;
25 static textfont_t tf;
26 int oldsz = lp->u.txt.nspans + 1;
27
28 lp->u.txt.span = ZALLOC(oldsz + 1, lp->u.txt.span, textspan_t, oldsz);
29 span = &(lp->u.txt.span[lp->u.txt.nspans]);
30 span->str = line;
31 span->just = terminator;
32 if (line && line[0]) {
33 tf.name = lp->fontname;
34 tf.size = lp->fontsize;
35 span->font = dtinsert(gvc->textfont_dt, &tf);
36 size = textspan_size(gvc, span);
37 }
38 else {
39 size.x = 0.0;
40 span->size.y = size.y = (int)(lp->fontsize * LINESPACING);
41 }
42
43 lp->u.txt.nspans++;
44 /* width = max line width */
45 lp->dimen.x = MAX(lp->dimen.x, size.x);
46 /* accumulate height */
47 lp->dimen.y += size.y;
48}
49
50/* compiles <str> into a label <lp> */
51void make_simple_label(GVC_t * gvc, textlabel_t * lp)
52{
53 char c, *p, *line, *lineptr, *str = lp->text;
54 unsigned char byte = 0x00;
55
56 lp->dimen.x = lp->dimen.y = 0.0;
57 if (*str == '\0')
58 return;
59
60 line = lineptr = NULL;
61 p = str;
62 line = lineptr = N_GNEW(strlen(p) + 1, char);
63 *line = 0;
64 while ((c = *p++)) {
65 byte = (unsigned char) c;
66 /* wingraphviz allows a combination of ascii and big-5. The latter
67 * is a two-byte encoding, with the first byte in 0xA1-0xFE, and
68 * the second in 0x40-0x7e or 0xa1-0xfe. We assume that the input
69 * is well-formed, but check that we don't go past the ending '\0'.
70 */
71 if ((lp->charset == CHAR_BIG5) && 0xA1 <= byte && byte <= 0xFE) {
72 *lineptr++ = c;
73 c = *p++;
74 *lineptr++ = c;
75 if (!c) /* NB. Protect against unexpected string end here */
76 break;
77 } else {
78 if (c == '\\') {
79 switch (*p) {
80 case 'n':
81 case 'l':
82 case 'r':
83 *lineptr++ = '\0';
84 storeline(gvc, lp, line, *p);
85 line = lineptr;
86 break;
87 default:
88 *lineptr++ = *p;
89 }
90 if (*p)
91 p++;
92 /* tcldot can enter real linend characters */
93 } else if (c == '\n') {
94 *lineptr++ = '\0';
95 storeline(gvc, lp, line, 'n');
96 line = lineptr;
97 } else {
98 *lineptr++ = c;
99 }
100 }
101 }
102
103 if (line != lineptr) {
104 *lineptr++ = '\0';
105 storeline(gvc, lp, line, 'n');
106 }
107
108 lp->space = lp->dimen;
109}
110
111/* make_label:
112 * Assume str is freshly allocated for this instance, so it
113 * can be freed in free_label.
114 */
115textlabel_t *make_label(void *obj, char *str, int kind, double fontsize, char *fontname, char *fontcolor)
116{
117 textlabel_t *rv = NEW(textlabel_t);
118 graph_t *g = NULL, *sg = NULL;
119 node_t *n = NULL;
120 edge_t *e = NULL;
121 char *s;
122
123 switch (agobjkind(obj)) {
124 case AGRAPH:
125 sg = (graph_t*)obj;
126 g = sg->root;
127 break;
128 case AGNODE:
129 n = (node_t*)obj;
130 g = agroot(agraphof(n));
131 break;
132 case AGEDGE:
133 e = (edge_t*)obj;
134 g = agroot(agraphof(aghead(e)));
135 break;
136 }
137 rv->fontname = fontname;
138 rv->fontcolor = fontcolor;
139 rv->fontsize = fontsize;
140 rv->charset = GD_charset(g);
141 if (kind & LT_RECD) {
142 rv->text = strdup(str);
143 if (kind & LT_HTML) {
144 rv->html = TRUE;
145 }
146 }
147 else if (kind == LT_HTML) {
148 rv->text = strdup(str);
149 rv->html = TRUE;
150 if (make_html_label(obj, rv)) {
151 switch (agobjkind(obj)) {
152 case AGRAPH:
153 agerr(AGPREV, "in label of graph %s\n",agnameof(sg));
154 break;
155 case AGNODE:
156 agerr(AGPREV, "in label of node %s\n", agnameof(n));
157 break;
158 case AGEDGE:
159 agerr(AGPREV, "in label of edge %s %s %s\n",
160 agnameof(agtail(e)), agisdirected(g)?"->":"--", agnameof(aghead(e)));
161 break;
162 }
163 }
164 }
165 else {
166 assert(kind == LT_NONE);
167 /* This call just processes the graph object based escape sequences. The formatting escape
168 * sequences (\n, \l, \r) are processed in make_simple_label. That call also replaces \\ with \.
169 */
170 rv->text = strdup_and_subst_obj0(str, obj, 0);
171 switch (rv->charset) {
172 case CHAR_LATIN1:
173 s = latin1ToUTF8(rv->text);
174 break;
175 default: /* UTF8 */
176 s = htmlEntityUTF8(rv->text, g);
177 break;
178 }
179 free(rv->text);
180 rv->text = s;
181 make_simple_label(GD_gvc(g), rv);
182 }
183 return rv;
184}
185
186/* free_textspan:
187 * Free resources related to textspan_t.
188 * tl is an array of cnt textspan_t's.
189 * It is also assumed that the text stored in the str field
190 * is all stored in one large buffer shared by all of the textspan_t,
191 * so only the first one needs to free its tlp->str.
192 */
193void free_textspan(textspan_t * tl, int cnt)
194{
195 int i;
196 textspan_t* tlp = tl;
197
198 if (!tl) return;
199 for (i = 0; i < cnt; i++) {
200 if ((i == 0) && tlp->str)
201 free(tlp->str);
202 if (tlp->layout && tlp->free_layout)
203 tlp->free_layout (tlp->layout);
204 tlp++;
205 }
206 free(tl);
207}
208
209void free_label(textlabel_t * p)
210{
211 if (p) {
212 free(p->text);
213 if (p->html) {
214 if (p->u.html) free_html_label(p->u.html, 1);
215 } else {
216 free_textspan(p->u.txt.span, p->u.txt.nspans);
217 }
218 free(p);
219 }
220}
221
222void emit_label(GVJ_t * job, emit_state_t emit_state, textlabel_t * lp)
223{
224 obj_state_t *obj = job->obj;
225 int i;
226 pointf p;
227 emit_state_t old_emit_state;
228
229 old_emit_state = obj->emit_state;
230 obj->emit_state = emit_state;
231
232 if (lp->html) {
233 emit_html_label(job, lp->u.html, lp);
234 obj->emit_state = old_emit_state;
235 return;
236 }
237
238 /* make sure that there is something to do */
239 if (lp->u.txt.nspans < 1)
240 return;
241
242 gvrender_begin_label(job, LABEL_PLAIN);
243 gvrender_set_pencolor(job, lp->fontcolor);
244
245 /* position for first span */
246 switch (lp->valign) {
247 case 't':
248 p.y = lp->pos.y + lp->space.y / 2.0 - lp->fontsize;
249 break;
250 case 'b':
251 p.y = lp->pos.y - lp->space.y / 2.0 + lp->dimen.y - lp->fontsize;
252 break;
253 case 'c':
254 default:
255 p.y = lp->pos.y + lp->dimen.y / 2.0 - lp->fontsize;
256 break;
257 }
258 if (obj->labeledgealigned)
259 p.y -= lp->pos.y;
260 for (i = 0; i < lp->u.txt.nspans; i++) {
261 switch (lp->u.txt.span[i].just) {
262 case 'l':
263 p.x = lp->pos.x - lp->space.x / 2.0;
264 break;
265 case 'r':
266 p.x = lp->pos.x + lp->space.x / 2.0;
267 break;
268 default:
269 case 'n':
270 p.x = lp->pos.x;
271 break;
272 }
273 gvrender_textspan(job, p, &(lp->u.txt.span[i]));
274
275 /* UL position for next span */
276 p.y -= lp->u.txt.span[i].size.y;
277 }
278
279 gvrender_end_label(job);
280 obj->emit_state = old_emit_state;
281}
282
283/* strdup_and_subst_obj0:
284 * Replace various escape sequences with the name of the associated
285 * graph object. A double backslash \\ can be used to avoid a replacement.
286 * If escBackslash is true, convert \\ to \; else leave alone. All other dyads
287 * of the form \. are passed through unchanged.
288 */
289static char *strdup_and_subst_obj0 (char *str, void *obj, int escBackslash)
290{
291 char c, *s, *p, *t, *newstr;
292 char *tp_str = "", *hp_str = "";
293 char *g_str = "\\G", *n_str = "\\N", *e_str = "\\E",
294 *h_str = "\\H", *t_str = "\\T", *l_str = "\\L";
295 size_t g_len = 2, n_len = 2, e_len = 2,
296 h_len = 2, t_len = 2, l_len = 2,
297 tp_len = 0, hp_len = 0;
298 size_t newlen = 0;
299 int isEdge = 0;
300 textlabel_t *tl;
301 port pt;
302
303 /* prepare substitution strings */
304 switch (agobjkind(obj)) {
305 case AGRAPH:
306 g_str = agnameof((graph_t *)obj);
307 g_len = strlen(g_str);
308 tl = GD_label((graph_t *)obj);
309 if (tl) {
310 l_str = tl->text;
311 if (str) l_len = strlen(l_str);
312 }
313 break;
314 case AGNODE:
315 g_str = agnameof(agraphof((node_t *)obj));
316 g_len = strlen(g_str);
317 n_str = agnameof((node_t *)obj);
318 n_len = strlen(n_str);
319 tl = ND_label((node_t *)obj);
320 if (tl) {
321 l_str = tl->text;
322 if (str) l_len = strlen(l_str);
323 }
324 break;
325 case AGEDGE:
326 isEdge = 1;
327 g_str = agnameof(agroot(agraphof(agtail(((edge_t *)obj)))));
328 g_len = strlen(g_str);
329 t_str = agnameof(agtail(((edge_t *)obj)));
330 t_len = strlen(t_str);
331 pt = ED_tail_port((edge_t *)obj);
332 if ((tp_str = pt.name))
333 tp_len = strlen(tp_str);
334 h_str = agnameof(aghead(((edge_t *)obj)));
335 h_len = strlen(h_str);
336 pt = ED_head_port((edge_t *)obj);
337 if ((hp_str = pt.name))
338 hp_len = strlen(hp_str);
339 h_len = strlen(h_str);
340 tl = ED_label((edge_t *)obj);
341 if (tl) {
342 l_str = tl->text;
343 if (str) l_len = strlen(l_str);
344 }
345 if (agisdirected(agroot(agraphof(agtail(((edge_t*)obj))))))
346 e_str = "->";
347 else
348 e_str = "--";
349 e_len = t_len + (tp_len?tp_len+1:0) + 2 + h_len + (hp_len?hp_len+1:0);
350 break;
351 }
352
353 /* two passes over str.
354 *
355 * first pass prepares substitution strings and computes
356 * total length for newstring required from malloc.
357 */
358 for (s = str; (c = *s++);) {
359 if (c == '\\') {
360 switch (c = *s++) {
361 case 'G':
362 newlen += g_len;
363 break;
364 case 'N':
365 newlen += n_len;
366 break;
367 case 'E':
368 newlen += e_len;
369 break;
370 case 'H':
371 newlen += h_len;
372 break;
373 case 'T':
374 newlen += t_len;
375 break;
376 case 'L':
377 newlen += l_len;
378 break;
379 case '\\':
380 if (escBackslash) {
381 newlen += 1;
382 break;
383 }
384 /* Fall through */
385 default: /* leave other escape sequences unmodified, e.g. \n \l \r */
386 newlen += 2;
387 }
388 } else {
389 newlen++;
390 }
391 }
392 /* allocate new string */
393 newstr = gmalloc(newlen + 1);
394
395 /* second pass over str assembles new string */
396 for (s = str, p = newstr; (c = *s++);) {
397 if (c == '\\') {
398 switch (c = *s++) {
399 case 'G':
400 for (t = g_str; (*p = *t++); p++);
401 break;
402 case 'N':
403 for (t = n_str; (*p = *t++); p++);
404 break;
405 case 'E':
406 if (isEdge) {
407 for (t = t_str; (*p = *t++); p++);
408 if (tp_len) {
409 *p++ = ':';
410 for (t = tp_str; (*p = *t++); p++);
411 }
412 for (t = e_str; (*p = *t++); p++);
413 for (t = h_str; (*p = *t++); p++);
414 if (hp_len) {
415 *p++ = ':';
416 for (t = hp_str; (*p = *t++); p++);
417 }
418 }
419 break;
420 case 'T':
421 for (t = t_str; (*p = *t++); p++);
422 break;
423 case 'H':
424 for (t = h_str; (*p = *t++); p++);
425 break;
426 case 'L':
427 for (t = l_str; (*p = *t++); p++);
428 break;
429 case '\\':
430 if (escBackslash) {
431 *p++ = '\\';
432 break;
433 }
434 /* Fall through */
435 default: /* leave other escape sequences unmodified, e.g. \n \l \r */
436 *p++ = '\\';
437 *p++ = c;
438 break;
439 }
440 } else {
441 *p++ = c;
442 }
443 }
444 *p++ = '\0';
445 return newstr;
446}
447
448/* strdup_and_subst_obj:
449 * Processes graph object escape sequences; also collapses \\ to \.
450 */
451char *strdup_and_subst_obj(char *str, void *obj)
452{
453 return strdup_and_subst_obj0 (str, obj, 1);
454}
455
456/* return true if *s points to &[A-Za-z]*; (e.g. &Ccedil; )
457 * or &#[0-9]*; (e.g. &#38; )
458 * or &#x[0-9a-fA-F]*; (e.g. &#x6C34; )
459 */
460static int xml_isentity(char *s)
461{
462 s++; /* already known to be '&' */
463 if (*s == '#') {
464 s++;
465 if (*s == 'x' || *s == 'X') {
466 s++;
467 while ((*s >= '0' && *s <= '9')
468 || (*s >= 'a' && *s <= 'f')
469 || (*s >= 'A' && *s <= 'F'))
470 s++;
471 } else {
472 while (*s >= '0' && *s <= '9')
473 s++;
474 }
475 } else {
476 while ((*s >= 'a' && *s <= 'z')
477 || (*s >= 'A' && *s <= 'Z'))
478 s++;
479 }
480 if (*s == ';')
481 return 1;
482 return 0;
483}
484
485char *xml_string(char *s)
486{
487 return xml_string0 (s, FALSE);
488}
489
490/* xml_string0:
491 * Encode input string as an xml string.
492 * If raw is true, the input is interpreted as having no
493 * embedded escape sequences, and \n and \r are changed
494 * into &#10; and &#13;, respectively.
495 * Uses a static buffer, so non-re-entrant.
496 */
497char *xml_string0(char *s, boolean raw)
498{
499 static char *buf = NULL;
500 static int bufsize = 0;
501 char *p, *sub, *prev = NULL;
502 int len, pos = 0;
503
504 if (!buf) {
505 bufsize = 64;
506 buf = gmalloc(bufsize);
507 }
508
509 p = buf;
510 while (s && *s) {
511 if (pos > (bufsize - 8)) {
512 bufsize *= 2;
513 buf = grealloc(buf, bufsize);
514 p = buf + pos;
515 }
516 /* escape '&' only if not part of a legal entity sequence */
517 if (*s == '&' && (raw || !(xml_isentity(s)))) {
518 sub = "&amp;";
519 len = 5;
520 }
521 /* '<' '>' are safe to substitute even if string is already UTF-8 coded
522 * since UTF-8 strings won't contain '<' or '>' */
523 else if (*s == '<') {
524 sub = "&lt;";
525 len = 4;
526 }
527 else if (*s == '>') {
528 sub = "&gt;";
529 len = 4;
530 }
531 else if (*s == '-') { /* can't be used in xml comment strings */
532 sub = "&#45;";
533 len = 5;
534 }
535 else if (*s == ' ' && prev && *prev == ' ') {
536 /* substitute 2nd and subsequent spaces with required_spaces */
537 sub = "&#160;"; /* inkscape doesn't recognise &nbsp; */
538 len = 6;
539 }
540 else if (*s == '"') {
541 sub = "&quot;";
542 len = 6;
543 }
544 else if (*s == '\'') {
545 sub = "&#39;";
546 len = 5;
547 }
548 else if ((*s == '\n') && raw) {
549 sub = "&#10;";
550 len = 5;
551 }
552 else if ((*s == '\r') && raw) {
553 sub = "&#13;";
554 len = 5;
555 }
556 else {
557 sub = s;
558 len = 1;
559 }
560 while (len--) {
561 *p++ = *sub++;
562 pos++;
563 }
564 prev = s;
565 s++;
566 }
567 *p = '\0';
568 return buf;
569}
570
571/* a variant of xml_string for urls in hrefs */
572char *xml_url_string(char *s)
573{
574 static char *buf = NULL;
575 static int bufsize = 0;
576 char *p, *sub;
577#if 0
578 char *prev = NULL;
579#endif
580 int len, pos = 0;
581
582 if (!buf) {
583 bufsize = 64;
584 buf = gmalloc(bufsize);
585 }
586
587 p = buf;
588 while (s && *s) {
589 if (pos > (bufsize - 8)) {
590 bufsize *= 2;
591 buf = grealloc(buf, bufsize);
592 p = buf + pos;
593 }
594 /* escape '&' only if not part of a legal entity sequence */
595 if (*s == '&' && !(xml_isentity(s))) {
596 sub = "&amp;";
597 len = 5;
598 }
599 /* '<' '>' are safe to substitute even if string is already UTF-8 coded
600 * since UTF-8 strings won't contain '<' or '>' */
601 else if (*s == '<') {
602 sub = "&lt;";
603 len = 4;
604 }
605 else if (*s == '>') {
606 sub = "&gt;";
607 len = 4;
608 }
609#if 0
610 else if (*s == '-') { /* can't be used in xml comment strings */
611 sub = "&#45;";
612 len = 5;
613 }
614 else if (*s == ' ' && prev && *prev == ' ') {
615 /* substitute 2nd and subsequent spaces with required_spaces */
616 sub = "&#160;"; /* inkscape doesn't recognise &nbsp; */
617 len = 6;
618 }
619#endif
620 else if (*s == '"') {
621 sub = "&quot;";
622 len = 6;
623 }
624 else if (*s == '\'') {
625 sub = "&#39;";
626 len = 5;
627 }
628 else {
629 sub = s;
630 len = 1;
631 }
632 while (len--) {
633 *p++ = *sub++;
634 pos++;
635 }
636#if 0
637 prev = s;
638#endif
639 s++;
640 }
641 *p = '\0';
642 return buf;
643}
644