@@ -259,7 +259,7 @@ end);
259259InstallMethod(Size, " for a graphviz map" ,
260260[ GV_IsMap] , m -> Length(GV_MapNames(m)));
261261
262- # ??
262+ # Graph child counter functions
263263
264264InstallMethod(GV_IncCounter,
265265" for a graphviz graph" ,
@@ -400,7 +400,7 @@ function(g, n)
400400 local graph;
401401 graph := GV_FindGraphWithNode(g, n);
402402 if graph = fail then
403- return graph ;
403+ return fail ;
404404 fi ;
405405 return graph[ n] ;
406406end );
@@ -464,6 +464,39 @@ function(x, edge)
464464 return x;
465465end );
466466
467+ InstallMethod(GV_RemoveGraphAttrIfExists,
468+ " for a graphviz graph context or digraph and a string" ,
469+ [ IsGraphvizGraphDigraphOrContext, IsString] ,
470+ function (obj, attr )
471+ local attrs, i, match;
472+ attrs := GraphvizAttrs(obj);
473+ attr := String(attr);
474+
475+ # checks if they attribute names match the one being removed
476+ match := function (key, str )
477+ for i in [ 1 .. Length(key)] do
478+ if i > Length(str) or key[ i] <> str[ i] then
479+ return false ;
480+ fi ;
481+ od ;
482+
483+ i := i + 1 ;
484+ while i <= Length(str) do
485+ if str[ i] = ' =' then
486+ return true ;
487+ elif str[ i] <> ' \s ' and str[ i] <> ' \t ' then
488+ return false ;
489+ fi ;
490+ i := i + 1 ;
491+ od ;
492+
493+ # attributes which are not key value or removal by value
494+ return true ;
495+ end ;
496+
497+ obj!. Attrs := Filtered(attrs, s -> not match(attr, s));
498+ end );
499+
467500# ##############################################################################
468501# Stringifying
469502# ##############################################################################
@@ -559,62 +592,42 @@ InstallMethod(GV_StringifyNodeEdgeAttrs,
559592" for a GV_Map" ,
560593[ GV_IsMap] ,
561594function (attrs )
562- local result, keys, key, val, n, i, tmp;
595+ local result, keys, key, val, n, i, tmp, format ;
563596
564597 result := " " ;
565598 n := Length(GV_MapNames(attrs));
566599 keys := SSortedList(GV_MapNames(attrs));
567600
601+ # helper for formatting attribute kv pairs
602+ format := function (format, key, val )
603+ tmp := Chomp(val);
604+ if " label" = key and StartsWith(tmp, " <<" ) and EndsWith(tmp, " >>" ) then
605+ val := StringFormatted(" {}" , val);
606+ else
607+ if ' ' in key then
608+ key := StringFormatted(" \" {}\" " , key);
609+ fi ;
610+
611+ if ' ' in val or ' >' in val or ' ^' in val or ' #' in val then
612+ val := StringFormatted(" \" {}\" " , val);
613+ fi ;
614+ fi ;
615+
616+ return StringFormatted(format, key, val);
617+ end ;
618+
568619 if n <> 0 then
569620 Append(result, " [" );
570621 for i in [ 1 .. n - 1 ] do
571622 key := keys[ i] ;
572623 val := attrs[ key] ;
573624
574- tmp := Chomp(val);
575- if " label" = key and StartsWith(tmp, " <<" ) and EndsWith(tmp, " >>" ) then
576- val := StringFormatted(" {}" , val);
577- else
578- # TODO it doesn't seem to be possible to enter the if-statement
579- # below, even with examples where the key contains spaces (probably
580- # the quotes are added somewhere else). Either uncomment or delete
581- # this code.
582- # if ' ' in key then
583- # key := StringFormatted("\"{}\"", key);
584- # fi;
585- if ' ' in val or ' >' in val or ' ^' in val or ' #' in val then
586- # TODO avoid code duplication here, and below
587- val := StringFormatted(" \" {}\" " , val);
588- fi ;
589- fi ;
590-
591- Append(result,
592- StringFormatted(" {}={}, " ,
593- key,
594- val));
625+ Append(result, format(" {}={}, " , key, val));
595626 od ;
596-
597627 # handle last element
598628 key := keys[ n] ;
599629 val := attrs[ key] ;
600-
601- tmp := Chomp(val);
602- if " label" = key and StartsWith(tmp, " <<" ) and EndsWith(tmp, " >>" ) then
603- val := StringFormatted(" {}" , val);
604- else
605- if ' ' in key then
606- key := StringFormatted(" \" {}\" " , key);
607- fi ;
608- if ' ' in val or ' >' in val or ' ^' in val or ' #' in val then
609- # TODO what are the allowed things in the value?
610- val := StringFormatted(" \" {}\" " , val);
611- fi ;
612- fi ;
613-
614- Append(result,
615- StringFormatted(" {}={}]" ,
616- key,
617- val));
630+ Append(result, format(" {}={}]" , key, val));
618631 fi ;
619632
620633 return result;
@@ -669,11 +682,9 @@ function(graph, is_subgraph)
669682 elif IsGraphvizGraph(graph) then
670683 Append(result, " //dot\n " );
671684 Append(result, GV_StringifyGraphHead(graph));
672- # TODO doesn't seem to be possible to reach the case below either, uncomment
673- # or delete
674- # else
675- # Append(result, "//dot\n");
676- # Append(result, GV_StringifyContextHead(graph));
685+ else
686+ ErrorFormatted(" Unknown graph category, " ,
687+ " expected a context, digraph or graph." );
677688 fi ;
678689
679690 Append(result, GV_StringifyGraphAttrs(graph));
@@ -737,3 +748,38 @@ function(gv, colors)
737748 fi ;
738749 Perform(colors, ErrorIfNotValidColor);
739750end );
751+
752+ InstallGlobalFunction(GV_ErrorIfNotValidLabel,
753+ function (label )
754+ local cond;
755+
756+ if Length(label) = 0 then
757+ ErrorFormatted(" invalid label \" {}\" , valid DOT labels " ,
758+ " cannot be empty strings" , label);
759+ fi ;
760+
761+ # double quoted string
762+ if StartsWith(label, " \" " ) and EndsWith(label, " \" " )then
763+ return ;
764+ fi ;
765+ # HTML string
766+ if StartsWith(label, " <" ) and EndsWith(label, " >" )then
767+ return ;
768+ fi ;
769+
770+ # numeral
771+ if Int(label) <> fail then
772+ return ;
773+ fi ;
774+
775+ cond := not IsDigitChar(label[ 1 ] );
776+ cond := cond and ForAll(label, c -> IsAlphaChar(c) or IsDigitChar(c)
777+ or c = ' _' or (' \200 ' <= c and c <= ' \377 ' ));
778+ if cond then
779+ return ;
780+ fi ;
781+
782+ ErrorFormatted(" invalid label \" {}\" , valid DOT labels " ,
783+ " https://graphviz.org/doc/info/lang.html" ,
784+ label);
785+ end );
0 commit comments