/* NSC -- new Scala compiler * Copyright 2007-2009 LAMP/EPFL * @author Sean McDirmid */ // $Id: ModelToXML.scala 18387 2009-07-24 15:28:37Z odersky $ package scala.tools.nsc package doc import scala.xml._ /** This class has functionality to format source code models as XML blocks. * * @author Sean McDirmid, Stephane Micheloud */ trait ModelToXML extends ModelExtractor { import global._ import definitions.AnyRefClass import DocUtil._ // decode entity into XML. type Frame protected def urlFor(sym: Symbol)(implicit frame: Frame): String protected def anchor(sym: Symbol)(implicit frame: Frame): NodeSeq def aref(href: String, label: String)(implicit frame: Frame): NodeSeq /* def link(entity: Symbol)(implicit frame: Frame): NodeSeq = { val url = urlFor(entity) // nothing to do but be verbose. if (url == null) Text(entity.owner.fullNameString('.') + '.' + entity.nameString) else aref(url, entity.nameString) } */ def link(entity: Symbol, label: String)(implicit frame: Frame): NodeSeq = { val url = urlFor(entity) if (url == null) { // external link (handled by script.js) val (href, attr) = if (entity.isClass || (entity==AnyRefClass)) ("", entity.owner.fullNameString('/') + '/' + entity.nameString) else ("#" + entity.nameString, entity.owner.fullNameString('/')) val name = entity.owner.fullNameString('.') + '.' + entity.nameString <a href={Utility.escape(href)} class={attr} target="contentFrame">{name}</a>; } else aref(url, label) } def link(entity: Symbol)(implicit frame: Frame): NodeSeq = link(entity, entity.nameString) def link(tpe: Type)(implicit frame: Frame): NodeSeq = { if (!tpe.typeArgs.isEmpty) { if (definitions.isFunctionType(tpe)) { val (args,r) = tpe.normalize.typeArgs.splitAt(tpe.normalize.typeArgs.length - 1); args.mkXML("(", ", ", ")")(link) ++ Text(" => ") ++ link(r.head); } else if (tpe.typeSymbol == definitions.RepeatedParamClass) { assert(tpe.typeArgs.length == 1) link(tpe.typeArgs(0)) ++ Text("*") } else if (tpe.typeSymbol == definitions.ByNameParamClass) { assert(tpe.typeArgs.length == 1) Text("=> ") ++ link(tpe.typeArgs(0)) } else if (tpe.typeSymbol.name.toString.startsWith("Tuple") && tpe.typeSymbol.owner.name == nme.scala_.toTypeName) { tpe.typeArgs.mkXML("(", ", ", ")")(link) } else link(decode(tpe.typeSymbol)) ++ tpe.typeArgs.surround("[", "]")(link) } else tpe match { case PolyType(tparams,result) => link(result) ++ tparams.surround("[", "]")(link) case RefinedType(parents,_) => val parents1 = if ((parents.length > 1) && (parents.head.typeSymbol eq definitions.ObjectClass)) parents.tail; else parents; parents1.mkXML(Text(""), <code> with </code>, Text(""))(link); case _ => if (tpe.typeSymbol == NoSymbol) { throw new Error(tpe + " has no type class " + tpe.getClass) } link(decode(tpe.typeSymbol)) } } private def printIf[T](what: Option[T], before: String, after: String)(f: T => NodeSeq): NodeSeq = if (what.isEmpty) Text("") else Text(before) ++ f(what.get) ++ Text(after) def bodyFor(entity: Entity)(implicit frame: Frame): NodeSeq = try { var seq = {entity.typeParams.surround("[", "]")(e => { Text(e.variance) ++ <em>{e.name}</em> ++ {printIf(e.hi, " <: ", "")(link)} ++ {printIf(e.lo, " >: ", "")(link)} })} ++ printIf(entity.hi, " <: ", "")(link) ++ printIf(entity.lo, " >: ", "")(link); {entity.valueParams.foreach(xs => { seq = seq ++ xs.mkXML("(", ", ", ")")(arg => { val str = arg.flagsString.trim if (str.length == 0) NodeSeq.Empty else <code>{Text(str)} </code> } ++ <em>{arg.name}</em> ++ (try { Text(" : ") ++ link(arg.resultType.get) } catch { case e : Throwable => System.err.println("ARG " + arg + " in " + entity); throw e }) ); seq })}; seq ++ {printIf(entity.resultType, " : ", "")(tpe => link(tpe))} } catch { case e => System.err.println("generating for " + entity); throw e } def extendsFor(entity: Entity)(implicit frame: Frame): NodeSeq = { if (entity.parents.isEmpty) NodeSeq.Empty else <code> extends </code>++ entity.parents.mkXML(Text(""), <code> with </code>, Text(""))(link); } def parse(str: String): NodeSeq = { new SpecialNode { def label = "#PCDATA" def buildString(sb: StringBuilder): StringBuilder = { sb.append(str.trim) sb } } } def longHeader(entity: Entity)(implicit frame: Frame): NodeSeq = Group({ anchor(entity.sym) ++ <dl> <dt> {attrsFor(entity)} <code>{Text(entity.flagsString)}</code> <code>{Text(entity.kind)}</code> <em>{entity.sym.nameString}</em>{bodyFor(entity)} </dt> <dd>{extendsFor(entity)}</dd> </dl>; } ++ { val cmnt = entity.decodeComment if (cmnt.isEmpty) NodeSeq.Empty else longComment(entity, cmnt.get) } ++ (entity match { case entity: ClassOrObject => classBody(entity) case _ => NodeSeq.Empty }) ++ { val overridden = entity.overridden if (overridden.isEmpty) NodeSeq.Empty else { <dl> <dt style="margin:10px 0 0 20px;"> <b>Overrides</b> </dt> <dd> { overridden.mkXML("",", ", "")(sym => link(decode(sym.owner)) ++ Text(".") ++ link(sym)) } </dd> </dl> } } ++ <hr/>); def longComment(entity: Entity, cmnt: Comment)(implicit frame: Frame): NodeSeq = { val attrs = <dl>{ var seq: NodeSeq = NodeSeq.Empty cmnt.decodeAttributes.foreach{ case (tag, xs) => seq = seq ++ <dt style="margin:10px 0 0 20px;"> <b>{decodeTag(tag)}</b></dt> ++ {xs.flatMap{ case (option,body) => <dd>{ if (option == null) NodeSeq.Empty; else decodeOption(tag, option); }{ tag match { case "see" => resolveSee(entity.sym, body.trim) case _ => parse(body) }}</dd> }} }; seq }</dl>; <xml:group> <dl><dd>{parse(cmnt.body)}</dd></dl> {attrs} </xml:group> } /** * Try to be smart about @see elements. If the body looks like a link, turn it into * a link. If it can be resolved in the symbol table, turn it into a link to the referenced * entity. */ private def resolveSee(owner: Symbol, body: String)(implicit frame: Frame): NodeSeq = { /** find a class either in the root package, in the current class or in the current package. */ def findClass(clsName: String): Symbol = { try { definitions.getClass(clsName) } catch { case f: FatalError => try { definitions.getMember(owner, clsName.toTypeName) } catch { case f: FatalError => definitions.getMember(owner.enclosingPackage, clsName.toTypeName) } } } if (body.startsWith("http://") || body.startsWith("https://") || body.startsWith("www")) { // a link body.split(" ") match { case Seq(href, txt, rest @ _*) => <a href={href}>{txt}{rest}</a> case _ => <a href={body}>{body}</a> } } else try { // treat it like a class or member reference body.split("#") match { case Seq(clazz, member) => val clazzSym = if (clazz.length == 0) owner.enclClass else findClass(clazz) link(definitions.getMember(clazzSym, member), body) case Seq(clazz, _*) => link(findClass(clazz), body) case _ => parse(body) } } catch { case f: FatalError => log("Error resolving @see: " + f.toString) parse(body) } } def classBody(entity: ClassOrObject)(implicit from: Frame): NodeSeq = <xml:group> {categories.mkXML("","\n","")(c => shortList(entity, c)) : NodeSeq} {categories.mkXML("","\n","")(c => longList(entity, c)) : NodeSeq} </xml:group>; def longList(entity: ClassOrObject, category: Category)(implicit from: Frame): NodeSeq = { val xs = entity.members(category) if (!xs.iterator.hasNext) NodeSeq.Empty else Group( <table cellpadding="3" class="member-detail" summary=""> <tr><td class="title">{Text(category.label)} Details</td></tr> </table> <div>{xs.mkXML("","\n","")(m => longHeader(m))}</div>) } def shortList(entity: ClassOrObject, category: Category)(implicit from: Frame): NodeSeq = { val xs = entity.members(category) var seq: NodeSeq = NodeSeq.Empty if (xs.iterator.hasNext) { // alphabetic val set = new scala.collection.immutable.TreeSet[entity.Member]()(new Ordering[entity.Member] { def compare(mA : entity.Member, mB: entity.Member): Int = if (mA eq mB) 0 else { val diff = mA.name compare mB.name if (diff != 0) diff else { val diff0 = mA.hashCode - mB.hashCode assert(diff0 != 0, mA.name) diff0 } } })++xs seq = seq ++ <table cellpadding="3" class="member" summary=""> <tr><td colspan="2" class="title">{Text(category.label + " Summary")}</td></tr> {set.mkXML("","\n","")(mmbr => shortHeader(mmbr))} </table> } // list inherited members...if any. for ((tpe,members) <- entity.inherited) { val members0 = members.filter(m => category.f(m.sym)); if (!members0.isEmpty) seq = seq ++ <table cellpadding="3" class="inherited" summary=""> <tr><td colspan="2" class="title"> {Text(category.plural + " inherited from ") ++ link(tpe)} </td></tr> <tr><td colspan="2" class="signature"> {members0.mkXML((""), (", "), (""))(m => { link(decode(m.sym)) ++ (if (m.sym.hasFlag(symtab.Flags.ABSTRACT) || m.sym.hasFlag(symtab.Flags.DEFERRED)) { Text(" (abstract)"); } else NodeSeq.Empty); })} </td></tr> </table> } seq; } protected def decodeOption(tag: String, string: String): NodeSeq = <code>{Text(string + " - ")}</code>; protected def decodeTag(tag: String): String = "" + Character.toUpperCase(tag.charAt(0)) + tag.substring(1) def shortHeader(entity: Entity)(implicit from: Frame): NodeSeq = <tr> <td valign="top" class="modifiers"> <code>{Text(entity.flagsString)} {Text(entity.kind)}</code> </td> <td class="signature"> <em>{link(decode(entity.sym))}</em> {bodyFor(entity) ++ extendsFor(entity)} { entity.resultType match { case Some(PolyType(_, ConstantType(v))) => Text(" = " + v.escapedStringValue) case _ => NodeSeq.Empty } } { val cmnt = entity.decodeComment if (cmnt.isEmpty) NodeSeq.Empty else shortComment(cmnt.get) } </td> </tr> import java.util.regex.Pattern // pattern detecting first line of comment (see ticket #224) private val pat = Pattern.compile("[ \t]*(/\\*)[ \t]*") /** Ticket #224 * Write the first sentence as a short summary of the method, as scaladoc * automatically places it in the method summary table (and index). * (see http://java.sun.com/j2se/javadoc/writingdoccomments/) */ def shortComment(cmnt: Comment): NodeSeq = { val lines = cmnt.body split "<p>" val first = if (lines.length < 2) lines(0) else { val line0 = lines(0) val mat = pat matcher line0 if (mat.matches()) line0 + lines(1) else line0 } <div>{parse(first/*cmnt.body*/)}</div> } def attrsFor(entity: Entity)(implicit from: Frame): NodeSeq = { def attrFor(attr: AnnotationInfo): Node = { val buf = new StringBuilder val AnnotationInfo(tpe, args, nvPairs) = attr val name = link(decode(tpe.typeSymbol)) if (!args.isEmpty) buf.append(args.mkString("(", ",", ")")) if (!nvPairs.isEmpty) for (((name, value), index) <- nvPairs.zipWithIndex) { if (index > 0) buf.append(", ") buf.append(name).append(" = ").append(value) } Group(name ++ Text(buf.toString)) } if (entity.sym.hasFlag(symtab.Flags.CASE)) NodeSeq.Empty; else { val sep = Text("@") val seq = // !!! does it still get confused otherwise? for (attr <- entity.attributes) yield Group({(sep ++ attrFor(attr) ++ <br/>)}) seq } } }