/** * @file * @brief DOT-GXL convert subroutines */ /************************************************************************* * Copyright (c) 2011 AT&T Intellectual Property * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-v10.html * * Contributors: Details at https://graphviz.org *************************************************************************/ #include #include #include #include #include "convert.h" #include #include #include #include #include #define EMPTY(s) ((s == 0) || (*s == '\0')) #define SLEN(s) (sizeof(s)-1) #define GXL_ATTR "_gxl_" #define GXL_ROLE "_gxl_role" #define GXL_HYPER "_gxl_hypergraph" #define GXL_ID "_gxl_id" #define GXL_FROM "_gxl_fromorder" #define GXL_TO "_gxl_toorder" #define GXL_TYPE "_gxl_type" #define GXL_COMP "_gxl_composite_" #define GXL_LOC "_gxl_locator_" #define GXL_COMP_LEN (SLEN(GXL_COMP)) typedef struct { Agrec_t h; int written; } Local_Agnodeinfo_t; static int Level; /* level of tabs */ static Agsym_t *Tailport, *Headport; typedef struct { Dtlink_t link; char *name; char *unique_name; } namev_t; static void *make_nitem(void *p, Dtdisc_t *disc) { (void)disc; namev_t *objp = p; namev_t *np = gv_alloc(sizeof(*np)); np->name = objp->name; np->unique_name = 0; return np; } static Dtdisc_t nameDisc = { offsetof(namev_t, name), -1, offsetof(namev_t, link), make_nitem, free, NULL, }; typedef struct { Dtlink_t link; char *name; } idv_t; static void free_iditem(void *idv) { idv_t *idp = idv; free(idp->name); free(idp); } static Dtdisc_t idDisc = { offsetof(idv_t, name), -1, offsetof(idv_t, link), NULL, free_iditem, NULL, }; typedef struct { Dt_t *nodeMap; Dt_t *graphMap; Dt_t *synNodeMap; Dt_t *idList; Agraph_t *root; char attrsNotWritten; char directed; } gxlstate_t; static void writeBody(gxlstate_t *, Agraph_t * g, FILE * gxlFile); static void iterateBody(gxlstate_t * stp, Agraph_t * g); static void tabover(FILE * gxlFile) { int temp = Level; while (temp--) putc('\t', gxlFile); } /* legalGXLName: * By XML spec, * ID := (alpha|'_'|':')(NameChar)* * NameChar := alpha|digit|'.'|':'|'-'|'_' */ static bool legalGXLName(const char *id) { char c = *id++; if (!gv_isalpha(c) && c != '_' && c != ':') return false; while ((c = *id++)) { if (!gv_isalnum(c) && c != '_' && c != ':' && c != '-' && c != '.') return false; } return true; } // `fputs` wrapper to handle the difference in calling convention to what // `xml_escape`’s `cb` expects static inline int put(void *stream, const char *s) { return fputs(s, stream); } // write a string to the given file, XML-escaping the input static inline int xml_puts(FILE *stream, const char *s) { const xml_flags_t flags = {.dash = 1, .nbsp = 1}; return xml_escape(s, flags, put, stream); } // wrapper around `xml_escape` to set flags for URL escaping static int xml_url_puts(FILE *f, const char *s) { const xml_flags_t flags = {0}; return xml_escape(s, flags, put, f); } static bool isGxlGrammar(const char *name) { return startswith(name, GXL_ATTR); } static bool isLocatorType(const char *name) { return startswith(name, GXL_LOC); } static bool idexists(Dt_t * ids, char *id) { return dtmatch(ids, id) != NULL; } /* addid: * assume id is not in ids. */ static char *addid(Dt_t *ids, const char *id) { idv_t *idp = gv_alloc(sizeof(*idp)); idp->name = gv_strdup(id); dtinsert(ids, idp); return idp->name; } static char *createGraphId(Dt_t * ids) { static int graphIdCounter = 0; agxbuf buf = {0}; char *name; do { agxbprint(&buf, "G_%d", graphIdCounter++); name = agxbuse(&buf); } while (idexists(ids, name)); char *rv = addid(ids, name); agxbfree(&buf); return rv; } static char *createNodeId(Dt_t * ids) { static int nodeIdCounter = 0; agxbuf buf = {0}; char *name; do { agxbprint(&buf, "N_%d", nodeIdCounter++); name = agxbuse(&buf); } while (idexists(ids, name)); char *rv = addid(ids, name); agxbfree(&buf); return rv; } static char *mapLookup(Dt_t * nm, char *name) { namev_t *objp = dtmatch(nm, name); if (objp) return objp->unique_name; return NULL; } /* nodeID: * Return id associated with the given node. */ static char *nodeID(gxlstate_t * stp, Agnode_t * n) { char *name = agnameof(n); char *uniqueName = mapLookup(stp->nodeMap, name); assert(uniqueName); return uniqueName; } #define EDGEOP "--" /* cannot use '>'; illegal in ID in GXL */ static char *createEdgeId(gxlstate_t * stp, Agedge_t * e) { int edgeIdCounter = 1; char *hname = nodeID(stp, AGHEAD(e)); char *tname = nodeID(stp, AGTAIL(e)); agxbuf bp = {0}; agxbprint(&bp, "%s%s%s", tname, EDGEOP, hname); char *id_name = agxbuse(&bp); while (idexists(stp->idList, id_name)) { agxbprint(&bp, "%s%s%s:%d", tname, EDGEOP, hname, edgeIdCounter++); id_name = agxbuse(&bp); } char *rv = addid(stp->idList, id_name); agxbfree(&bp); return rv; } static void addToMap(Dt_t * map, char *name, char *uniqueName) { namev_t obj = {.name = name}; namev_t *objp = dtinsert(map, &obj); assert(objp->unique_name == NULL); objp->unique_name = uniqueName; } static void graphAttrs(FILE * gxlFile, Agraph_t * g) { char *val = agget(g, GXL_ROLE); if (!EMPTY(val)) { fprintf(gxlFile, " role=\""); xml_puts(gxlFile, val); fprintf(gxlFile, "\""); } val = agget(g, GXL_HYPER); if (!EMPTY(val)) { fprintf(gxlFile, " hypergraph=\""); xml_puts(gxlFile, val); fprintf(gxlFile, "\""); } } static void edgeAttrs(FILE * gxlFile, Agedge_t * e) { char *val = agget(e, GXL_ID); if (!EMPTY(val)) { fprintf(gxlFile, " id=\""); xml_puts(gxlFile, val); fprintf(gxlFile, "\""); } val = agget(e, GXL_FROM); if (!EMPTY(val)) { fprintf(gxlFile, " fromorder=\""); xml_puts(gxlFile, val); fprintf(gxlFile, "\""); } val = agget(e, GXL_TO); if (!EMPTY(val)) { fprintf(gxlFile, " toorder=\""); xml_puts(gxlFile, val); fprintf(gxlFile, "\""); } } static void printHref(FILE * gxlFile, void *n) { char *val = agget(n, GXL_TYPE); if (!EMPTY(val)) { tabover(gxlFile); fprintf(gxlFile, "\t\n"); tabover(gxlFile); fprintf(gxlFile, "\t\n"); } } static void writeDict(FILE *gxlFile, const char *name, Dict_t *dict, bool isGraph) { Dict_t *view = dtview(dict, NULL); for (Agsym_t *sym = dtfirst(dict); sym; sym = dtnext(dict, sym)) { if (!isGxlGrammar(sym->name)) { if (EMPTY(sym->defval)) { /* try to skip empty str (default) */ if (view == NULL) continue; /* no parent */ Agsym_t *psym = dtsearch(view, sym); /* assert(psym); */ if (EMPTY(psym->defval)) continue; /* also empty in parent */ } if (isLocatorType(sym->defval)) { char *locatorVal = sym->defval + strlen(GXL_LOC); tabover(gxlFile); fprintf(gxlFile, "\tname); fprintf(gxlFile, "\">\n"); tabover(gxlFile); fprintf(gxlFile, "\t\t\n"); tabover(gxlFile); fprintf(gxlFile, "\t\n"); } else { tabover(gxlFile); if (isGraph) { fprintf(gxlFile, "\tname); fprintf(gxlFile, "\" "); fprintf(gxlFile, "kind=\""); xml_puts(gxlFile, name); fprintf(gxlFile, "\">\n"); } else { fprintf(gxlFile, "\tname); fprintf(gxlFile, "\" kind=\""); xml_puts(gxlFile, name); fprintf(gxlFile, "\">\n"); } tabover(gxlFile); fprintf(gxlFile, "\t\t"); xml_puts(gxlFile, sym->defval); fprintf(gxlFile, "\n"); tabover(gxlFile); fprintf(gxlFile, "\t\n"); } } else { /* gxl attr; check for special cases like composites */ if (startswith(sym->name, GXL_COMP)) { if (EMPTY(sym->defval)) { if (view == NULL) continue; Agsym_t *psym = dtsearch(view, sym); if (EMPTY(psym->defval)) continue; } tabover(gxlFile); fprintf(gxlFile, "\tname + GXL_COMP_LEN); fprintf(gxlFile, "\" "); fprintf(gxlFile, "kind=\""); xml_puts(gxlFile, name); fprintf(gxlFile, "\">\n"); tabover(gxlFile); fprintf(gxlFile, "\t\t"); xml_puts(gxlFile, sym->defval); fprintf(gxlFile, "\n"); tabover(gxlFile); fprintf(gxlFile, "\t\n"); } } } dtview(dict, view); /* restore previous view */ } static void writeDicts(Agraph_t * g, FILE * gxlFile) { Agdatadict_t *def; if ((def = agdatadict(g, false))) { writeDict(gxlFile, "graph", def->dict.g, true); writeDict(gxlFile, "node", def->dict.n, false); writeDict(gxlFile, "edge", def->dict.e, false); } } static void writeHdr(gxlstate_t *stp, Agraph_t *g, FILE *gxlFile, bool top) { char *kind; Level++; stp->attrsNotWritten = AGATTRWF(g); char *name = agnameof(g); if (g->desc.directed) kind = "directed"; else kind = "undirected"; if (!top && agparent(g)) { /* this must be anonymous graph */ agxbuf buf = {0}; agxbprint(&buf, "N_%s", name); char *bp = agxbuse(&buf); if (idexists(stp->idList, bp) || !legalGXLName(bp)) { bp = createNodeId(stp->idList); } else { bp = addid(stp->idList, bp); } addToMap(stp->synNodeMap, name, bp); tabover(gxlFile); fprintf(gxlFile, "\n", bp); agxbfree(&buf); Level++; } else { Tailport = agattr(g, AGEDGE, "tailport", NULL); Headport = agattr(g, AGEDGE, "headport", NULL); } char *uniqueName = mapLookup(stp->graphMap, name); tabover(gxlFile); fprintf(gxlFile, "\n"); if (uniqueName && (strcmp(name, uniqueName) != 0)) { tabover(gxlFile); fprintf(gxlFile, "\t\n"); tabover(gxlFile); fprintf(gxlFile, "\t\t"); xml_puts(gxlFile, name); fprintf(gxlFile, "\n"); tabover(gxlFile); fprintf(gxlFile, "\t\n"); } if (agisstrict(g)) { tabover(gxlFile); fprintf(gxlFile, "\t\n"); tabover(gxlFile); fprintf(gxlFile, "\t\ttrue\n"); tabover(gxlFile); fprintf(gxlFile, "\t\n"); } writeDicts(g, gxlFile); printHref(gxlFile, g); AGATTRWF(g) = !(AGATTRWF(g)); } static void writeTrl(Agraph_t *g, FILE *gxlFile, bool top) { tabover(gxlFile); fprintf(gxlFile, "\n"); Level--; if (!top && agparent(g)) { tabover(gxlFile); fprintf(gxlFile, "\n"); Level--; } } static void writeSubgs(gxlstate_t * stp, Agraph_t * g, FILE * gxlFile) { for (Agraph_t *subg = agfstsubg(g); subg; subg = agnxtsubg(subg)) { writeHdr(stp, subg, gxlFile, false); writeBody(stp, subg, gxlFile); writeTrl(subg, gxlFile, false); } } static void writeEdgeName(Agedge_t *e, FILE *gxlFile) { char *p = agnameof(e); if (!(EMPTY(p))) { tabover(gxlFile); fprintf(gxlFile, "\t\n"); tabover(gxlFile); fprintf(gxlFile, "\t\t"); xml_puts(gxlFile, p); fprintf(gxlFile, "\n"); tabover(gxlFile); fprintf(gxlFile, "\t\n"); } } static void writeNondefaultAttr(void *obj, FILE * gxlFile, Dict_t * defdict) { if (AGTYPE(obj) == AGINEDGE || AGTYPE(obj) == AGOUTEDGE) { writeEdgeName(obj, gxlFile); } Agattr_t *data = agattrrec(obj); if (data) { for (Agsym_t *sym = dtfirst(defdict); sym; sym = dtnext(defdict, sym)) { if (!isGxlGrammar(sym->name)) { if (AGTYPE(obj) == AGINEDGE || AGTYPE(obj) == AGOUTEDGE) { if (Tailport && sym->id == Tailport->id) continue; if (Headport && sym->id == Headport->id) continue; } if (data->str[sym->id] != sym->defval) { if (strcmp(data->str[sym->id], "") == 0) continue; if (isLocatorType(data->str[sym->id])) { char *locatorVal = data->str[sym->id] + strlen(GXL_LOC); tabover(gxlFile); fprintf(gxlFile, "\tname); fprintf(gxlFile, "\">\n"); tabover(gxlFile); fprintf(gxlFile, "\t\t\n"); tabover(gxlFile); fprintf(gxlFile, "\t\n"); } else { tabover(gxlFile); fprintf(gxlFile, "\tname); fprintf(gxlFile, "\""); if (aghtmlstr(data->str[sym->id])) { // This is a <…> string. Note this in the kind. fprintf(gxlFile, " kind=\"HTML-like string\""); } fprintf(gxlFile, ">\n"); tabover(gxlFile); fprintf(gxlFile, "\t\t"); xml_puts(gxlFile, data->str[sym->id]); fprintf(gxlFile, "\n"); tabover(gxlFile); fprintf(gxlFile, "\t\n"); } } } else { /* gxl attr; check for special cases like composites */ if (startswith(sym->name, GXL_COMP)) { if (data->str[sym->id] != sym->defval) { tabover(gxlFile); fprintf(gxlFile, "\tname + GXL_COMP_LEN); fprintf(gxlFile, "\">\n"); tabover(gxlFile); fprintf(gxlFile, "\t\t"); xml_puts(gxlFile, data->str[sym->id]); fprintf(gxlFile, "\n"); tabover(gxlFile); fprintf(gxlFile, "\t\n"); } } } } } AGATTRWF(obj) = !(AGATTRWF(obj)); } static bool attrs_written(gxlstate_t * stp, void *obj) { return AGATTRWF(obj) != stp->attrsNotWritten; } static void writeNode(gxlstate_t * stp, Agnode_t * n, FILE * gxlFile, Dict_t * d) { char *name = agnameof(n); char *uniqueName = nodeID(stp, n); Level++; tabover(gxlFile); fprintf(gxlFile, "\n", uniqueName); printHref(gxlFile, n); if (strcmp(name, uniqueName)) { tabover(gxlFile); fprintf(gxlFile, "\t\n"); tabover(gxlFile); fprintf(gxlFile, "\t\t"); xml_puts(gxlFile, name); fprintf(gxlFile, "\n"); tabover(gxlFile); fprintf(gxlFile, "\t\n"); } if (!attrs_written(stp, n)) writeNondefaultAttr(n, gxlFile, d); tabover(gxlFile); fprintf(gxlFile, "\n"); Level--; } static void writePort(Agedge_t * e, FILE * gxlFile, char *name) { char *val = agget(e, name); if (val && val[0]) { tabover(gxlFile); fprintf(gxlFile, "\t\n"); tabover(gxlFile); fprintf(gxlFile, "\t\t"); xml_puts(gxlFile, val); fprintf(gxlFile, "\n"); tabover(gxlFile); fprintf(gxlFile, "\t\n"); } } static bool writeEdgeTest(Agraph_t *g, Agedge_t *e) { /* can use agedge() because we subverted the dict compar_f */ for (Agraph_t *subg = agfstsubg(g); subg; subg = agnxtsubg(subg)) { if (agsubedge(subg, e, 0)) return false; } return true; } static void writeEdge(gxlstate_t * stp, Agedge_t * e, FILE * gxlFile, Dict_t * d) { Agnode_t *t = AGTAIL(e); Agnode_t *h = AGHEAD(e); Level++; tabover(gxlFile); fprintf(gxlFile, "directed) { fprintf(gxlFile, " isdirected=\"true\""); } else { fprintf(gxlFile, " isdirected=\"false\""); } char *edge_id = agget(e, GXL_ID); if (!EMPTY(edge_id)) { fprintf(gxlFile, ">\n"); } else { char *bp = createEdgeId(stp, e); fprintf(gxlFile, " id=\"%s\">\n", bp); } printHref(gxlFile, e); writePort(e, gxlFile, "tailport"); writePort(e, gxlFile, "headport"); if (!(attrs_written(stp, e))) writeNondefaultAttr(e, gxlFile, d); else writeEdgeName(e, gxlFile); tabover(gxlFile); fprintf(gxlFile, "\n"); Level--; } #define writeval(n) (((Local_Agnodeinfo_t*)((n)->base.data))->written) static void writeBody(gxlstate_t * stp, Agraph_t * g, FILE * gxlFile) { writeSubgs(stp, g, gxlFile); Agdatadict_t *dd = agdatadict(g, false); for (Agnode_t *n = agfstnode(g); n; n = agnxtnode(g, n)) { Agnode_t *realn = agidnode(stp->root, AGID(n), 0); if (!writeval(realn)) { writeval(realn) = 1; writeNode(stp, n, gxlFile, dd->dict.n); } for (Agedge_t *e = agfstout(g, n); e; e = agnxtout(g, e)) { if (writeEdgeTest(g, e)) writeEdge(stp, e, gxlFile, dd->dict.e); } } } static void iterateHdr(gxlstate_t * stp, Agraph_t * g) { char *name = agnameof(g); char *gxlId = agget(g, GXL_ID); if (EMPTY(gxlId)) gxlId = name; if (idexists(stp->idList, gxlId) || !legalGXLName(gxlId)) gxlId = createGraphId(stp->idList); else gxlId = addid(stp->idList, gxlId); addToMap(stp->graphMap, name, gxlId); } static void iterate_subgs(gxlstate_t * stp, Agraph_t * g) { for (Agraph_t *subg = agfstsubg(g); subg; subg = agnxtsubg(subg)) { iterateHdr(stp, subg); iterateBody(stp, subg); } } static void iterateBody(gxlstate_t * stp, Agraph_t * g) { iterate_subgs(stp, g); for (Agnode_t *n = agfstnode(g); n; n = agnxtnode(g, n)) { char *nodename = agnameof(n); if (!mapLookup(stp->nodeMap, nodename)) { char *gxlId = agget(n, GXL_ID); if (EMPTY(gxlId)) gxlId = nodename; if (idexists(stp->idList, gxlId) || !legalGXLName(gxlId)) gxlId = createNodeId(stp->idList); else gxlId = addid(stp->idList, gxlId); addToMap(stp->nodeMap, nodename, gxlId); } for (Agedge_t *e = agfstout(g, n); e; e = agnxtout(g, e)) { if (writeEdgeTest(g, e)) { char *edge_id = agget(e, GXL_ID); if (!EMPTY(edge_id)) addid(stp->idList, edge_id); } } } } static gxlstate_t initState(Agraph_t *g) { gxlstate_t stp = {0}; stp.nodeMap = dtopen(&nameDisc, Dtoset); stp.graphMap = dtopen(&nameDisc, Dtoset); stp.synNodeMap = dtopen(&nameDisc, Dtoset); stp.idList = dtopen(&idDisc, Dtoset); stp.attrsNotWritten = 0; stp.root = g; stp.directed = agisdirected(g) != 0; return stp; } static void freeState(gxlstate_t stp) { dtclose(stp.nodeMap); dtclose(stp.graphMap); dtclose(stp.synNodeMap); dtclose(stp.idList); } void gv_to_gxl(Agraph_t * g, FILE * gxlFile) { gxlstate_t stp = initState(g); aginit(g, AGNODE, "node", sizeof(Local_Agnodeinfo_t), true); iterateHdr(&stp, g); iterateBody(&stp, g); Level = 0; fprintf(gxlFile, "\n"); fprintf(gxlFile, "\n"); writeHdr(&stp, g, gxlFile, true); writeBody(&stp, g, gxlFile); writeTrl(g, gxlFile, true); fprintf(gxlFile, "\n"); freeState(stp); }