summaryrefslogtreecommitdiff
path: root/src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala
diff options
context:
space:
mode:
Diffstat (limited to 'src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala')
-rw-r--r--src/compiler/scala/tools/nsc/doc/model/comment/CommentFactory.scala428
1 files changed, 245 insertions, 183 deletions
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) {