#!/usr/bin/env perl use 5.032; use warnings; use utf8; use experimental 'signatures'; use List::Util 'first'; use File::Temp; use Getopt::Long; use Pod::Usage; sub read_graph_from_file ($file, $option) { open my $fh, '<:utf8', $file or die "Can't open $file for reading: $!"; my $graph = {}; my @stack; my $pos = 0; while (<$fh>) { chomp; next if /\A\s*(#|\z)/; # strip comments and white lines my $prev; my @parts = split /->/; $pos = 0; # if the line starts with an arrow, # try to find a parent in the stack for the first item if ( $parts[0] =~ /\A *\z/ ) { $pos = length shift @parts; $prev = first { $_->{start} <= $pos <= $_->{end} } reverse @stack; die "Can't find the previous node for line $.:\n$_" unless $prev; } # compute each node and add it to the graph while ( my $name = shift @parts ) { my ($indent) = $name =~ /\A(\s*)/g; my $node = { start => $pos + length($indent), end => $pos += length($name), }; $pos += 2; # arrow my $done = $name =~ s/\s*(?:āœ“|\s[+X])\s*\z//; $done = '' if $option->{ignore_done}; $name =~ s/\A\s*|\s*\z//g; $name =~ s/"/\\"/g; $node->{name} = $name; # the graph is about dependencies and done status $graph->{$name} //= { name => $name, prereqs => [] }; $graph->{$name}{done} ||= $done; $graph->{$name}{root} = !$prev; if ($prev) { push $graph->{ $prev->{name} }{prereqs}->@*, $name; } push @stack, $prev = $node; } } close $fh; return $graph; } sub graph_as_dot ( $graph, $option ) { # sort nodes starting from the leaves my @nodes; { my %seen; my @queue = grep $_->{root}, values %$graph; while ( my $node = shift @queue ) { next if $seen{ $node->{name} }++; push @queue, map $graph->{$_}, $node->{prereqs}->@*; push @nodes, $graph->{ $node->{name} }; } } # mark nodes with all prereqs done as done # (starting from the leaves) if ( $option->{auto_done} ) { for my $node ( reverse @nodes ) { next if $node->{done} || !$node->{prereqs}->@*; my $done = 1; $graph->{$_}{done} or $done = 0 for $node->{prereqs}->@*; $node->{done} = $done; } } # header my $dot = "digraph {\n rankdir=$option->{rankdir}\n"; # process all nodes my $dimmed = 'grey60'; for my $node (@nodes) { if ( $node->{root} || $node->{done} ) { next if $option->{hide_done} && $node->{done}; my @attr; # node attributes push @attr, "style=bold" if $node->{root}; push @attr, "color=$dimmed", "fontcolor=$dimmed" if $node->{done}; $dot .= qq{ "$node->{name}" [${\join ',', @attr }]\n}; } for my $prereq ( $node->{prereqs}->@* ) { next if $option->{hide_done} && $graph->{$prereq}{done}; $dot .= qq{ "$node->{name}" -> "$prereq"}; $dot .= " [color=$dimmed]" if $graph->{$prereq}{done}; $dot .= "\n"; } } # footer return $dot . "}\n"; } # main my %option = ( graphviz => 1, # generate the image by default output => 'png', # default format: PNG rankdir => 'RL', # default direction: right-to-left ); GetOptions( \%option, 'graphviz|viz|exec!', # negate to dump the Graphviz code 'output=s', # output format 'rankdir=s', # graph direction 'auto_done|auto-done', # mark nodes with all prereqs done as done 'hide_done|hide-done', # hide done nodes 'ignore_done|ignore-done', # ignore done information 'help', # show the online help ) or pod2usage(1); pod2usage( -verbose => 2 ) if $option{help}; for my $file (@ARGV) { $file =~ /\A(.*)\.[.]+\z/; my $dot = graph_as_dot( read_graph_from_file( $file, \%option ), \%option ); if ( $option{graphviz} ) { $file =~ /\A(.*)(?:\.[^.]*)\z/; my $base = $1 // $file; my $temp = File::Temp->new; $temp->print($dot); my @cmd = ( 'dot', "-T$option{output}", "-o$base.$option{output}", $temp->filename ); system @cmd; } else { print $dot; } } __END__ =encoding utf8 =head1 NAME mikado - Turn a basic Mikado method format into a Graphviz graph =head1 SYNOPSIS mikado [options] [files] =head1 OPTIONS --no-graphviz Print the Graphviz input file instead of running dot --auto-done Mark nodes with all prereqs done as done --hide-done Hide all nodes marked done --ignore-done Ignore "done" information (cancels the options above) --output Set output format (default: png) --rankdir Set graph direction (default: RL) --help Show the manual page =head1 DESCRIPTION B turns a simple input format into a Graphviz-generated graph image, to help work using the L. Example input: A -> B -> C -> Dāœ“ In this simple diagram, the goal C depends on node C only, and C depends on both C and C. digraph { rankdir=RL "A" -> "B" "B" -> "C" { // done nodes node [color=grey60 fontcolor=grey60] "D" } { // edges to done nodes edge [color=grey60] "B" -> "D" } } Note that the C node is marked as "done". This node and the links to it will be grayed out in the generated graph. Instead of the Unicode character C<āœ“>, one can also use C<+> or C at the end of a node to mark it done. =head1 FORMAT DETAILS Nodes are strings of text. Embedded C<\n> will be turned into newlines in the Graphviz image. The dependency relation between nodes is inferred from the position of the C<< -> >> arrows. An arrow between two nodes indicates that the node on the left depends on the node on the right. When the arrow is the first item on the line, the indentation is used to determine the "parent" node. This makes it possible to avoid duplicating nodes in the input file. In Mikado graphs, most nodes have multiple dependencies, and are more rarely a dependencies for multiple nodes. When the it happens, the current solution is duplicate the node text. =head1 AUTHOR Philippe Bruhat (BooK) =head1 COPYRIGHT Copyright 2022 Philippe Bruhat (BooK), all rights reserved. =head1 LICENSE This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut