/** * @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 attributes 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 = name + " " } val label = "<" + "" + img + "" + "
" + name + "
>" // dot does not allow to specify a CSS class, therefore // set the id to "{class}|{id}", which will be used in // the transform method val id = "graph" + counter + "_" + node2Index(node) attr += ("id" -> (cssClass(node) + "|" + id)) // return dot string "node" + node2Index(node) + " [label=" + label + "," + flatten(attr.toMap) + "];\n" } /** * Returns the CSS class for an edge connecting node1 and node2. */ private def cssClass(node1: Node, node2: Node): String = { if (node1.isImplicitNode && node2.isThisNode) "implicit-incoming" else if (node1.isThisNode && node2.isImplicitNode) "implicit-outgoing" else "inheritance" } /** * Returns the CSS class for a node. */ private def cssClass(node: Node): String = if (node.isImplicitNode && incomingImplicitNodes.contains(node)) "implicit-incoming" + cssBaseClass(node, "", " ") else if (node.isImplicitNode) "implicit-outgoing" + cssBaseClass(node, "", " ") else if (node.isThisNode) "this" + cssBaseClass(node, "", " ") else if (node.isOutsideNode) "outside" + cssBaseClass(node, "", " ") else cssBaseClass(node, "default", "") private def cssBaseClass(node: Node, default: String, space: String) = if (node.isClassNode) space + "class" else if (node.isTraitNode) space + "trait" else if (node.isObjectNode) space + "object" else if (node.isTypeNode) space + "type" else default /** * Calls dot with a given dot string and returns the SVG output. */ private def generateSVG(dotInput: String, template: DocTemplateEntity) = { val dotOutput = dotRunner.feedToDot(dotInput, template) var tSVG = -System.currentTimeMillis val result = if (dotOutput != null) { val src = scala.io.Source.fromString(dotOutput) try { val cpa = scala.xml.parsing.ConstructingParser.fromSource(src, preserveWS = false) val doc = cpa.document() if (doc != null) transform(doc.docElem) else NodeSeq.Empty } catch { case exc: Exception => if (settings.docDiagramsDebug) { settings.printMsg("\n\n**********************************************************************") settings.printMsg("Encountered an error while generating page for " + template.qualifiedName) settings.printMsg(dotInput.toString.split("\n").mkString("\nDot input:\n\t","\n\t","")) settings.printMsg(dotOutput.toString.split("\n").mkString("\nDot output:\n\t","\n\t","")) settings.printMsg(exc.getStackTrace.mkString("\nException: " + exc.toString + ":\n\tat ", "\n\tat ","")) settings.printMsg("\n\n**********************************************************************") } else { settings.printMsg("\nThe diagram for " + template.qualifiedName + " could not be created due to an internal error.") settings.printMsg("Use " + settings.docDiagramsDebug.name + " for more information and please file this as a bug.") } NodeSeq.Empty } } else NodeSeq.Empty tSVG += System.currentTimeMillis DiagramStats.addSvgTime(tSVG) result } /** * Transforms the SVG generated by dot: * - adds a class attribute to the SVG element * - changes the path of the node images from absolute to relative * - assigns id and class attributes to nodes and edges * - removes title elements */ private def transform(e:scala.xml.Node): scala.xml.Node = e match { // add an id and class attribute to the SVG element case Elem(prefix, "svg", attribs, scope, child @ _*) => { val klass = if (isInheritanceDiagram) "class-diagram" else "package-diagram" Elem(prefix, "svg", attribs, scope, true, child map(x => transform(x)) : _*) % new UnprefixedAttribute("id", "graph" + counter, Null) % new UnprefixedAttribute("class", klass, Null) } // change the path of the node images from absolute to relative case img @ => { val href = (img \ "@{http://www.w3.org/1999/xlink}href").toString val file = href.substring(href.lastIndexOf("/") + 1, href.size) img.asInstanceOf[Elem] % new PrefixedAttribute("xlink", "href", pathToLib + file, Null) } // assign id and class attributes to edges and nodes: // the id attribute generated by dot has the format: "{class}|{id}" case g @ Elem(prefix, "g", attribs, scope, children @ _*) if (List("edge", "node").contains((g \ "@class").toString)) => { var res = new Elem(prefix, "g", attribs, scope, true, (children map(x => transform(x))): _*) val dotId = (g \ "@id").toString if (dotId.count(_ == '|') == 1) { val Array(klass, id) = dotId.toString.split("\\|") /* Sometimes dot "forgets" to add the image -- that's very annoying, but it seems pretty random, and simple * tests like execute 20K times and diff the output don't trigger the bug -- so it's up to us to place the image * back in the node */ val kind = getKind(klass) if (kind != "") if (((g \ "a" \ "image").isEmpty)) { DiagramStats.addBrokenImage() val xposition = getPosition(g, "x", -22) val yposition = getPosition(g, "y", -11.3334) if (xposition.isDefined && yposition.isDefined) { val imageNode = val anchorNode = (g \ "a") match { case Seq(Elem(prefix, "a", attribs, scope, children @ _*)) => transform(new Elem(prefix, "a", attribs, scope, true, (children ++ imageNode): _*)) case _ => g \ "a" } res = new Elem(prefix, "g", attribs, scope, true, anchorNode: _*) DiagramStats.addFixedImage() } } res % new UnprefixedAttribute("id", id, Null) % new UnprefixedAttribute("class", (g \ "@class").toString + " " + klass, Null) } else res } // remove titles case { _* } => scala.xml.Text("") // apply recursively case Elem(prefix, label, attribs, scope, child @ _*) => Elem(prefix, label, attribs, scope, true, child map(x => transform(x)) : _*) case x => x } def getKind(klass: String): String = if (klass.contains("class")) "class" else if (klass.contains("trait")) "trait" else if (klass.contains("object")) "object" else "" def getPosition(g: scala.xml.Node, axis: String, offset: Double): Option[Double] = { val node = g \ "a" \ "text" \ ("@" + axis) if (node.isEmpty) None else Some(node.toString.toDouble + offset) } /* graph / node / edge attributes */ private val graphAttributes: Map[String, String] = Map( "compound" -> "true", "rankdir" -> "TB" ) private val nodeAttributes = Map( "shape" -> "rect", "style" -> "filled,rounded", "penwidth" -> "1", "margin" -> "0.08,0.01", "width" -> "0.0", "height" -> "0.0", "fontname" -> "Source Code Pro", "fontsize" -> "8.00" ) private val edgeAttributes = Map( "color" -> "#d4d4d4", "arrowsize" -> "0.7", "fontcolor" -> "#aaaaaa", "fontsize" -> "9.00", "fontname" -> "Source Code Pro" ) private val defaultStyle = Map( "color" -> "#ababab", "fillcolor" -> "#e1e1e1", "fontcolor" -> "#7d7d7d", "margin" -> "0.1,0.04" ) private val implicitStyle = Map( "color" -> "#ababab", "fillcolor" -> "#e1e1e1", "fontcolor" -> "#7d7d7d" ) private val outsideStyle = Map( "color" -> "#ababab", "fillcolor" -> "#e1e1e1", "fontcolor" -> "#7d7d7d" ) private val traitStyle = Map( "color" -> "#2E6D82", "fillcolor" -> "#2E6D82", "fontcolor" -> "#ffffff" ) private val classStyle = Map( "color" -> "#418565", "fillcolor" -> "#418565", "fontcolor" -> "#ffffff" ) private val objectStyle = Map( "color" -> "#103A51", "fillcolor" -> "#103A51", "fontcolor" -> "#ffffff" ) private val typeStyle = Map( "color" -> "#2E6D82", "fillcolor" -> "#2E6D82", "fontcolor" -> "#ffffff" ) private def flatten(attributes: Map[String, String]) = attributes.map{ case (key, value) => key + "=\"" + value + "\"" }.mkString(", ") private val graphAttributesStr = graphAttributes.map{ case (key, value) => key + "=\"" + value + "\";\n" }.mkString private val nodeAttributesStr = flatten(nodeAttributes) private val edgeAttributesStr = flatten(edgeAttributes) }