/** * @author Damien Obrist * @author Vlad Ureche */ package scala package tools package nsc package doc package html package page package diagram import scala.xml.{NodeSeq, PrefixedAttribute, Elem, Null, UnprefixedAttribute} import scala.collection.immutable._ import model._ import model.diagram._ class DotDiagramGenerator(settings: doc.Settings, dotRunner: DotRunner) extends DiagramGenerator { // the page where the diagram will be embedded private var page: HtmlPage = null // path to the "lib" folder relative to the page private var pathToLib: String = null // maps nodes to unique indices private var node2Index: Map[Node, Int] = null // true if the current diagram is a class diagram private var isInheritanceDiagram = false // incoming implicit nodes (needed for determining the CSS class of a node) private var incomingImplicitNodes: List[Node] = List() // the suffix used when there are two many classes to show private final val MultiSuffix = " classes/traits" // used to generate unique node and edge ids (i.e. avoid conflicts with multiple diagrams) private var counter = 0 def generate(diagram: Diagram, template: DocTemplateEntity, page: HtmlPage):NodeSeq = { counter = counter + 1 this.page = page pathToLib = "../" * (page.templateToPath(template).size - 1) + "lib/" val dot = generateDot(diagram) val result = generateSVG(dot, template) // clean things up a bit, so we don't leave garbage on the heap this.page = null node2Index = null incomingImplicitNodes = List() result } /** * Generates a dot string for a given diagram. */ private def generateDot(d: Diagram) = { // inheritance nodes (all nodes except thisNode and implicit nodes) var nodes: List[Node] = null // inheritance edges (all edges except implicit edges) var edges: List[(Node, List[Node])] = null // timing var tDot = -System.currentTimeMillis // variables specific to class diagrams: // current node of a class diagram var thisNode:Node = null var subClasses = List[Node]() var superClasses = List[Node]() var incomingImplicits = List[Node]() var outgoingImplicits = List[Node]() isInheritanceDiagram = false d match { case InheritanceDiagram(_thisNode, _superClasses, _subClasses, _incomingImplicits, _outgoingImplicits) => def textTypeEntity(text: String) = new TypeEntity { val name = text def refEntity: SortedMap[Int, (base.LinkTo, Int)] = SortedMap() } // it seems dot chokes on node names over 8000 chars, so let's limit the size of the string // conservatively, we'll limit at 4000, to be sure: def limitSize(str: String) = if (str.length > 4000) str.substring(0, 3996) + " ..." else str // avoid overcrowding the diagram: // if there are too many super / sub / implicit nodes, represent // them by on node with a corresponding tooltip superClasses = if (_superClasses.length > settings.docDiagramsMaxNormalClasses.value) { val superClassesTooltip = Some(limitSize(_superClasses.map(_.tpe.name).mkString(", "))) List(NormalNode(textTypeEntity(_superClasses.length + MultiSuffix), None)(superClassesTooltip)) } else _superClasses subClasses = if (_subClasses.length > settings.docDiagramsMaxNormalClasses.value) { val subClassesTooltip = Some(limitSize(_subClasses.map(_.tpe.name).mkString(", "))) List(NormalNode(textTypeEntity(_subClasses.length + MultiSuffix), None)(subClassesTooltip)) } else _subClasses incomingImplicits = if (_incomingImplicits.length > settings.docDiagramsMaxImplicitClasses.value) { val incomingImplicitsTooltip = Some(limitSize(_incomingImplicits.map(_.tpe.name).mkString(", "))) List(ImplicitNode(textTypeEntity(_incomingImplicits.length + MultiSuffix), None)(incomingImplicitsTooltip)) } else _incomingImplicits outgoingImplicits = if (_outgoingImplicits.length > settings.docDiagramsMaxImplicitClasses.value) { val outgoingImplicitsTooltip = Some(limitSize(_outgoingImplicits.map(_.tpe.name).mkString(", "))) List(ImplicitNode(textTypeEntity(_outgoingImplicits.length + MultiSuffix), None)(outgoingImplicitsTooltip)) } else _outgoingImplicits thisNode = _thisNode nodes = List() edges = (thisNode -> superClasses) :: subClasses.map(_ -> List(thisNode)) node2Index = (thisNode::subClasses:::superClasses:::incomingImplicits:::outgoingImplicits).zipWithIndex.toMap isInheritanceDiagram = true incomingImplicitNodes = incomingImplicits case _ => nodes = d.nodes edges = d.edges node2Index = d.nodes.zipWithIndex.toMap incomingImplicitNodes = List() } val implicitsDot = { if (!isInheritanceDiagram) "" else { // dot cluster containing thisNode val thisCluster = "subgraph clusterThis {\n" + "style=\"invis\"\n" + node2Dot(thisNode) + "}" // dot cluster containing incoming implicit nodes, if any val incomingCluster = { if(incomingImplicits.isEmpty) "" else "subgraph clusterIncoming {\n" + "style=\"invis\"\n" + incomingImplicits.reverse.map(n => node2Dot(n)).mkString + (if (incomingImplicits.size > 1) incomingImplicits.map(n => "node" + node2Index(n)).mkString(" -> ") + " [constraint=\"false\", style=\"invis\", minlen=\"0.0\"];\n" else "") + "}" } // dot cluster containing outgoing implicit nodes, if any val outgoingCluster = { if(outgoingImplicits.isEmpty) "" else "subgraph clusterOutgoing {\n" + "style=\"invis\"\n" + outgoingImplicits.reverse.map(n => node2Dot(n)).mkString + (if (outgoingImplicits.size > 1) outgoingImplicits.map(n => "node" + node2Index(n)).mkString(" -> ") + " [constraint=\"false\", style=\"invis\", minlen=\"0.0\"];\n" else "") + "}" } // assemble clusters into another cluster val incomingTooltip = incomingImplicits.map(_.name).mkString(", ") + " can be implicitly converted to " + thisNode.name val outgoingTooltip = thisNode.name + " can be implicitly converted to " + outgoingImplicits.map(_.name).mkString(", ") "subgraph clusterAll {\n" + "style=\"invis\"\n" + outgoingCluster + "\n" + thisCluster + "\n" + incomingCluster + "\n" + // incoming implicit edge (if (!incomingImplicits.isEmpty) { val n = incomingImplicits.last "node" + node2Index(n) +" -> node" + node2Index(thisNode) + " [id=\"" + cssClass(n, thisNode) + "|" + node2Index(n) + "_" + node2Index(thisNode) + "\", tooltip=\"" + incomingTooltip + "\"" + ", constraint=\"false\", minlen=\"2\", ltail=\"clusterIncoming\", lhead=\"clusterThis\", label=\"implicitly\"];\n" } else "") + // outgoing implicit edge (if (!outgoingImplicits.isEmpty) { val n = outgoingImplicits.head "node" + node2Index(thisNode) + " -> node" + node2Index(n) + " [id=\"" + cssClass(thisNode, n) + "|" + node2Index(thisNode) + "_" + node2Index(n) + "\", tooltip=\"" + outgoingTooltip + "\"" + ", constraint=\"false\", minlen=\"2\", ltail=\"clusterThis\", lhead=\"clusterOutgoing\", label=\"implicitly\"];\n" } else "") + "}" } } // assemble graph val graph = "digraph G {\n" + // graph / node / edge attributes graphAttributesStr + "node [" + nodeAttributesStr + "];\n" + "edge [" + edgeAttributesStr + "];\n" + implicitsDot + "\n" + // inheritance nodes nodes.map(n => node2Dot(n)).mkString + subClasses.map(n => node2Dot(n)).mkString + superClasses.map(n => node2Dot(n)).mkString + // inheritance edges edges.map{ case (from, tos) => tos.map(to => { val id = "graph" + counter + "_" + node2Index(to) + "_" + node2Index(from) // the X -> Y edge is inverted twice to keep the diagram flowing the right way // that is, an edge from node X to Y will result in a dot instruction nodeY -> nodeX [dir="back"] "node" + node2Index(to) + " -> node" + node2Index(from) + " [id=\"" + cssClass(to, from) + "|" + id + "\", " + "tooltip=\"" + from.name + (if (from.name.endsWith(MultiSuffix)) " are subtypes of " else " is a subtype of ") + to.name + "\", dir=\"back\", arrowtail=\"empty\"];\n" }).mkString}.mkString + "}" tDot += System.currentTimeMillis DiagramStats.addDotGenerationTime(tDot) graph } /** * Generates the dot string of a given node. */ private def node2Dot(node: Node) = { // escape HTML characters in node names def escape(name: String) = name.replace("&", "&").replace("<", "<").replace(">", ">") // assemble node attribues in a map val attr = scala.collection.mutable.Map[String, String]() // link node.doctpl match { case Some(tpl) => attr += "URL" -> (page.relativeLinkTo(tpl) + "#inheritance-diagram") case _ => } // tooltip node.tooltip match { case Some(text) => attr += "tooltip" -> text // show full name where available (instead of TraversableOps[A] show scala.collection.parallel.TraversableOps[A]) case None if node.tpl.isDefined => attr += "tooltip" -> node.tpl.get.qualifiedName case _ => } // styles if(node.isImplicitNode) attr ++= implicitStyle else if(node.isOutsideNode) attr ++= outsideStyle else if(node.isTraitNode) attr ++= traitStyle else if(node.isClassNode) attr ++= classStyle else if(node.isObjectNode) attr ++= objectStyle else if(node.isTypeNode) attr ++= typeStyle else attr ++= defaultStyle // HTML label var name = escape(node.name) var img = if(node.isTraitNode) "trait_diagram.png" else if(node.isClassNode) "class_diagram.png" else if(node.isObjectNode) "object_diagram.png" else if(node.isTypeNode) "type_diagram.png" else "" if(!img.equals("")) { img = "
" + name + " |