Skip to content

Instantly share code, notes, and snippets.

@steveroush
Created March 15, 2023 20:56
Show Gist options
  • Save steveroush/4287562c90855fd0d6a991e6159003ce to your computer and use it in GitHub Desktop.
Save steveroush/4287562c90855fd0d6a991e6159003ce to your computer and use it in GitHub Desktop.
word wrap for Graphviz
## set -x
## This is a Linux (and maybe MacOS?) shell script.
## It will need to be modified to run on Windows.
## It performs word-wrap (https://en.wikipedia.org/wiki/Line_wrap_and_word_wrap) on the labels of certain Graphviz nodes.
## - not html-like labels
## - not record or Mrecord shaped nodes
## - not edge labels
## - not graph or cluster labels
## - not xlabels
## - it DOES NOT recognize any meaning of TAB character (ASCII 09) or
## of HTML non-breaking space: ( )
## - it DOES try to honor \n \r \l embedded within labels
##
## to use:
## - set (new) maxTextWidth attribute for each node where word-wrap is desired
## maxTextWidth units are points (1/72 of inch)
## - invoke this script with one argument: the name of the Graphviz input file
## - output will be a modified version of the input file, with appropriate
## linebreaks (\n, \r, or \l) inserted in the node labels
##
##
## personal note: this is NOT the "right" way to do word-wrap
## it is a true kludge
## but it seems to work reasonably well
## steve roush
##
E=dot
E=twopi # we want a fast engine
gvpr -c '
BEGIN{
node_t thisNode, newNode;
int i, cnt;
string nl, rl, ll, Bell, DC1, DC2, DC3, aChar;
string p1, p2, justChar;
nl="\\" + "n";
rl="\\" + "r";
ll="\\" + "l";
// Bell, DC1, DC2, and DC3 are unprintable ASCII characters
// this program assumes that they will not be part of any label
// so we use them to convey formatting purposes
Bell=sprintf("%c", 7); // ascii Bell (decimal 7)
DC1=sprintf("%c", 17); // ascii DC1 (decimal 17)
DC2=sprintf("%c", 18); // ascii DC1 (decimal 18)
DC3=sprintf("%c", 19); // ascii DC1 (decimal 18)
}
N{
if (hasAttr($, "maxTextWidth") && $.maxTextWidth!="" && $.maxTextWidth!="0"){
}else{
//printf(2, "Note:: no word-wrap for node: %s. It does not have maxTextWidth attribute.\n", $.name);
break;
}
//HASATTR($, height)
if (hasAttr($, "height") && $.height!="")
$.__saveHeight=$.height;
else
$.__saveHeight="";
if (hasAttr($, "width") && $.width!="")
$.__saveWidth=$.width;
else
$.__saveWidth="";
if (hasAttr($, "label") && $.label!="" && (ishtml($.label)!=1)){
//// need to count \ chars before [nrl]
//// if the count is odd, replace with \[nrl] Bell nl
//// else leave alone
cnt=0;
i=0;
while (i<length($.label)){
aChar=substr($.label,i,1);
if (strcmp(aChar,"\\")==0) {
cnt++;
}else{
if (((cnt%2)==1) && aChar=="[rnl]"){
p1=substr($.label,0,i+1);
p2=substr($.label,i+1);
$.label=p1 + Bell + aChar + nl + p2;
i+=length(Bell + aChar + nl);
}
cnt=0;
}
i++;
}
// ugh, we need to find any justification chars (\[rln])
// and then propagate that info backwards (toward the front of the text)
justChar="n";
for (i=length($.label)-2;i>0;i--){
aChar=substr($.label,i,1);
if (strcmp(aChar,Bell)==0) {
justChar=substr($.label,i+1,1);
}else if (aChar==" "){
p1=substr($.label,0,i);
p2=substr($.label,i+1);
if (justChar=="n") $.label=p1 + DC1 + p2;
if (justChar=="l") $.label=p1 + DC2 + p2;
if (justChar=="r") $.label=p1 + DC3 + p2;
}
}
$.label=gsub($.label,DC1,nl+" "+nl); // wrap \? around each space
$.label=gsub($.label,DC2,ll+" "+ll); // wrap \? around each space
$.label=gsub($.label,DC3,rl+" "+rl); // wrap \? around each space
}
}
/***********************************************************
**** for time being, do not worry about edge labels
**** or headlabels, taillabels, xlabels, or graph/cluster labels
*************************************************************/
' $1 |
$E -G"linelength=999999" -Txdot |
/bin/gvpr -c '
BEGIN{
int cnt, i, tmpCnt, just, oldJust, len;
int maxLineWidth, lineWidth, wordWidth; // units = points
string Xstr, tok, thisWord, nl, rl, ll, Bell;
float Xpos, Ypos;
obj_t thisObj;
nl="\\" + "n";
rl="\\" + "r";
ll="\\" + "l";
Bell=sprintf("%c", 7); // ascii Bell (decimal 7)
/////////////////////////////////////////////////
// index is (temporarily) broken, so ...
/////////////////////////////////////////////////
int myIndex(string Main, string Pattern){
int _I, _L, _P, _RC ;
string _SUB;
_RC=-1;
_L=length(Main);
_P=length(Pattern);
for (_I=0; _I<(_L-_P+1);_I++){
_SUB=substr(Main,_I,_P);
if (_SUB==Pattern){
_RC=_I;
break;
}
}
return(_RC);
}
/////////////////////////////////////////////////
int adjustCnt(string rawTxt, int cnt){
string tmpStr;
int j;
tmpStr=rawTxt+" "; // pad for substr
for (j=0;j<cnt;j++){
if (strcmp(substr(tmpStr,j,2),"\\\\")==0){
cnt++;
}
}
return cnt;
}
///////////////////////////////////////////////////////////////////
string getTok(){
string tstr;
Xstr=sub(Xstr,"^+( )",""); // strip leading spaces - ksh pattern
i=myIndex(Xstr, " ");
tstr=substr(Xstr,0,i);
Xstr=substr(Xstr,i);
Xstr=sub(Xstr,"^+( )",""); // strip leading spaces - ksh pattern
return(tstr);
}
/////////////////////////////////////////////////////////////////
void ldrawParse(){
tok=getTok();
just=0; //
while (tok!=""){
switch(tok){
case "F": // font
tok=getTok(); // font size, discard
//cnt=1+(int)getTok(); // font name char count (plus leading -)
cnt=(int)getTok(); // font name char count (plus leading -)
cnt++;
tok=substr(Xstr,0,cnt); // font name, discard
Xstr=substr(Xstr,cnt); // strip fontname from Xstr
break;
case "c": // pen color
case "C": // fill color
cnt=1+(int)getTok(); // color char count (plus leading -)
tok=substr(Xstr,0,cnt); // font name, discard
Xstr=substr(Xstr,cnt); // strip fontname from Xstr
break;
case "T": // text
// by now, we have put each "word" on a different line
// time to stitch the words into lines
Xpos=(float)getTok(); // X pos
Ypos=(float)getTok(); // Y pos
oldJust=just;
just=(int)getTok(); // justify
wordWidth=(int)getTok(); // word width
cnt=(int)getTok(); // word char count
///////////////////////////////////////////////////////////
// if string contains //
// count will be short - length of "//" is 1 !!!!!!
// we have already taken care of \G \N \E ...
///////////////////////////////////////////////////////////
Xstr=substr(Xstr,1); // chop off leading "-"
cnt=adjustCnt(Xstr,cnt);
// now we have adjusted character/byte count
thisWord=substr(Xstr,0,cnt); // word
//////////// time to wrap ////////////////////
tmpCnt=wordWidth;
if (substr(thisWord,0,1)==Bell){
lineWidth=0;
thisObj.label+=("\\"+substr(thisWord,1,1));
}else if ((lineWidth+tmpCnt)<=maxLineWidth){
lineWidth+=tmpCnt;
thisObj.label+=thisWord;
}else{
// first, strip trailing space at end of previous line
// ksh pattern matching does not support ^ or $,
// so grind it out
len=length(thisObj.label);
if(substr(thisObj.label, len-1)==" ")
thisObj.label=substr(thisObj.label,0,len-1);
// do not start line with space
if (thisWord!=" "){
lineWidth=wordWidth;
// add justification
if (oldJust==0) thisObj.label+=(nl+thisWord);
if (oldJust==-1) thisObj.label+=(ll+thisWord);
if (oldJust==1) thisObj.label+=(rl+thisWord);
}
}
Xstr=substr(Xstr,cnt); // strip word from Xstr
Xstr=sub(Xstr,"^+( )",""); // strip leading spaces - ksh pattern
break;
case "S": // style
cnt=1+(int)getTok(); // style char count (plus leading -)
tok=substr(Xstr,0,cnt); // style, discard
Xstr=substr(Xstr,cnt); // strip style from Xstr
break;
case "t": // font characteristics
cnt=1+(int)getTok(); // style char count (plus leading -)
tok=substr(Xstr,0,cnt); // style, discard
Xstr=substr(Xstr,cnt); // strip style from Xstr
break;
case "E": // filled ellipse
case "e": // unfilled ellipse
tok=substr(Xstr,0,cnt); // X, discard
tok=substr(Xstr,0,cnt); // Y, discard
tok=substr(Xstr,0,cnt); // width, discard
tok=substr(Xstr,0,cnt); // height, discard
break;
case "P": // filled polygon
case "p": // unfilled polygon
case "L": // polyline
case "B": // unfilled B-spline
case "b": // filled polygon
cnt=1+(int)getTok(); // point (X,Y) count
for (i=0;i<cnt;i++){ // delete multiple point (pos) values
tok=substr(Xstr,0,cnt); // X, discard
tok=substr(Xstr,0,cnt); // Y, discard
}
Xstr=substr(Xstr,cnt); // strip style from Xstr
break;
case "I": // external image
break;
default:
break;
}
if (length(Xstr)>0){
tok=getTok();
}else{
break;
}
}
}
}
N{
thisObj=$;
lineWidth=0;
maxLineWidth=0; // ??
if (hasAttr($, "maxTextWidth") && $.maxTextWidth!="" && $.maxTextWidth!="0"){
maxLineWidth=(int)(72. * maxTextWidth);
}else{
printf(2, "Note:: no word-wrap for node: %s. It does not have maxTextWidth attribute.\n", $.name);
break;
}
if (hasAttr($, "__saveHeight") && $.__saveHeight!="")
$.height=$.__saveHeight;
else
$.height="";
if (hasAttr($, "__saveWidth") && $.__saveWidth!="")
$.width=$.__saveWidth;
else
$.width="";
if (hasAttr($, "_ldraw_") && $._ldraw_!=""){
$.label=""; // we will rebuild
Xstr=$._ldraw_;
ldrawParse(); // do heavy lifting
$._ldraw_="";
}
}'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment