diff options
author | Gilles Dubochet <gilles.dubochet@epfl.ch> | 2010-03-24 16:59:22 +0000 |
---|---|---|
committer | Gilles Dubochet <gilles.dubochet@epfl.ch> | 2010-03-24 16:59:22 +0000 |
commit | d43ccc679de41aca085072d96a61e363e5e23e34 (patch) | |
tree | 803b33d6e9e7c7116a97687d2963c666571a2bd9 /src/compiler | |
parent | c7c8981b43a6df71e088b444dacf53d609a21ffc (diff) | |
download | scala-d43ccc679de41aca085072d96a61e363e5e23e34.tar.gz scala-d43ccc679de41aca085072d96a61e363e5e23e34.tar.bz2 scala-d43ccc679de41aca085072d96a61e363e5e23e34.zip |
[scaladoc] Improved Scaladoc comment syntax, co...
[scaladoc] Improved Scaladoc comment syntax, contributed by Pedro
Furlanetto.
- Wiki syntax supports nested, numbered and unnumbered lists;
- Wiki syntax supports links (entity links currently require fully qualified names);
- Stars no longer are mandatory to start comment lines.
Already reviewed by dubochet; no review.
Diffstat (limited to 'src/compiler')
12 files changed, 337 insertions, 241 deletions
diff --git a/src/compiler/scala/tools/nsc/doc/DocFactory.scala b/src/compiler/scala/tools/nsc/doc/DocFactory.scala index b70d8c10ec..c4d231d750 100644 --- a/src/compiler/scala/tools/nsc/doc/DocFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/DocFactory.scala @@ -53,8 +53,8 @@ class DocFactory(val reporter: Reporter, val settings: doc.Settings) { processor assert(settings.docformat.value == "html") if (!reporter.hasErrors) { val modelFactory = (new model.ModelFactory(compiler, settings)) - val htmlFactory = (new html.HtmlFactory(reporter, settings)) val docModel = modelFactory.makeModel + val htmlFactory = (new html.HtmlFactory(docModel)) println("model contains " + modelFactory.templatesCount + " documentable templates") htmlFactory generate docModel } diff --git a/src/compiler/scala/tools/nsc/doc/Universe.scala b/src/compiler/scala/tools/nsc/doc/Universe.scala new file mode 100644 index 0000000000..666a06dc4b --- /dev/null +++ b/src/compiler/scala/tools/nsc/doc/Universe.scala @@ -0,0 +1,8 @@ +package scala.tools.nsc.doc + +/** + * Class to hold common dependencies across Scaladoc classes. + * @author Pedro Furlanetto + * @author Gilles Dubochet + */ +class Universe(val settings: Settings, val rootPackage: model.Package) diff --git a/src/compiler/scala/tools/nsc/doc/html/HtmlFactory.scala b/src/compiler/scala/tools/nsc/doc/html/HtmlFactory.scala index a78c122798..f01984052f 100644 --- a/src/compiler/scala/tools/nsc/doc/html/HtmlFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/html/HtmlFactory.scala @@ -16,19 +16,19 @@ import scala.collection._ /** A class that can generate Scaladoc sites to some fixed root folder. * @author David Bernard * @author Gilles Dubochet */ -class HtmlFactory(val reporter: Reporter, val settings: Settings) { +class HtmlFactory(val universe: Universe) { /** The character encoding to be used for generated Scaladoc sites. This value is currently always UTF-8. */ def encoding: String = "UTF-8" /** The character encoding to be used for generated Scaladoc sites. This value is defined by the generator's * settings. */ - def siteRoot: File = new File(settings.outdir.value) + def siteRoot: File = new File(universe.settings.outdir.value) /** Generates the Scaladoc site for a model into the site toot. A scaladoc site is a set of HTML and related files * that document a model extracted from a compiler run. * @param model The model to generate in the form of a sequence of packages. */ - def generate(modelRoot: Package): Unit = { + def generate(universe: Universe): Unit = { def copyResource(subPath: String) { val buf = new Array[Byte](1024) @@ -67,7 +67,7 @@ class HtmlFactory(val reporter: Reporter, val settings: Settings) { copyResource("lib/filter_box_right.png") copyResource("lib/remove.png") - new page.Index(modelRoot) writeFor this + new page.Index(universe) writeFor this val written = mutable.HashSet.empty[DocTemplateEntity] @@ -77,7 +77,7 @@ class HtmlFactory(val reporter: Reporter, val settings: Settings) { tpl.templates filter { t => !(written contains t) } map (writeTemplate(_)) } - writeTemplate(modelRoot) + writeTemplate(universe.rootPackage) } diff --git a/src/compiler/scala/tools/nsc/doc/html/HtmlPage.scala b/src/compiler/scala/tools/nsc/doc/html/HtmlPage.scala index 520bcb9a36..cea8d04caa 100644 --- a/src/compiler/scala/tools/nsc/doc/html/HtmlPage.scala +++ b/src/compiler/scala/tools/nsc/doc/html/HtmlPage.scala @@ -133,8 +133,8 @@ abstract class HtmlPage { thisPage => case Code(data) => <pre>{ Unparsed(data) }</pre> case UnorderedList(items) => <ul>{ listItemsToHtml(items) }</ul> - case OrderedList(items) => - <ol>{ listItemsToHtml(items) }</ol> + case OrderedList(items, listStyle) => + <ol class={ listStyle }>{ listItemsToHtml(items) }</ol> case DefinitionList(items) => <dl>{items map { case (t, d) => <dt>{ inlineToHtml(t) }</dt><dd>{ blockToHtml(d) }</dd> } }</dl> case HorizontalRule() => @@ -144,7 +144,7 @@ abstract class HtmlPage { thisPage => def listItemsToHtml(items: Seq[Block]) = items.foldLeft(xml.NodeSeq.Empty){ (xmlList, item) => item match { - case OrderedList(_) | UnorderedList(_) => // html requires sub ULs to be put into the last LI + case OrderedList(_, _) | UnorderedList(_) => // html requires sub ULs to be put into the last LI xmlList.init ++ <li>{ xmlList.last.child ++ blockToHtml(item) }</li> case Paragraph(inline) => xmlList :+ <li>{ inlineToHtml(inline) }</li> // LIs are blocks, no need to use Ps @@ -154,14 +154,14 @@ abstract class HtmlPage { thisPage => } def inlineToHtml(inl: Inline): NodeSeq = inl match { - //case URLLink(url, text) => <a href={url}>{if(text.isEmpty)url else inlineSeqsToXml(text)}</a> case Chain(items) => items flatMap (inlineToHtml(_)) case Italic(in) => <i>{ inlineToHtml(in) }</i> case Bold(in) => <b>{ inlineToHtml(in) }</b> case Underline(in) => <u>{ inlineToHtml(in) }</u> case Superscript(in) => <sup>{ inlineToHtml(in) }</sup> case Subscript(in) => <sub>{ inlineToHtml(in) }</sub> - case Link(raw,title) => <a href={ raw }>{ title.getOrElse(raw) }</a> // TODO link to target + case Link(raw, title) => <a href={ raw }>{ inlineToHtml(title) }</a> + case EntityLink(entity) => templateToHtml(entity) case Monospace(text) => <code>{ Unparsed(text) }</code> case Text(text) => Unparsed(text) } @@ -183,7 +183,7 @@ abstract class HtmlPage { thisPage => val (tpl, width) = tpe.refEntity(inPos) (tpl match { case dtpl:DocTemplateEntity if hasLinks => - <a href={ relativeLinkTo(tpl) } class="extype" name={ dtpl.qualifiedName }>{ + <a href={ relativeLinkTo(dtpl) } class="extype" name={ dtpl.qualifiedName }>{ string.slice(inPos, inPos + width) }</a> case tpl => @@ -199,7 +199,7 @@ abstract class HtmlPage { thisPage => /** Returns the HTML code that represents the template in `tpl` as a hyperlinked name. */ def templateToHtml(tpl: TemplateEntity) = tpl match { case dTpl: DocTemplateEntity => - <a href={ relativeLinkTo(dTpl) }>{ dTpl.name }</a> + <a href={ relativeLinkTo(dTpl) } class="extype" name={ dTpl.qualifiedName }>{ dTpl.name }</a> case ndTpl: NoDocTemplate => xml.Text(ndTpl.name) } diff --git a/src/compiler/scala/tools/nsc/doc/html/page/Index.scala b/src/compiler/scala/tools/nsc/doc/html/page/Index.scala index cdc040c15d..7bbd8ef821 100644 --- a/src/compiler/scala/tools/nsc/doc/html/page/Index.scala +++ b/src/compiler/scala/tools/nsc/doc/html/page/Index.scala @@ -13,11 +13,15 @@ import model._ import scala.collection._ import scala.xml._ -class Index(modelRoot: Package) extends HtmlPage { +class Index(universe: Universe) extends HtmlPage { def path = List("index.html") - def title = "Scaladoc: all classes and objects" + def title = { + val s = universe.settings + ( if (!s.doctitle.isDefault) s.doctitle.value else "" ) + + ( if (!s.docversion.isDefault) (" " + s.docversion.value) else "" ) + } def headers = <xml:group> @@ -74,7 +78,7 @@ class Index(modelRoot: Package) extends HtmlPage { }</ol> </xml:group> } - packageElem(modelRoot) + packageElem(universe.rootPackage) }</div> </div> <div id="content"> diff --git a/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.css b/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.css index 64c3e0ad8a..a23c8b6402 100644 --- a/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.css +++ b/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.css @@ -176,12 +176,37 @@ div.members > ol > li { margin: 2px 0 2px 0; } -.cmt ul, .cmt ol { +.cmt ul { display: block; list-style: circle; padding-left:20px; } +.cmt ol { + display: block; + padding-left:20px; +} + +.cmt ol.decimal { + list-style: decimal; +} + +.cmt ol.lowerAlpha { + list-style: lower-alpha; +} + +.cmt ol.upperAlpha { + list-style: upper-alpha; +} + +.cmt ol.lowerRoman { + list-style: lower-roman; +} + +.cmt ol.upperRoman { + list-style: upper-roman; +} + .cmt li { display:list-item; } diff --git a/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.js b/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.js index 4ff278e80b..cb159a97c0 100644 --- a/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.js +++ b/src/compiler/scala/tools/nsc/doc/html/resource/lib/template.js @@ -33,7 +33,7 @@ $(document).ready(function(){ filterInherit(); }); //http://flowplayer.org/tools/tooltip.html - $(".signature .symbol .extype").tooltip({ + $(".extype").tooltip({ tip: "#tooltip", position:"top center", onBeforeShow: function(ev) { diff --git a/src/compiler/scala/tools/nsc/doc/model/Entity.scala b/src/compiler/scala/tools/nsc/doc/model/Entity.scala index 2362016152..f3cf0518f1 100644 --- a/src/compiler/scala/tools/nsc/doc/model/Entity.scala +++ b/src/compiler/scala/tools/nsc/doc/model/Entity.scala @@ -111,6 +111,9 @@ trait Package extends Object { def packages: List[Package] } +/** A package represent the root of Entities hierarchy */ +trait RootPackage extends Package + trait NonTemplateMemberEntity extends MemberEntity { def isUseCase: Boolean } diff --git a/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala b/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala index 44b542db0a..f7fe5678fe 100644 --- a/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/model/ModelFactory.scala @@ -10,8 +10,10 @@ import scala.collection._ import symtab.Flags +import model.{ RootPackage => RootPackageEntity } + /** This trait extracts all required information for documentation from compilation units */ -class ModelFactory(val global: Global, val settings: doc.Settings) { extractor => +class ModelFactory(val global: Global, val settings: doc.Settings) extends CommentFactory { thisFactory => import global._ import definitions.{ ObjectClass, ScalaObjectClass, RootPackage, EmptyPackage } @@ -19,41 +21,26 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { extractor = private var droppedPackages = 0 def templatesCount = templatesCache.size - droppedPackages - /** */ - def makeModel: Package = - makePackage(RootPackage, null) getOrElse abort("no documentable class found in compilation units") - - object commentator { - - val factory = new CommentFactory(reporter) - - private val commentCache = mutable.HashMap.empty[(Symbol, TemplateImpl), Comment] - - def registeredUseCase(sym: Symbol, inTpl: => TemplateImpl, docStr: String, docPos: Position): Symbol = { - commentCache += (sym, inTpl) -> factory.parse(docStr, docPos) - sym - } - - def comment(sym: Symbol, inTpl: => DocTemplateImpl): Option[Comment] = { - val key = (sym, inTpl) - if (commentCache isDefinedAt key) - Some(commentCache(key)) - else { // not reached for use-case comments - val rawComment = expandedDocComment(sym, inTpl.sym) - if (rawComment == "") None else { - val c = factory.parse(rawComment, docCommentPos(sym)) - commentCache += (sym, inTpl) -> c - Some(c) - } - } - } + private var modelFinished = false + /** */ + def makeModel: Universe = { + val rootPackage = + makeRootPackage getOrElse { throw new Error("no documentable class found in compilation units") } + val universe = new Universe(settings, rootPackage) + modelFinished = true + universe } /** */ protected val templatesCache = new mutable.LinkedHashMap[(Symbol, TemplateImpl), DocTemplateImpl] + def findTemplate(query: String): Option[DocTemplateImpl] = { + if (!modelFinished) throw new Error("cannot find template in unfinished universe") + templatesCache.values find { tpl => tpl.qualifiedName == query && !tpl.isObject } + } + def optimize(str: String): String = if (str.length < 16) str.intern else str @@ -87,13 +74,13 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { extractor = /** Provides a default implementation for instances of the `MemberEntity` type. It must be instantiated as a * `SymbolicEntity` to access the compiler symbol that underlies the entity. */ abstract class MemberImpl(sym: Symbol, inTpl: => DocTemplateImpl) extends EntityImpl(sym, inTpl) with MemberEntity { - val comment = - if (inTpl == null) None else commentator.comment(sym, inTpl) + lazy val comment = + if (inTpl == null) None else thisFactory.comment(sym, inTpl) override def inTemplate = inTpl override def toRoot: List[MemberImpl] = this :: inTpl.toRoot def inDefinitionTemplates = if (inTpl == null) - makePackage(RootPackage, null).toList + makeRootPackage.toList else makeTemplate(sym.owner) :: (sym.allOverriddenSymbols map { inhSym => makeTemplate(inhSym.owner) }) def visibility = { @@ -121,7 +108,7 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { extractor = } def deprecation = if (sym.isDeprecated && sym.deprecationMessage.isDefined) - Some(commentator.factory.parseWiki(sym.deprecationMessage.get, NoPosition)) + Some(parseWiki(sym.deprecationMessage.get, NoPosition)) else if (sym.isDeprecated) Some(Body(Nil)) else if (comment.isDefined) @@ -209,6 +196,8 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { extractor = val packages = members partialMap { case p: Package => p } } + abstract class RootPackageImpl(sym: Symbol) extends PackageImpl(sym, null) with RootPackageEntity + abstract class NonTemplateMemberImpl(sym: Symbol, inTpl: => DocTemplateImpl) extends MemberImpl(sym, inTpl) with NonTemplateMemberEntity { override def qualifiedName = optimize(inTemplate.qualifiedName + "#" + name) override def definitionName = optimize(inDefinitionTemplates.head.qualifiedName + "#" + name) @@ -232,6 +221,9 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { extractor = aSym } + def makeRootPackage: Option[PackageImpl] = + makePackage(RootPackage, null) + /** Creates a package entity for the given symbol or returns `None` if the symbol does not denote a package that * contains at least one ''documentable'' class, trait or object. Creating a package entity */ def makePackage(aSym: Symbol, inTpl: => PackageImpl): Option[PackageImpl] = { @@ -241,7 +233,7 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { extractor = else { val pack = if (bSym == RootPackage) - new PackageImpl(bSym, null) { + new RootPackageImpl(bSym) { override val name = "root" override def inTemplate = this override def toRoot = this :: Nil @@ -278,7 +270,7 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { extractor = val bSym = normalizeTemplate(aSym) if (bSym.isPackage) inTpl match { case inPkg: PackageImpl => makePackage(bSym, inPkg) getOrElse (new NoDocTemplateImpl(bSym, inPkg)) - case _ => abort("'" + bSym + "' must be in a package") + case _ => throw new Error("'" + bSym + "' must be in a package") } else if ((bSym.sourceFile != null) && bSym.isPublic && !bSym.isLocal) inTpl match { case inDTpl: DocTemplateImpl => makeDocTemplate(bSym, inDTpl) @@ -317,7 +309,7 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { extractor = def isCaseClass = sym.isClass && sym.hasFlag(Flags.CASE) } else - abort("'" + bSym + "' that isn't a class, trait or object cannot be built as a documentable template") + throw new Error("'" + bSym + "' that isn't a class, trait or object cannot be built as a documentable template") } /** */ @@ -385,7 +377,7 @@ class ModelFactory(val global: Global, val settings: doc.Settings) { extractor = Nil else { val allSyms = useCases(aSym, inTpl.sym) map { case (bSym, bComment, bPos) => - commentator.registeredUseCase(bSym, inTpl, bComment, bPos) + addCommentBody(bSym, inTpl, bComment, bPos) } (allSyms ::: List(aSym)) flatMap (makeMember0(_)) } diff --git a/src/compiler/scala/tools/nsc/doc/model/comment/Body.scala b/src/compiler/scala/tools/nsc/doc/model/comment/Body.scala index c87ecb7d4d..7d2aee2d98 100644 --- a/src/compiler/scala/tools/nsc/doc/model/comment/Body.scala +++ b/src/compiler/scala/tools/nsc/doc/model/comment/Body.scala @@ -19,7 +19,7 @@ final case class Title(text: Inline, level: Int) extends Block final case class Paragraph(text: Inline) extends Block final case class Code(data: String) extends Block final case class UnorderedList(items: Seq[Block]) extends Block -final case class OrderedList(items: Seq[Block]) extends Block +final case class OrderedList(items: Seq[Block], style: String) extends Block final case class DefinitionList(items: SortedMap[Inline, Block]) extends Block final case class HorizontalRule() extends Block @@ -32,6 +32,7 @@ final case class Bold(text: Inline) extends Inline final case class Underline(text: Inline) extends Inline final case class Superscript(text: Inline) extends Inline final case class Subscript(text: Inline) extends Inline -final case class Link(target: String, title: Option[String]) extends Inline +final case class Link(target: String, title: Inline) extends Inline +final case class EntityLink(target: TemplateEntity) extends Inline final case class Monospace(text: String) extends Inline final case class Text(text: String) extends Inline diff --git a/src/compiler/scala/tools/nsc/doc/model/comment/Comment.scala b/src/compiler/scala/tools/nsc/doc/model/comment/Comment.scala index 762ff843cf..7fe2e58991 100644 --- a/src/compiler/scala/tools/nsc/doc/model/comment/Comment.scala +++ b/src/compiler/scala/tools/nsc/doc/model/comment/Comment.scala @@ -65,7 +65,8 @@ abstract class Comment { override def toString = body.toString + "\n" + (authors map ("@author " + _.toString)).mkString("\n") + - (result map ("@return " + _.toString)).mkString + (result map ("@return " + _.toString)).mkString("\n") + + (version map ("@version " + _.toString)).mkString } diff --git a/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala b/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala index e39942e029..5c5b320c34 100644 --- a/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala +++ b/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala @@ -19,28 +19,42 @@ import scala.annotation.switch * * @author Manohar Jonnalagedda * @author Gilles Dubochet */ -final class CommentFactory(val reporter: Reporter) { parser => +trait CommentFactory { thisFactory: ModelFactory with CommentFactory => - val endOfText = '\u0003' - val endOfLine = '\u000A' + val global: Global + import global.reporter - /** Something that should not have happened, happened, and Scaladoc should exit. */ - protected def oops(msg: String): Nothing = - throw FatalError("program logic: " + msg) + private val commentCache = mutable.HashMap.empty[(global.Symbol, TemplateImpl), Comment] - protected val CleanHtml = - new Regex("""</?(p|h\d|pre|dl|dt|dd|ol|ul|li|blockquote|div|hr|br|br).*/?>""") + def addCommentBody(sym: global.Symbol, inTpl: => TemplateImpl, docStr: String, docPos: global.Position): global.Symbol = { + commentCache += (sym, inTpl) -> parse(docStr, docPos) + sym + } - protected val ShortLineEnd = - new Regex("""\.|</?.*>""") + def comment(sym: global.Symbol, inTpl: => DocTemplateImpl): Option[Comment] = { + val key = (sym, inTpl) + if (commentCache isDefinedAt key) + Some(commentCache(key)) + else { // not reached for use-case comments + val rawComment = global.expandedDocComment(sym, inTpl.sym).trim + if (rawComment == "") None else { + val c = parse(rawComment, global.docCommentPos(sym)) + commentCache += (sym, inTpl) -> c + Some(c) + } + } + } + + protected val endOfText = '\u0003' + protected val endOfLine = '\u000A' - /** The body of a comment, dropping start and end markers. */ - protected val CleanComment = - new Regex("""(?s)\s*/\*\*((?:[^\*]\*)*)\*/\s*""") + /** Something that should not have happened, happened, and Scaladoc should exit. */ + protected def oops(msg: String): Nothing = + throw FatalError("program logic: " + msg) - /** The body of a line, dropping the start star-marker, one leading whitespace and all trailing whitespace. */ + /** The body of a line, dropping the (optional) start star-marker, one leading whitespace and all trailing whitespace. */ protected val CleanCommentLine = - new Regex("""\*\s?(.*)""") + new Regex("""(?:\s*\*\s?)?(.*)""") /** A Scaladoc tag not linked to a symbol. Returns the name of the tag, and the rest of the line. */ protected val SimpleTag = @@ -71,155 +85,157 @@ final class CommentFactory(val reporter: Reporter) { parser => /** Parses a raw comment string into a `Comment` object. * @param comment The raw comment string (including start and end markers) to be parsed. * @param pos The position of the comment in source. */ - def parse(comment: String, pos: Position): Comment = { + protected def parse(comment: String, pos: Position): Comment = { /** The cleaned raw comment as a list of lines. Cleaning removes comment start and end markers, line start markers * and unnecessary whitespace. */ val cleaned: List[String] = { - def cleanLine(line: String): Option[String] = { - line.trim match { - case CleanCommentLine(ctl) => Some(ctl) - case "" => - None + def cleanLine(line: String): String = { + //replaceAll removes trailing whitespaces + line.replaceAll("""\s+$""", "") match { + case "" => "" // Empty lines are require to keep paragraphs + case CleanCommentLine(ctl) => ctl case tl => - reporter.warning(pos, "Comment has no start-of-line marker ('*')") - Some(tl) + reporter.warning(pos, "Please re-check this line of the comment") + tl + } } - } - comment.trim.stripPrefix("/*").stripSuffix("*/").lines.toList flatMap (cleanLine(_)) - } - - /** Parses a comment (in the form of a list of lines) to a Comment instance, recursively on lines. To do so, it - * splits the whole comment into main body and tag bodies, then runs the `WikiParser` on each body before creating - * the comment instance. - * - * @param body The body of the comment parsed until now. - * @param tags All tags parsed until now. - * @param lastTagKey The last parsed tag, or `None` if the tag section hasn't started. Lines that are not tagged - * are part of the previous tag or, if none exists, of the body. - * @param remaining The lines that must still recursively be parsed. - * @param inCodeBlock Whether the next line is part of a code block (in which no tags must be read). */ - def parse0(docBody: String, tags: Map[TagKey, List[String]], lastTagKey: Option[TagKey], remaining: List[String], inCodeBlock: Boolean): Comment = { - remaining match { - - case CodeBlockStart(before, after) :: ls if (!inCodeBlock) => - if (before.trim != "") - parse0(docBody, tags, lastTagKey, before :: ("{{{" + after) :: ls, false) - else if (after.trim != "") - parse0(docBody, tags, lastTagKey, after :: ls, true) - else - parse0(docBody, tags, lastTagKey, ls, true) + comment.trim.stripPrefix("/*").stripSuffix("*/").lines.toList map (cleanLine(_)) + } + + /** Parses a comment (in the form of a list of lines) to a Comment instance, recursively on lines. To do so, it + * splits the whole comment into main body and tag bodies, then runs the `WikiParser` on each body before creating + * the comment instance. + * + * @param body The body of the comment parsed until now. + * @param tags All tags parsed until now. + * @param lastTagKey The last parsed tag, or `None` if the tag section hasn't started. Lines that are not tagged + * are part of the previous tag or, if none exists, of the body. + * @param remaining The lines that must still recursively be parsed. + * @param inCodeBlock Whether the next line is part of a code block (in which no tags must be read). */ + def parse0(docBody: String, tags: Map[TagKey, List[String]], lastTagKey: Option[TagKey], remaining: List[String], inCodeBlock: Boolean): Comment = { + remaining match { + + case CodeBlockStart(before, after) :: ls if (!inCodeBlock) => + if (before.trim != "") + parse0(docBody, tags, lastTagKey, before :: ("{{{" + after) :: ls, false) + else if (after.trim != "") + parse0(docBody, tags, lastTagKey, after :: ls, true) + else + parse0(docBody, tags, lastTagKey, ls, true) + + case CodeBlockEnd(before, after) :: ls => + if (before.trim != "") + parse0(docBody, tags, lastTagKey, before :: ("}}}" + after) :: ls, true) + else if (after.trim != "") + parse0(docBody, tags, lastTagKey, after :: ls, false) + else + parse0(docBody, tags, lastTagKey, ls, false) + + case SymbolTag(name, sym, body) :: ls if (!inCodeBlock) => + val key = SymbolTagKey(name, sym) + val value = body :: tags.getOrElse(key, Nil) + parse0(docBody, tags + (key -> value), Some(key), ls, inCodeBlock) + + case SimpleTag(name, body) :: ls if (!inCodeBlock) => + val key = SimpleTagKey(name) + val value = body :: tags.getOrElse(key, Nil) + parse0(docBody, tags + (key -> value), Some(key), ls, inCodeBlock) + + case line :: ls if (lastTagKey.isDefined) => + val key = lastTagKey.get + val value = + ((tags get key): @unchecked) match { + case Some(b :: bs) => (b + endOfLine + line) :: bs + case None => oops("lastTagKey set when no tag exists for key") + } + parse0(docBody, tags + (key -> value), lastTagKey, ls, inCodeBlock) - case CodeBlockEnd(before, after) :: ls => - if (before.trim != "") - parse0(docBody, tags, lastTagKey, before :: ("}}}" + after) :: ls, true) - else if (after.trim != "") - parse0(docBody, tags, lastTagKey, after :: ls, false) - else - parse0(docBody, tags, lastTagKey, ls, false) - - case SymbolTag(name, sym, body) :: ls if (!inCodeBlock) => - val key = SymbolTagKey(name, sym) - val value = body :: tags.getOrElse(key, Nil) - parse0(docBody, tags + (key -> value), Some(key), ls, inCodeBlock) - - case SimpleTag(name, body) :: ls if (!inCodeBlock) => - val key = SimpleTagKey(name) - val value = body :: tags.getOrElse(key, Nil) - parse0(docBody, tags + (key -> value), Some(key), ls, inCodeBlock) - - case line :: ls if (lastTagKey.isDefined) => - val key = lastTagKey.get - val value = - ((tags get key): @unchecked) match { - case Some(b :: bs) => (b + endOfLine + line) :: bs - case None => oops("lastTagKey set when no tag exists for key") - } - parse0(docBody, tags + (key -> value), lastTagKey, ls, inCodeBlock) + case line :: ls => + val newBody = if (docBody == "") line else docBody + endOfLine + line + parse0(newBody, tags, lastTagKey, ls, inCodeBlock) - case line :: ls => - val newBody = if (docBody == "") line else docBody + endOfLine + line - parse0(newBody, tags, lastTagKey, ls, inCodeBlock) + case Nil => - case Nil => + val bodyTags: mutable.Map[TagKey, List[Body]] = + mutable.Map(tags mapValues (_ map (parseWiki(_, pos))) toSeq: _*) - val bodyTags: mutable.Map[TagKey, List[Body]] = - mutable.Map(tags mapValues (_ map (parseWiki(_, pos))) toSeq: _*) + def oneTag(key: SimpleTagKey): Option[Body] = + ((bodyTags remove key): @unchecked) match { + case Some(r :: rs) => + if (!rs.isEmpty) reporter.warning(pos, "Only one '@" + key.name + "' tag is allowed") + Some(r) + case None => None + } - def oneTag(key: SimpleTagKey): Option[Body] = - ((bodyTags remove key): @unchecked) match { - case Some(r :: rs) => - if (!rs.isEmpty) reporter.warning(pos, "Only one '@" + key.name + "' tag is allowed") - Some(r) - case None => None + def allTags(key: SimpleTagKey): List[Body] = + (bodyTags remove key) getOrElse Nil + + def allSymsOneTag(key: TagKey): Map[String, Body] = { + val keys: Seq[SymbolTagKey] = + bodyTags.keys.toSeq flatMap { + case stk: SymbolTagKey if (stk.name == key.name) => Some(stk) + case stk: SimpleTagKey if (stk.name == key.name) => + reporter.warning(pos, "Tag '@" + stk.name + "' must be followed by a symbol name") + None + case _ => None + } + val pairs: Seq[(String, Body)] = + for (key <- keys) yield { + val bs = (bodyTags remove key).get + if (bs.length > 1) + reporter.warning(pos, "Only one '@" + key.name + "' tag for symbol " + key.symbol + " is allowed") + (key.symbol, bs.head) + } + Map.empty[String, Body] ++ pairs } - def allTags(key: SimpleTagKey): List[Body] = - (bodyTags remove key) getOrElse Nil - - def allSymsOneTag(key: TagKey): Map[String, Body] = { - val keys: Seq[SymbolTagKey] = - bodyTags.keys.toSeq flatMap { - case stk: SymbolTagKey if (stk.name == key.name) => Some(stk) - case stk: SimpleTagKey if (stk.name == key.name) => - reporter.warning(pos, "Tag '@" + stk.name + "' must be followed by a symbol name") - None - case _ => None - } - val pairs: Seq[(String, Body)] = - for (key <- keys) yield { - val bs = (bodyTags remove key).get - if (bs.length > 1) - reporter.warning(pos, "Only one '@" + key.name + "' tag for symbol " + key.symbol + " is allowed") - (key.symbol, bs.head) - } - Map.empty[String, Body] ++ pairs - } - - val com = new Comment { - val body = parseWiki(docBody, pos) - val authors = allTags(SimpleTagKey("author")) - val see = allTags(SimpleTagKey("see")) - val result = oneTag(SimpleTagKey("return")) - val throws = allSymsOneTag(SimpleTagKey("throws")) - val valueParams = allSymsOneTag(SimpleTagKey("param")) - val typeParams = allSymsOneTag(SimpleTagKey("tparam")) - val version = oneTag(SimpleTagKey("version")) - val since = oneTag(SimpleTagKey("since")) - val todo = allTags(SimpleTagKey("todo")) - val deprecated = oneTag(SimpleTagKey("deprecated")) - val note = allTags(SimpleTagKey("note")) - val example = allTags(SimpleTagKey("example")) - val short = { - val shortText = ShortLineEnd.findFirstMatchIn(docBody) match { - case None => docBody - case Some(m) => docBody.take(m.start) - } - val safeText = CleanHtml.replaceAllIn(shortText, "") // get rid of all layout-busting html tags - parseWiki(safeText, pos) match { - case Body(Paragraph(inl) :: _) => inl - case _ => - if (safeText != "") - reporter.warning(pos, "Comment must start with a sentence") - Text("") + val com = new Comment { + val body = parseWiki(docBody, pos) + val authors = allTags(SimpleTagKey("author")) + val see = allTags(SimpleTagKey("see")) + val result = oneTag(SimpleTagKey("return")) + val throws = allSymsOneTag(SimpleTagKey("throws")) + val valueParams = allSymsOneTag(SimpleTagKey("param")) + val typeParams = allSymsOneTag(SimpleTagKey("tparam")) + val version = oneTag(SimpleTagKey("version")) + val since = oneTag(SimpleTagKey("since")) + val todo = allTags(SimpleTagKey("todo")) + val deprecated = oneTag(SimpleTagKey("deprecated")) + val note = allTags(SimpleTagKey("note")) + val example = allTags(SimpleTagKey("example")) + val short = { + def findShort(blocks: Iterable[Block]): Inline = + if (blocks.isEmpty) Text("") + else blocks.head match { + case Title(text, _) => text + case Paragraph(text) => text + case Code(data) => Monospace(data.lines.next) + case UnorderedList(items) => findShort(items) + case OrderedList(items, _) => findShort(items) + case DefinitionList(items) => findShort(items.values) + case HorizontalRule() => findShort(blocks.tail) + } + findShort(body.blocks) } } - } - for ((key, _) <- bodyTags) - reporter.warning(pos, "Tag '@" + key.name + "' is not recognised") + for ((key, _) <- bodyTags) + reporter.warning(pos, "Tag '@" + key.name + "' is not recognised") - com + com } } + parse0("", Map.empty, None, cleaned, false) + } /** Parses a string containing wiki syntax into a `Comment` object. Note that the string is assumed to be clean: - * * Removed Scaladoc start and end markers. - * * Removed start-of-line star and one whitespace afterwards (if present). - * * Removed all end-of-line whitespace. - * * Only `endOfLine` is used to mark line endings. */ + * - Removed Scaladoc start and end markers. + * - Removed start-of-line star and one whitespace afterwards (if present). + * - Removed all end-of-line whitespace. + * - Only `endOfLine` is used to mark line endings. */ def parseWiki(string: String, pos: Position): Body = new WikiParser(string.toArray, pos).document() @@ -230,6 +246,17 @@ final class CommentFactory(val reporter: Reporter) { parser => * @author Gilles Dubochet */ protected final class WikiParser(val buffer: Array[Char], pos: Position) extends CharReader(buffer) { wiki => + /** listStyle ::= '-' spc | '1.' spc | 'I.' spc | 'i.' spc | 'A.' spc | 'a.' spc + * Characters used to build lists and their contructors */ + protected val listStyles = Map[String, (Seq[Block] => Block)]( // TODO Should this be defined at some list companion? + "- " -> ( UnorderedList(_) ), + "1. " -> ( OrderedList(_,"decimal") ), + "I. " -> ( OrderedList(_,"upperRoman") ), + "i. " -> ( OrderedList(_,"lowerRoman") ), + "A. " -> ( OrderedList(_,"upperAlpha") ), + "a. " -> ( OrderedList(_,"lowerAlpha") ) + ) + def document(): Body = { nextChar() val blocks = new mutable.ListBuffer[Block] @@ -248,47 +275,59 @@ final class CommentFactory(val reporter: Reporter) { parser => title() else if (check("----")) hrule() - else if (check(" - ")) - listBlock(countWhitespace, '-', UnorderedList) - else if (check(" 1 ")) - listBlock(countWhitespace, '1', OrderedList) + else if (checkList) + listBlock else { para() } } - /** - * {{{ - * nListBlock ::= nLine { mListBlock } - * nLine ::= nSpc '*' para '\n' - * }}} - * Where n and m stand for the number of spaces. When m > n, a new list is nested. */ - def listBlock(indentation: Int, marker: Char, constructor: (Seq[Block] => Block)): Block = { - var count = indentation - val start = " " * count + marker + " " - var chk = check(start) - var line = listLine(indentation, marker) - val blocks = mutable.ListBuffer.empty[Block] - while (chk) { - blocks += line - count = countWhitespace - if (count > indentation) { // nesting-in - blocks += listBlock(count, marker, constructor) // TODO is tailrec really needed here? + /** Checks if the current line is formed with more than one space and one the listStyles */ + def checkList = + countWhitespace > 0 && listStyles.keysIterator.indexWhere(checkSkipWhitespace(_)) >= 0 + + /** {{{ + * nListBlock ::= nLine { mListBlock } + * nLine ::= nSpc listStyle para '\n' + * }}} + * Where n and m stand for the number of spaces. When m > n, a new list is nested. */ + def listBlock: Block = { + /** consumes one line of a list block */ + def listLine(indentedListStyle: String): Block = { + // deals with mixed lists in the same nesting level by skipping it + if(!jump(indentedListStyle)) { // TODO show warning when jump is false + nextChar(); + nextChar() } - chk = check(start) - if (chk) { line = listLine(indentation, marker) } + val p = Paragraph(inline(check(Array(endOfLine)))) + blockEnded("end of list line ") + p } - constructor(blocks) - } + def listLevel(leftSide: String, listStyle: String, constructor: (Seq[Block] => Block)): Block = { + val blocks = mutable.ListBuffer.empty[Block] + val length = leftSide.length + val indentedListStyle = leftSide + listStyle + + var index = 1 + var line = listLine(indentedListStyle) + + while (index > -1) { + blocks += line + if (countWhitespace > length) { // nesting-in + blocks += listBlock // TODO is tailrec really needed here? + } + index = listStyles.keysIterator.indexWhere(x => check(leftSide)) + if (index > -1) { line = listLine(indentedListStyle) } + } - def listLine(indentation: Int, marker: Char): Block = { - jump(" " * indentation + marker + " ") - val p = Paragraph(inline(check(Array(endOfLine)))) - blockEnded("end of list line ") - p + constructor(blocks) + } + val indentation = countWhitespace + val indentStr = " " * indentation + val style = listStyles.keysIterator.find( x => check(indentStr + x) ) + val constructor = listStyles(style.get) + listLevel(indentStr, style.get, constructor) } - - /** {{{ code ::= "{{{" { char } '}' "}}" '\n' }}} */ def code(): Block = { jump("{{{") readUntil("}}}") @@ -421,20 +460,39 @@ final class CommentFactory(val reporter: Reporter) { parser => Subscript(i) } + protected val SchemeUri = + new Regex("""([^:]+:.*)""") + + def entityLink(query: String): Inline = findTemplate(query) match { + case Some(tpl) => + EntityLink(tpl) + case None => + Text(query) + } + def link(isInlineEnd: => Boolean, isBlockEnd: => Boolean): Inline = { jump("[[") - readUntil { check("]]") } + readUntil { check("]]") || check(" ") } + val target = getRead() + val title = + if (!check("]]")) Some({ + jump(" ") + inline(check("]]"), isBlockEnd) + }) + else None jump("]]") - val read = getRead() - val (target, title) = { - val index = read.indexOf(' '); - val split = read.splitAt( if (index > -1) index else 0 ) - if (split._1 == "") - (split._2, None) - else - (split._1, Some(split._2.trim)) + (target, title) match { + case (SchemeUri(uri), Some(title)) => + Link(uri, title) + case (SchemeUri(uri), None) => + Link(uri, Text(uri)) + case (qualName, None) => + entityLink(qualName) + case (qualName, Some(text)) => + reportError(pos, "entity link to " + qualName + " cannot have a custom title'" + text + "'") + entityLink(qualName) } - Link(target, title) + } /* UTILITY */ @@ -516,6 +574,10 @@ final class CommentFactory(val reporter: Reporter) { parser => /* JUMPERS */ + /** jumps all the characters in chars + * @return true only if the correct characters have been jumped + * consumes any matching characters + */ final def jump(chars: Array[Char]): Boolean = { var index = 0 while (index < chars.length && char == chars(index) && char != endOfText) { |