Skip to content

Commit 285df86

Browse files
committed
Create a TikZ exporter
The layout is computed using Graphviz.
1 parent 4dd2ecd commit 285df86

17 files changed

Lines changed: 697 additions & 9 deletions

File tree

src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,13 @@ public boolean isAdd() {
919919
return this.diffType.equals(DiffType.ADD);
920920
}
921921

922+
/**
923+
* Returns the diff type of this node.
924+
*/
925+
public DiffType getDiffType() {
926+
return this.diffType;
927+
}
928+
922929
/**
923930
* Returns true if this node represents an ELIF macro.
924931
* @see CodeType#ELIF
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.variantsync.diffdetective.diff.difftree.serialize;
2+
3+
import java.io.IOException;
4+
import java.io.OutputStream;
5+
import org.variantsync.diffdetective.diff.difftree.DiffTree;
6+
7+
/**
8+
* Common interface for serialisation of a single {@code DiffTree}.
9+
* Not all formats have to provide a way to deserialize a {@link DiffTree} from this format.
10+
*
11+
* @author Benjamin Moosherr
12+
*/
13+
public interface Exporter {
14+
/**
15+
* Export a {@code diffTree} into {@code destination}.
16+
*
17+
* This method should have no side effects besides writing to {@code destination}. Above all,
18+
* {@code diffTree} shouldn't be modified. Furthermore, {@code destination} shouldn't be
19+
* closed to allow the embedding of the exported format into a surrounding file.
20+
*
21+
* It can be assumed, that {@code destination} is sufficiently buffered.
22+
*
23+
* @param diffTree to be exported
24+
* @param destination where the result should be written
25+
*/
26+
void exportDiffTree(DiffTree diffTree, OutputStream destination) throws IOException;
27+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package org.variantsync.diffdetective.diff.difftree.serialize;
2+
3+
import java.util.function.Consumer;
4+
import org.variantsync.diffdetective.diff.difftree.DiffNode;
5+
import org.variantsync.diffdetective.diff.difftree.DiffTree;
6+
import org.variantsync.diffdetective.diff.difftree.serialize.edgeformat.EdgeLabelFormat;
7+
import org.variantsync.diffdetective.diff.difftree.serialize.nodeformat.DiffNodeLabelFormat;
8+
9+
/**
10+
* Format used for exporting a {@link DiffTree}.
11+
* For easy reusability this class is composed of separate node and edge formats.
12+
*
13+
* The exported {@link DiffTree} can be influenced in the following ways:
14+
* - Providing both a node and an edge label format.
15+
* - Changing the order, filtering or adding the nodes and edges by creating a subclass of {@code
16+
* Format}.
17+
*/
18+
public class Format {
19+
private final DiffNodeLabelFormat nodeFormat;
20+
private final EdgeLabelFormat edgeFormat;
21+
22+
public Format(DiffNodeLabelFormat nodeFormat, EdgeLabelFormat edgeFormat) {
23+
this.nodeFormat = nodeFormat;
24+
this.edgeFormat = edgeFormat;
25+
}
26+
27+
public DiffNodeLabelFormat getNodeFormat() {
28+
return nodeFormat;
29+
}
30+
31+
public EdgeLabelFormat getEdgeFormat() {
32+
return edgeFormat;
33+
}
34+
35+
/**
36+
* Iterates over all {@link DiffNode}s in {@code diffTree} and calls {@code callback}.
37+
*
38+
* Exporters should use this method to enable subclasses of {@code Format} to filter nodes, add
39+
* new nodes and change the order of the exported nodes.
40+
*
41+
* This implementation is equivalent to {@link DiffTree#forAll}.
42+
*
43+
* @param diffTree to be exported
44+
* @param callback is called for each node
45+
*/
46+
public void forEachNode(DiffTree diffTree, Consumer<DiffNode> callback) {
47+
diffTree.forAll(callback);
48+
}
49+
50+
/**
51+
* Iterates over all edges in {@code diffTree} and calls {@code callback}.
52+
*
53+
* Exporters should use this method to enable subclasses of {@code Format} to filter edges, add
54+
* new edges and change the order of the exported edges.
55+
*
56+
* @param diffTree to be exported
57+
* @param callback is called for each edge
58+
*/
59+
public void forEachEdge(DiffTree diffTree, Consumer<StyledEdge> callback) {
60+
diffTree.forAll((node) -> {
61+
processEdge(node, node.getBeforeParent(), StyledEdge.BEFORE, callback);
62+
processEdge(node, node.getAfterParent(), StyledEdge.AFTER, callback);
63+
});
64+
}
65+
66+
private void processEdge(DiffNode node, DiffNode parent, StyledEdge.Style style, Consumer<StyledEdge> callback) {
67+
if (parent == null) {
68+
return;
69+
}
70+
71+
var edge = edgeFormat.getEdgeDirection().sort(node, parent);
72+
callback.accept(new StyledEdge(
73+
edge.first(),
74+
edge.second(),
75+
style));
76+
}
77+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package org.variantsync.diffdetective.diff.difftree.serialize;
2+
3+
import java.io.BufferedInputStream;
4+
import java.io.BufferedOutputStream;
5+
import java.io.IOException;
6+
import java.io.InputStream;
7+
import java.io.OutputStream;
8+
import java.io.PrintStream;
9+
import java.util.List;
10+
import java.util.regex.Pattern;
11+
import java.util.stream.Collectors;
12+
13+
import org.variantsync.diffdetective.diff.difftree.DiffTree;
14+
15+
/**
16+
* Exporter for the Graphviz dot format.
17+
* Using this exporter the Graphviz application can be used to layout a {@link DiffTree} for
18+
* visualisation.
19+
*
20+
* Currently only basic layout relevant information is exported, so if the result is rendered directly by Graphviz no styling is applied.
21+
*/
22+
public class GraphvizExporter implements Exporter {
23+
private static final Pattern quotePattern = Pattern.compile("[\"\\\\]");
24+
private Format format;
25+
26+
public enum LayoutAlgorithm {
27+
DOT("dot"),
28+
NEATO("neato"),
29+
TWOPI("twopi"),
30+
CIRCO("circo"),
31+
FDP("fdp"),
32+
SFDP("sfdp"),
33+
PATCHWORK("patchwork"),
34+
OSAGE("osage");
35+
36+
private final String executableName;
37+
38+
private LayoutAlgorithm(String executableName) {
39+
this.executableName = executableName;
40+
}
41+
42+
public String getExecutableName() {
43+
return executableName;
44+
}
45+
}
46+
47+
public enum OutputFormat {
48+
// Graphviz supports way more output formats. Add them when necessary.
49+
JPG("jpg"),
50+
JSON("json"),
51+
PDF("pdf"),
52+
PLAIN("plain"),
53+
PLAIN_EXT("plain-ext"),
54+
PNG("png"),
55+
SVG("svg");
56+
57+
private final String formatName;
58+
59+
private OutputFormat(String formatName) {
60+
this.formatName = formatName;
61+
}
62+
63+
public String getFormatName() {
64+
return formatName;
65+
}
66+
}
67+
68+
public GraphvizExporter(Format format) {
69+
this.format = format;
70+
}
71+
72+
/**
73+
* Export {@code diffTree} as Graphviz graph into {@code destination}.
74+
* The exported graph is unstyled, but includes all necessary layout information.
75+
*
76+
* @param diffTree to be exported
77+
* @param destination where the result should be written
78+
*/
79+
@Override
80+
public void exportDiffTree(DiffTree diffTree, OutputStream destination) throws IOException {
81+
var output = new PrintStream(destination);
82+
83+
output.println("digraph g {");
84+
85+
format.forEachNode(diffTree, (node) -> {
86+
output.format(" %d [label=\"%s\"];%n",
87+
node.getID(),
88+
escape(format.getNodeFormat().toMultilineLabel(node)));
89+
});
90+
91+
format.forEachEdge(diffTree, (edge) -> {
92+
output.format(" %d -> %d;%n", edge.from().getID(), edge.to().getID());
93+
});
94+
95+
output.println("}");
96+
output.flush();
97+
}
98+
99+
/**
100+
* Runs the Graphviz {@code dot} program returning its result.
101+
*
102+
* @param diffTree is the tree to be layouted by Graphviz.
103+
* @param outputFormat is the requested format which is passed to the {@code dot} program with
104+
* the {@code -T} flag.
105+
* @return a buffered {@code InputStream} of the Graphviz output
106+
*/
107+
public InputStream computeGraphvizLayout(
108+
DiffTree diffTree,
109+
LayoutAlgorithm algorithm,
110+
OutputFormat outputFormat)
111+
throws IOException {
112+
// Print error messages to stderr so grogramming errors in {@code exportDiffTree} can be
113+
// diagnosed more easily.
114+
var graphvizProcess =
115+
new ProcessBuilder(algorithm.getExecutableName(), "-T" + outputFormat.getFormatName())
116+
.redirectInput(ProcessBuilder.Redirect.PIPE)
117+
.redirectOutput(ProcessBuilder.Redirect.PIPE)
118+
.redirectError(ProcessBuilder.Redirect.INHERIT)
119+
.start();
120+
121+
// This could lead to a dead lock if {@code graphvizProcess} is not consuming its input and
122+
// the OS buffer fills up, but Graphviz needs the whole graph to generate a layout so this
123+
// should be safe.
124+
var graphizInput = new BufferedOutputStream(graphvizProcess.getOutputStream());
125+
exportDiffTree(diffTree, graphizInput);
126+
graphizInput.close();
127+
128+
return new BufferedInputStream(graphvizProcess.getInputStream());
129+
}
130+
131+
/**
132+
* Replaces all special characters with uninterpreted escape codes.
133+
*
134+
* The Graphviz parser for strings interprets some characters specially. The result of this
135+
* function can be used in Graphviz strings (surrounded by double quotes) resulting in all
136+
* characters appearing literally in the output.
137+
*
138+
* Note that some backends of Graphviz may still interpret some strings specially, most
139+
* commonly strings containing HTML tags.
140+
*
141+
* @param label a list of lines to be used as verbatim label
142+
* @return a single string which produces the lines of {@code label} verbatim
143+
*/
144+
static private String escape(List<String> label) {
145+
return label
146+
.stream()
147+
.map((line) -> quotePattern.matcher(line).replaceAll((match) -> "\\\\" + match.group()))
148+
.collect(Collectors.joining("\\n"));
149+
150+
}
151+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.variantsync.diffdetective.diff.difftree.serialize;
2+
3+
import org.variantsync.diffdetective.diff.difftree.DiffNode;
4+
5+
/**
6+
* Product of all data relevant for exporting a single edge.
7+
*
8+
* Note that an edge doesn't need to describe a parent child relationship between {@code from} and
9+
* {@code to}. Information related to the type of the relation between {@code from} and {@code to}
10+
* should be encoded into {@code style}.
11+
*/
12+
public record StyledEdge(DiffNode from, DiffNode to, Style style) {
13+
public record Style(char lineGraphType, String tikzStyle) {
14+
}
15+
16+
public static final Style BEFORE = new Style('b', "before");
17+
public static final Style AFTER = new Style('a', "after");
18+
}

0 commit comments

Comments
 (0)