]> git.ozlabs.org Git - ccan/blobdiff - ccan/ungraph/ungraph.c
ungraph: new module
[ccan] / ccan / ungraph / ungraph.c
diff --git a/ccan/ungraph/ungraph.c b/ccan/ungraph/ungraph.c
new file mode 100644 (file)
index 0000000..ba29d22
--- /dev/null
@@ -0,0 +1,721 @@
+/* MIT (BSD) license - see LICENSE file for details */
+#include <ccan/ungraph/ungraph.h>
+#include <ccan/tal/str/str.h>
+
+struct xy {
+       size_t x, y;
+};
+
+struct text {
+       struct xy xy;
+       size_t width;
+       const char *text;
+       /* If it's a node, this is non-NULL */
+       void *node;
+       /* NULL if none found, edge it one found, self if > 1 */
+       struct edge *nearest_edge;
+};
+
+struct edge {
+       struct text *src, *dst;
+       bool bidir;
+       const char **labels;
+};
+
+/* This means we actually found two "nearest_edge" */
+static struct edge fake_edge;
+
+#define EDGES "+/-\\|"
+#define ARROWS "<^>v"
+
+enum dir {
+       UP,
+       UP_RIGHT,
+       RIGHT,
+       DOWN_RIGHT,
+       DOWN,
+       DOWN_LEFT,
+       LEFT,
+       UP_LEFT,
+       INVALID,
+};
+
+static enum dir opposite_dir(enum dir dir)
+{
+       return (dir + 4) % 8;
+}
+
+static enum dir clockwise(enum dir dir)
+{
+       return (dir + 1) % 8;
+}
+
+static enum dir anticlockwise(enum dir dir)
+{
+       return (dir + 7) % 8;
+}
+
+static enum dir dir_away(const struct text *t, struct xy xy)
+{
+       int xdir, ydir;
+       enum dir dirs[3][3] = {{UP_LEFT, UP, UP_RIGHT},
+                              {LEFT, INVALID, RIGHT},
+                              {DOWN_LEFT, DOWN, DOWN_RIGHT}};
+       
+       if (xy.y < t->xy.y)
+               ydir = -1;
+       else if (xy.y > t->xy.y)
+               ydir = 1;
+       else
+               ydir = 0;
+       if (xy.x >= t->xy.x + t->width)
+               xdir = 1;
+       else if (xy.x < t->xy.x)
+               xdir = -1;
+       else
+               xdir = 0;
+
+       return dirs[ydir+1][xdir+1];
+}
+
+static char line_for_dir(enum dir dir)
+{
+       switch (dir) {
+       case UP:
+       case DOWN:
+               return '|';
+       case UP_RIGHT:
+       case DOWN_LEFT:
+               return '/';
+       case RIGHT:
+       case LEFT:
+               return '-';
+       case DOWN_RIGHT:
+       case UP_LEFT:
+               return '\\';
+       case INVALID:
+               break;
+       }
+       abort();
+}
+
+static char arrow_for_dir(enum dir dir)
+{
+       switch (dir) {
+       case UP:
+       case UP_RIGHT:
+       case UP_LEFT:
+               return '^';
+       case DOWN:
+       case DOWN_RIGHT:
+       case DOWN_LEFT:
+               return 'v';
+       case LEFT:
+               return '<';
+       case RIGHT:
+               return '>';
+       case INVALID:
+               break;
+       }
+       abort();
+}
+
+static struct xy move_in_dir(struct xy xy, enum dir dir)
+{
+       switch (dir) {
+       case UP:
+               xy.y = xy.y - 1;
+               return xy;
+       case DOWN:
+               xy.y = xy.y + 1;
+               return xy;
+       case UP_RIGHT:
+               xy.x = xy.x + 1;
+               xy.y = xy.y - 1;
+               return xy;
+       case DOWN_LEFT:
+               xy.x = xy.x - 1;
+               xy.y = xy.y + 1;
+               return xy;
+       case RIGHT:
+               xy.x = xy.x + 1;
+               return xy;
+       case LEFT:
+               xy.x = xy.x - 1;
+               return xy;
+       case DOWN_RIGHT:
+               xy.x = xy.x + 1;
+               xy.y = xy.y + 1;
+               return xy;
+       case UP_LEFT:
+               xy.x = xy.x - 1;
+               xy.y = xy.y - 1;
+               return xy;
+       case INVALID:
+               break;
+       }
+       abort();
+}
+
+static char *sqc(char **sq, struct xy xy)
+{
+       return &sq[xy.y][xy.x];
+}
+
+/* Try straight ahead first, then a bit to either side, then
+ * finally left and right */
+static struct xy scan_move_next(struct xy xy, enum dir start, enum dir *cur)
+{
+       if (*cur == start)
+               *cur = clockwise(start);
+       else if (*cur == clockwise(start))
+               *cur = anticlockwise(start);
+       else if (*cur == anticlockwise(start))
+               *cur = anticlockwise(anticlockwise(start));
+       else if (*cur == anticlockwise(anticlockwise(start)))
+               *cur = clockwise(clockwise(start));
+       else {
+               *cur = INVALID;
+               return xy;
+       }
+       return move_in_dir(xy, *cur);
+}
+
+static void start_perimeter(struct xy *xyp, enum dir *dirp, struct xy xy)
+{
+       *dirp = RIGHT;
+       xyp->x = xy.x - 1;
+       xyp->y = xy.y - 1;
+}
+
+static void next_perimeter(struct xy *xyp, enum dir *dirp, struct xy xy, size_t width)
+{
+       *xyp = move_in_dir(*xyp, *dirp);
+       if (*dirp == RIGHT && xyp->x == xy.x + width)
+               *dirp = DOWN;
+       else if (*dirp == DOWN && xyp->y == xy.y + 1)
+               *dirp = LEFT;
+       else if (*dirp == LEFT && xyp->x == xy.x - 1)
+               *dirp = UP;
+       else if (*dirp == UP && xyp->y == xy.y - 2)
+               *dirp = INVALID;
+}
+
+/* Useful iterators. */
+#define for_each_scan_dir(xyp, dirp, xy, dir)                  \
+       for (*dirp = dir, *xyp = move_in_dir(xy, *dirp);        \
+            *dirp != INVALID;                                  \
+            *xyp = scan_move_next(xy, dir, dirp))
+
+#define for_each_perimeter(xyp, dirp, xy, width)       \
+       for (start_perimeter(xyp, dirp, xy);            \
+            *dirp != INVALID;                          \
+            next_perimeter(xyp, dirp, xy, width))
+
+/* Canonicalizes str into array of characters, finds text strings. */
+static char **square(const tal_t *ctx,
+                    const char *str,
+                    struct text **texts)
+{
+       size_t width = 0, height = 0;
+       size_t line_len = 0;
+       char **sq;
+       struct text *cur_text;
+       size_t strlen;
+
+       *texts = tal_arr(ctx, struct text, 0);
+
+       strlen = 0;
+       for (size_t i = 0; str[i]; i++) {
+               if (str[i] == '\n') {
+                       height++;
+                       line_len = 0;
+               } else {
+                       line_len++;
+                       if (line_len > width)
+                               width = line_len;
+               }
+               strlen++;
+       }
+
+       /* If didn't end in \n, it's implied */
+       if (line_len != 0) {
+               height++;
+               strlen++;
+       }
+
+       /* For analysis simplicity, create a blank border. */
+       sq = tal_arr(ctx, char *, height + 2);
+       for (size_t i = 0; i < height + 2; i++) {
+               sq[i] = tal_arr(sq, char, width + 2);
+               memset(sq[i], ' ', width + 2);
+       }
+
+       /* Copy across and find text */
+       cur_text = NULL;
+       width = height = 1;
+       for (size_t i = 0; i < strlen; i++) {
+               bool end_text;
+               bool eol;
+
+               eol = (str[i] == '\n' || str[i] == '\0');
+               if (!eol)
+                       sq[height][width] = str[i];
+
+               /* v by itself handled separately below */
+               if (strchr(EDGES ARROWS "\n", str[i]) && str[i] != 'v') {
+                       end_text = (cur_text != NULL);
+               } else if (cur_text) {
+                       /* Two spaces ends text */
+                       end_text = (str[i] == ' ' && str[i-1] == ' ') || eol;
+               } else if (str[i] != ' ') {
+                       size_t num_texts = tal_count(*texts);
+                       tal_resize(texts, num_texts+1);
+                       cur_text = &(*texts)[num_texts];
+                       cur_text->xy.x = width;
+                       cur_text->xy.y = height;
+                       cur_text->width = 0;
+                       cur_text->node = NULL;
+                       cur_text->nearest_edge = NULL;
+                       end_text = false;
+               } else
+                       end_text = false;
+
+               if (end_text) {
+                       /* Trim final space */
+                       if (sq[cur_text->xy.y][cur_text->xy.x + cur_text->width-1] == ' ')
+                               cur_text->width--;
+                       /* Ignore lone 'v' */
+                       if (cur_text->width == 1 && sq[cur_text->xy.y][cur_text->xy.x] == 'v')
+                               tal_resize(texts, tal_count(*texts)-1);
+                       else {
+                               cur_text->text = tal_strndup(ctx, &sq[cur_text->xy.y][cur_text->xy.x],
+                                                            cur_text->width);
+                       }
+                       cur_text = NULL;
+               }
+
+               if (cur_text)
+                       cur_text->width++;
+               if (eol) {
+                       height++;
+                       width = 1;
+               } else
+                       width++;
+       }
+
+       return sq;
+}
+
+/* If text was not previously a node, it is now! */
+static const char *text_now_node(const tal_t *ctx,
+                                char **sq,
+                                struct text *text,
+                                void *(*add_node)(const tal_t *ctx,
+                                                  const char *name,
+                                                  const char **errstr,
+                                                  void *arg),
+                                void *arg)
+{
+       const char *err;
+
+       /* Already a node? */
+       if (text->node)
+               return NULL;
+
+       text->node = add_node(ctx, text->text, &err, arg);
+       if (!text->node)
+               return err;
+       return NULL;
+}
+
+static bool correct_line_char(char c, enum dir dir, enum dir *newdir)
+{
+       if (c == line_for_dir(dir)) {
+               *newdir = dir;
+               return true;
+       } else if (c == line_for_dir(anticlockwise(dir))) {
+               *newdir = anticlockwise(dir);
+               return true;
+       } else if (c == line_for_dir(clockwise(dir))) {
+               *newdir = clockwise(dir);
+               return true;
+       }
+       return false;
+}
+
+static bool seek_line(char **sq, struct xy *xy, enum dir *dir)
+{
+       struct xy scan;
+       enum dir scandir;
+
+       for_each_scan_dir(&scan, &scandir, *xy, *dir) {
+               if (correct_line_char(*sqc(sq, scan), scandir, &scandir))
+                       goto found;
+               /* + in front always works */
+               if (*dir == scandir && *sqc(sq, scan) == '+')
+                       goto found;
+       }
+       return false;
+
+found:
+       *xy = scan;
+       *dir = scandir;
+       return true;
+}
+
+static bool seek_arrowhead(char **sq, struct xy *xy, enum dir *dir)
+{
+       struct xy scan;
+       enum dir scandir;
+
+       for_each_scan_dir(&scan, &scandir, *xy, *dir) {
+               if (strchr(ARROWS, *sqc(sq, scan))) {
+                       *xy = scan;
+                       *dir = scandir;
+                       return true;
+               }
+       }
+       return false;
+}
+
+static struct text *in_text(struct text *texts, struct xy xy)
+{
+       for (size_t i = 0; i < tal_count(texts); i++) {
+               if (texts[i].xy.y != xy.y)
+                       continue;
+               if (xy.x >= texts[i].xy.x + texts[i].width)
+                       continue;
+               if (xy.x < texts[i].xy.x)
+                       continue;
+               return texts + i;
+       }
+       return NULL;
+}
+
+static struct text *seek_text(struct text *texts,
+                             struct xy xy, enum dir dir)
+{
+       struct xy scan;
+       enum dir scandir;
+
+       for_each_scan_dir(&scan, &scandir, xy, dir) {
+               struct text *t = in_text(texts, scan);
+               if (t)
+                       return t;
+       }
+       return NULL;
+}
+
+static void erase_line(char **sq,
+                      struct xy xy,
+                      enum dir dir,
+                      enum dir prev_dir)
+{
+       char c = ' ';
+
+       /* If we go straight through a +, convert for crossover */
+       if (prev_dir == dir && *sqc(sq, xy) == '+') {
+               if (dir == UP || dir == DOWN)
+                       c = '-';
+               if (dir == LEFT || dir == RIGHT)
+                       c = '|';
+       }
+       *sqc(sq, xy) = c;
+}
+
+static bool in_nearby(struct text **nearby, const struct text *t)
+{
+       for (size_t i = 0; i < tal_count(nearby); i++) {
+               if (nearby[i] == t)
+                       return true;
+       }
+       return false;
+}
+
+static void add_nearby(struct text ***nearby,
+                      struct text *texts,
+                      struct xy xy)
+{
+       struct xy perim;
+       enum dir pdir;
+       size_t n = tal_count(*nearby);
+
+       for_each_perimeter(&perim, &pdir, xy, 1) {
+               struct text *t = in_text(texts, perim);
+               if (!t)
+                       continue;
+               /* Don't care if it's already a node */
+               if (t->node)
+                       continue;
+               if (in_nearby(*nearby, t))
+                       continue;
+               tal_resize(nearby, n+1);
+               (*nearby)[n++] = t;
+       }
+}
+
+/* Clears lines as it goes. */
+static struct text *follow_line(char **sq,
+                               struct text *texts,
+                               struct xy xy,
+                               enum dir dir,
+                               bool *arrow_src,
+                               bool *arrow_dst,
+                               bool *dangling,
+                               struct text ***nearby)
+{
+       char expect_arrow = arrow_for_dir(opposite_dir(dir));
+       enum dir prev_dir;
+
+       *nearby = tal_arr(sq, struct text *, 0);
+
+       if (*sqc(sq, xy) == expect_arrow) {
+               *arrow_src = true;
+       } else if (*sqc(sq, xy) == line_for_dir(dir)) {
+               *arrow_src = false;
+       } else if (*sqc(sq, xy) == line_for_dir(anticlockwise(dir))) {
+               *arrow_src = false;
+               dir = anticlockwise(dir);
+       } else if (*sqc(sq, xy) == line_for_dir(clockwise(dir))) {
+               *arrow_src = false;
+               dir = clockwise(dir);
+       } else {
+               *dangling = false;
+               /* No arrow is fine. */
+               return NULL;
+       }
+
+       erase_line(sq, xy, dir, INVALID);
+       add_nearby(nearby, texts, xy);
+
+       *arrow_dst = false;
+       prev_dir = dir;
+       for (;;) {
+               /* Try to continue line */
+               if (!*arrow_dst && seek_line(sq, &xy, &dir)) {
+                       erase_line(sq, xy, dir, prev_dir);
+                       add_nearby(nearby, texts, xy);
+                       prev_dir = dir;
+                       continue;
+               }
+               /* Look for arrow */
+               if (!*arrow_dst && seek_arrowhead(sq, &xy, &dir)) {
+                       erase_line(sq, xy, dir, prev_dir);
+                       add_nearby(nearby, texts, xy);
+                       *arrow_dst = true;
+                       prev_dir = dir;
+                       continue;
+               }
+               break;
+       }
+
+       /* Must be in text! */
+       *dangling = true;
+       return seek_text(texts, xy, dir);
+}
+
+static const char *try_create_edge(const tal_t *ctx,
+                                  char **sq,
+                                  struct text *texts,
+                                  struct xy xy,
+                                  struct text *src,
+                                  void *(*add_node)(const tal_t *ctx,
+                                                    const char *name,
+                                                    const char **errstr,
+                                                    void *arg),
+                                  void *arg,
+                                  struct edge **edge)
+{
+       struct text *dst;
+       bool arrow_src, arrow_dst, dangling;
+       struct text **nearby;
+       const char *err;
+
+       *edge = NULL;
+       if (in_text(texts, xy))
+               return NULL;
+
+       dst = follow_line(sq, texts, xy, dir_away(src, xy), &arrow_src, &arrow_dst, &dangling, &nearby);
+       if (!dst) {
+               if (dangling)
+                       return tal_fmt(ctx, "Found dangling arrow at (%zu,%zu)", xy.x-1, xy.y-1);
+               return NULL;
+       }
+
+       /* If you weren't a node before, you are now! */
+       err = text_now_node(ctx, sq, src, add_node, arg);
+       if (err)
+               return err;
+       err = text_now_node(ctx, sq, dst, add_node, arg);
+       if (err)
+               return err;
+
+       /* No arrows equiv to both arrows */
+       if (!arrow_src && !arrow_dst)
+               arrow_src = arrow_dst = true;
+
+       *edge = tal(NULL, struct edge);
+       if (arrow_dst) {
+               (*edge)->src = src;
+               (*edge)->dst = dst;
+               (*edge)->bidir = arrow_src;
+       } else {
+               (*edge)->src = dst;
+               (*edge)->dst = src;
+               (*edge)->bidir = false;
+       }
+       (*edge)->labels = tal_arr(*edge, const char *, 0);
+
+       /* Now record any texts it passed by, in case they're labels */
+       for (size_t i = 0; i < tal_count(nearby); i++) {
+               /* We might have just made it a node */
+               if (nearby[i]->node)
+                       continue;
+               /* Already has an edge?  Mark it as near two, to error
+                * later if it's a label */
+               if (nearby[i]->nearest_edge)
+                       nearby[i]->nearest_edge = &fake_edge;
+               else
+                       nearby[i]->nearest_edge = *edge;
+       }
+               
+       return NULL;
+}
+
+static const char *scan_for_unused(const tal_t *ctx,
+                                  struct text *texts,
+                                  char **sq)
+{
+       struct xy xy;
+       for (xy.y = 0; xy.y < tal_count(sq); xy.y++) {
+               for (xy.x = 0; xy.x < tal_count(sq[xy.y]); xy.x++) {
+                       if (in_text(texts, xy))
+                               continue;
+                       if (*sqc(sq,xy) != ' ')
+                               return tal_fmt(ctx, "Unused '%c' at (%zu,%zu)",
+                                              *sqc(sq, xy), xy.x-1, xy.y-1);
+               }
+       }
+       return NULL;
+}
+
+static void add_label(struct edge *edge, const struct text *label)
+{
+       size_t n = tal_count(edge->labels);
+       tal_resize(&edge->labels, n+1);
+       edge->labels[n] = label->text;
+}
+       
+const char *ungraph_(const tal_t *ctx,
+                    const char *str,
+                    void *(*add_node)(const tal_t *ctx,
+                                      const char *name,
+                                      const char **errstr,
+                                      void *arg),
+                    const char *(*add_edge)(const tal_t *ctx,
+                                            void *source_node,
+                                            void *dest_node,
+                                            bool bidir,
+                                            const char **labels,
+                                            void *arg),
+                    void *arg)
+{
+       /* To hold all our temporaries! */
+       const tal_t *sub = tal(ctx, char);
+       char **sq;
+       struct text *texts, *remaining_label;
+       const char *err;
+       bool progress;
+       struct edge **edges = tal_arr(sub, struct edge *, 0);
+       size_t num_edges = 0;
+
+       /* We create canonical square, find texts. */
+       sq = square(sub, str, &texts);
+
+       /* Now search for arrows around each text, cleaning
+        * as we go! */
+       for (size_t i = 0; i < tal_count(texts); i++) {
+               struct xy perim;
+               enum dir pdir;
+               struct text *t = &texts[i];
+
+               for_each_perimeter(&perim, &pdir, t->xy, t->width) {
+                       struct edge *edge;
+                       err = try_create_edge(ctx, sq, texts, perim, t, add_node, arg, &edge);
+                       if (err)
+                               goto fail;
+                       if (edge) {
+                               tal_resize(&edges, num_edges+1);
+                               edges[num_edges++] = tal_steal(edges, edge);
+                       }
+               }
+       }
+
+       /* Now attach any remaining labels */
+       for (size_t i = 0; i < tal_count(texts); i++) {
+               struct text *t = &texts[i];
+
+               if (t->node)
+                       continue;
+               if (t->nearest_edge == &fake_edge) {
+                       err = tal_fmt(ctx, "Label at (%zu,%zu) near more than one edge",
+                                     t->xy.x-1, t->xy.y-1);
+                       goto fail;
+               }
+               if (t->nearest_edge)
+                       add_label(t->nearest_edge, t);
+       }
+
+       /* Any remaining labels must be attached to already-attached labels */
+       do {
+               progress = false;
+               remaining_label = NULL;
+
+               for (size_t i = 0; i < tal_count(texts); i++) {
+                       struct xy perim;
+                       enum dir pdir;
+                       struct text *t = &texts[i];
+
+                       if (t->node || t->nearest_edge)
+                               continue;
+
+                       remaining_label = t;
+                       for_each_perimeter(&perim, &pdir, t->xy, t->width) {
+                               struct text *neighbor = in_text(texts, perim);
+                               if (!neighbor || neighbor->node || !neighbor->nearest_edge)
+                                       continue;
+                               t->nearest_edge = neighbor->nearest_edge;
+                               add_label(t->nearest_edge, t);
+                               progress = true;
+                               break;
+                       }
+               }
+       } while (progress);
+
+       if (remaining_label) {
+               err = tal_fmt(ctx, "Label at (%zu,%zu) not near any edge",
+                             remaining_label->xy.x-1,
+                             remaining_label->xy.y-1);
+               goto fail;
+       }
+
+       err = scan_for_unused(ctx, texts, sq);
+       if (err)
+               goto fail;
+
+       /* Now add edges, complete with labels */
+       for (size_t i = 0; i < tal_count(edges); i++) {
+               err = add_edge(ctx, edges[i]->src->node, edges[i]->dst->node,
+                              edges[i]->bidir, edges[i]->labels, arg);
+               if (err)
+                       goto fail;
+       }
+       
+       tal_free(sub);
+       return NULL;
+
+fail:
+       tal_free(sub);
+       return err;
+}