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 | |
19 | static char *strdup_and_subst_obj0 (char *str, void *obj, int escBackslash); |
20 | |
21 | static 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> */ |
51 | void 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 | */ |
115 | textlabel_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 | */ |
193 | void 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 | |
209 | void 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 | |
222 | void 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 | */ |
289 | static 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 | */ |
451 | char *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. Ç ) |
457 | * or &#[0-9]*; (e.g. & ) |
458 | * or &#x[0-9a-fA-F]*; (e.g. 水 ) |
459 | */ |
460 | static 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 | |
485 | char *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 and , respectively. |
495 | * Uses a static buffer, so non-re-entrant. |
496 | */ |
497 | char *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 = "&" ; |
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 = "<" ; |
525 | len = 4; |
526 | } |
527 | else if (*s == '>') { |
528 | sub = ">" ; |
529 | len = 4; |
530 | } |
531 | else if (*s == '-') { /* can't be used in xml comment strings */ |
532 | sub = "-" ; |
533 | len = 5; |
534 | } |
535 | else if (*s == ' ' && prev && *prev == ' ') { |
536 | /* substitute 2nd and subsequent spaces with required_spaces */ |
537 | sub = " " ; /* inkscape doesn't recognise */ |
538 | len = 6; |
539 | } |
540 | else if (*s == '"') { |
541 | sub = """ ; |
542 | len = 6; |
543 | } |
544 | else if (*s == '\'') { |
545 | sub = "'" ; |
546 | len = 5; |
547 | } |
548 | else if ((*s == '\n') && raw) { |
549 | sub = " " ; |
550 | len = 5; |
551 | } |
552 | else if ((*s == '\r') && raw) { |
553 | sub = " " ; |
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 */ |
572 | char *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 = "&" ; |
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 = "<" ; |
603 | len = 4; |
604 | } |
605 | else if (*s == '>') { |
606 | sub = ">" ; |
607 | len = 4; |
608 | } |
609 | #if 0 |
610 | else if (*s == '-') { /* can't be used in xml comment strings */ |
611 | sub = "-" ; |
612 | len = 5; |
613 | } |
614 | else if (*s == ' ' && prev && *prev == ' ') { |
615 | /* substitute 2nd and subsequent spaces with required_spaces */ |
616 | sub = " " ; /* inkscape doesn't recognise */ |
617 | len = 6; |
618 | } |
619 | #endif |
620 | else if (*s == '"') { |
621 | sub = """ ; |
622 | len = 6; |
623 | } |
624 | else if (*s == '\'') { |
625 | sub = "'" ; |
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 | |