Repository: scalameta/scalameta
Pull request #2537 (2022-01-01 12:19:50)

ScaladocParser: allow other list-embedded elements
For now, that does not include markdown as that requires extra indent. Minor modification based on #2489, @dos65 did the bulk of the word.


Patch:
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScaladocParser.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScaladocParser.scala
index 0fddf17e0..cc87ce302 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScaladocParser.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScaladocParser.scala
@@ -71,10 +71,18 @@ object ScaladocParser {
    * https://spec.commonmark.org/0.29/#fenced-code-blocks
    */
 
-  private def mdCodeBlockIndent[_: P] = hspace.rep(max = 3)
+  /* while spec mentions that a fence marker can be indented between 0 and 3 spaces,
+   * examples under "list item" show that it's *relative* to the containing element;
+   * also, lists are included in "container blocks" which have that property.
+   * however, for non-indented case (mdOffset = 0), since we don't capture the actual
+   * offset, let's allow 0 (next to asterisk) and 1 (one space from asterisk). */
+  private def getMdOffsetMax(mdOffset: Int) = math.max(1, mdOffset) + 3
+
   private def mdCodeBlockFence[_: P] = "`".rep(3) | "~".rep(3)
 
-  private def mdCodeBlockParser[_: P]: P[MdCodeBlock] = P {
+  private def mdCodeBlockParser[_: P](mdOffset: Int = 0): P[MdCodeBlock] = P {
+    def mdCodeBlockIndent = hspace.rep(min = mdOffset, max = getMdOffsetMax(mdOffset))
+
     (mdCodeBlockIndent.! ~ mdCodeBlockFence.!).flatMap { case (indent, fence) =>
       def info = hspaces0 ~ labelParser.!.rep(0, sep = hspaces0) ~ nl
       info.flatMap { infoSeq =>
@@ -107,29 +115,34 @@ object ScaladocParser {
     pattern.map { case (x, y) => new Link(x, y) }
   }
 
-  private def nextPartParser[_: P]: P[Unit] = P {
-    def mdCodeBlockPrefix = mdCodeBlockIndent ~ mdCodeBlockFence
+  private def nextPartParser[_: P](mdOffset: Int = 0): P[Unit] = P {
+    // used to terminate previous part, hence indent can be less
+    def mdCodeBlockPrefix = hspace.rep(max = getMdOffsetMax(mdOffset)) ~ mdCodeBlockFence
     def anotherBeg = P(CharIn("@=") | (codePrefix ~ nl) | listPrefix | tableSep | "+-" | nl)
     nl | mdCodeBlockPrefix | hspaces0 ~/ anotherBeg
   }
 
-  private def textParser[_: P]: P[Text] = P {
-    def end = P(nl ~/ nextPartParser)
+  private def textParser[_: P](mdOffset: Int = 0): P[Text] = P {
+    def end = P(nl ~/ nextPartParser(mdOffset))
     def part: P[TextPart] = P(codeExprParser | linkParser | wordParser)
     def sep = P(!end ~ nlHspaces0)
     hspaces0 ~ part.rep(1, sep = sep).map(x => Text(x))
   }
 
   // this is to be used at the start of a line
-  private def leadTextParser[_: P] = !nextPartParser ~ textParser
+  private def leadTextParser[_: P](mdOffset: Int = 0) =
+    !nextPartParser(mdOffset) ~ textParser(mdOffset)
 
   private def tagParser[_: P]: P[Tag] = P {
-    def label = P((nl ~ !nextPartParser).? ~ hspaces0 ~ wordParser)
-    def desc = P(textParser | nl ~ leadTextParser)
+    def label = P((nl ~ !nextPartParser()).? ~ hspaces0 ~ wordParser)
+    def desc = P {
+      (textParser().? ~ embeddedTermsParser())
+        .map { case (x, terms) => x.fold(terms)(_ +: terms) }
+    }
     hspaces0 ~ ("@" ~ labelParser).!.flatMap { tag =>
       val tagType = TagType.getTag(tag)
       def labelOpt = if (tagType.hasLabel) label.map(x => Some(x)) else Pass(None)
-      def descOpt = if (tagType.optDesc) desc.? else Pass(None)
+      def descOpt = if (tagType.optDesc) desc else Pass(Nil)
       (labelOpt ~ descOpt).map { case (label, desc) => Tag(tagType, label, desc) }
     }
   }
@@ -137,16 +150,19 @@ object ScaladocParser {
   private def listBlockParser[_: P](indent: Int = 0): P[ListBlock] = P {
     /* https://docs.scala-lang.org/overviews/scaladoc/for-library-authors.html#other-formatting-notes
      * For list blocks, "you must have extra space in front", hence min `indent + 1`. */
-    def listMarker = hspacesMinWithLen(indent + 1) ~ listPrefix.! ~ hspaces1
-    listMarker.flatMap { case (listIndent, prefix) =>
+    /* however, for markdown lists, the outer list does not need to have an extra space
+     * but the nested list item must be offset by "marker + space after it" + [0, 3] */
+    def listMarker = hspacesMinWithLen(indent + 1) ~ listPrefix.! ~ hspacesMinWithLen(1)
+    listMarker.flatMap { case (listIndent, prefix, ws) =>
+      val mdOffset = listIndent + prefix.length + ws
       def sep = nl ~ hspace.rep(exactly = listIndent) ~ prefix ~ hspaces1
-      listItemParser(listIndent).rep(1, sep = sep).map(x => ListBlock(prefix, x))
+      listItemParser(listIndent, mdOffset).rep(1, sep = sep).map(x => ListBlock(prefix, x))
     }
   }
 
-  private def listItemParser[_: P](indent: Int) = P {
-    (textParser ~ (nl ~ listBlockParser(indent)).?)
-      .map { case (desc, list) => ListItem(desc, list) }
+  private def listItemParser[_: P](indent: Int, mdOffset: Int) = P {
+    (textParser(mdOffset) ~ embeddedTermsParser(indent, mdOffset))
+      .map { case (x, terms) => ListItem(x, terms) }
   }
 
   private def tableParser[_: P]: P[Table] = P {
@@ -217,13 +233,23 @@ object ScaladocParser {
       tagParser
     def completeParser = // will consume full line
       listBlockParser() |
-        mdCodeBlockParser |
+        mdCodeBlockParser() |
         codeBlockParser |
         headingParser |
         tableParser |
-        textParser // keep at the end, this is the fallback
+        textParser() // keep at the end, this is the fallback
     (nl | Start) ~ (leadingParser | (completeParser ~ nlOrEndPeek)) |
-      textParser // could be following an element leaving trailing text, e.g. tagParser
+      textParser() // could be following an element leaving trailing text, e.g. tagParser
+  }
+
+  private def embeddedTermsParser[_: P](indent: Int = 0, mdOffset: Int = 0): P[Seq[Term]] = P {
+    def completeParser = // will consume full line
+      listBlockParser(indent) |
+        mdCodeBlockParser(mdOffset) |
+        codeBlockParser |
+        tableParser |
+        leadTextParser(mdOffset) // keep at the end, this is the fallback
+    (nl ~ completeParser ~ nlOrEndPeek).rep
   }
 
   /** Contains all scaladoc parsers */
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/internal/Scaladoc.scala b/scalameta/trees/shared/src/main/scala/scala/meta/internal/Scaladoc.scala
index 1d8d9239a..a9e1f1a17 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/internal/Scaladoc.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/internal/Scaladoc.scala
@@ -97,7 +97,7 @@ object Scaladoc {
   /* List blocks */
 
   /** Represents a list item */
-  final case class ListItem(text: Text, nested: Option[ListBlock] = None)
+  final case class ListItem(text: Text, terms: Seq[Term] = Nil)
 
   /** Represents a list block */
   final case class ListBlock(prefix: String, items: Seq[ListItem]) extends Term
@@ -116,8 +116,7 @@ object Scaladoc {
    * @param label set iff `tag.hasLabel`
    * @param desc set iff `tag.hasDesc`
    */
-  final case class Tag(tag: TagType, label: Option[Word] = None, desc: Option[Text] = None)
-      extends Term
+  final case class Tag(tag: TagType, label: Option[Word] = None, desc: Seq[Term] = Nil) extends Term
 
   object TagType {
 


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/ScaladocParserSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/ScaladocParserSuite.scala
index d22665a35..547742b6f 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/ScaladocParserSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/ScaladocParserSuite.scala
@@ -384,6 +384,22 @@ class ScaladocParserSuite extends FunSuite {
     assertEquals(result, expectation)
   }
 
+  test("markdown code blocks - no leading spaces") {
+    val result = parseString(
+      s"""
+          /**```scala
+            *println(42)
+            *```
+            */
+       """.stripMargin
+    )
+
+    val expectation = Option(
+      Scaladoc(Seq(Paragraph(Seq(MdCodeBlock(Seq("scala"), Seq("println(42)"), "```")))))
+    )
+    assertEquals(result, expectation)
+  }
+
   test("headings") {
     val level1HeadingBody = "Level 1"
     val level2HeadingBody = "Level 2"
@@ -495,11 +511,11 @@ class ScaladocParserSuite extends FunSuite {
                 Seq(
                   ListItem(
                     Text(Seq(Word(list11))),
-                    Some(ListBlock("1.", Seq(ListItem(Text(Seq(Word(list21)))))))
+                    Seq(ListBlock("1.", Seq(ListItem(Text(Seq(Word(list21)))))))
                   ),
                   ListItem(
                     Text(Seq(Word(list12))),
-                    Some(
+                    Seq(
                       ListBlock(
                         "a.",
                         Seq(ListItem(Text(Seq(Word(list21)))), ListItem(Text(Seq(Word(list22)))))
@@ -549,13 +565,13 @@ class ScaladocParserSuite extends FunSuite {
                 Seq(
                   ListItem(
                     Text(Seq(Word(list11))),
-                    Some(
+                    Seq(
                       ListBlock(
                         "i.",
                         Seq(
                           ListItem(
                             Text(Seq(Word(list21))),
-                            Some(ListBlock("I.", Seq(ListItem(Text(Seq(Word(list31)))))))
+                            Seq(ListBlock("I.", Seq(ListItem(Text(Seq(Word(list31)))))))
                           )
                         )
                       )
@@ -568,13 +584,13 @@ class ScaladocParserSuite extends FunSuite {
                 Seq(
                   ListItem(
                     Text(Seq(Word(list12))),
-                    Some(
+                    Seq(
                       ListBlock(
                         "I.",
                         Seq(
                           ListItem(
                             Text(Seq(Word(list22))),
-                            Some(ListBlock("I.", Seq(ListItem(Text(Seq(Word(list32)))))))
+                            Seq(ListBlock("I.", Seq(ListItem(Text(Seq(Word(list32)))))))
                           )
                         )
                       )
@@ -618,7 +634,7 @@ class ScaladocParserSuite extends FunSuite {
                 Seq(
                   ListItem(
                     Text(Seq(Word(list11), Word(list11))),
-                    Some(ListBlock("-", Seq(ListItem(Text(Seq(Word(list21)))))))
+                    Seq(ListBlock("-", Seq(ListItem(Text(Seq(Word(list21)))))))
                   ),
                   ListItem(Text(Seq(Word(list12))))
                 )
@@ -701,6 +717,463 @@ class ScaladocParserSuite extends FunSuite {
     assertEquals(result, expected)
   }
 
+  test("list 7, indented markdown code") {
+    val list11 = "List11"
+    val list12 = "List12"
+    val list13 = "List13"
+    val list21 = "List21"
+
+    val result =
+      parseString(
+        s"""
+        /**
+          * Some text:
+          * - $list11
+          * - $list12
+          * continue text
+          *    ```scala
+          *    println(42)
+          *    ```
+          * and some text
+          *   - $list21
+          * - $list13
+          */
+         """
+      )
+    val expected = Option(
+      Scaladoc(
+        Seq(
+          Paragraph(
+            Seq(
+              Text(Seq(Word("Some"), Word("text:"))),
+              ListBlock(
+                "-",
+                Seq(
+                  ListItem(Text(Seq(Word(list11)))),
+                  ListItem(
+                    Text(Seq(Word(list12), Word("continue"), Word("text"))),
+                    Seq(
+                      MdCodeBlock(Seq("scala"), Seq("println(42)"), "```"),
+                      Text(Seq(Word("and"), Word("some"), Word("text"))),
+                      ListBlock("-", Seq(ListItem(Text(Seq(Word(list21))))))
+                    )
+                  ),
+                  ListItem(Text(Seq(Word(list13))))
+                )
+              )
+            )
+          )
+        )
+      )
+    )
+    assertEquals(result, expected)
+  }
+
+  test("list 7, over-indented markdown-like code") {
+    val list11 = "List11"
+    val list12 = "List12"
+    val list13 = "List13"
+    val list21 = "List21"
+
+    val result =
+      parseString(
+        s"""
+        /**
+          * Some text:
+          * - $list11
+          * - $list12
+          * continue text
+          *       ```scala
+          *    println(42)
+          *       ```
+          * and some text
+          *   - $list21
+          * - $list13
+          */
+         """
+      )
+    val expected = Option(
+      Scaladoc(
+        Seq(
+          Paragraph(
+            Seq(
+              Text(Seq(Word("Some"), Word("text:"))),
+              ListBlock(
+                "-",
+                Seq(
+                  ListItem(Text(Seq(Word(list11)))),
+                  ListItem(
+                    Text(
+                      Seq(Word(list12), Word("continue"), Word("text")) ++
+                        Seq(Word("```scala"), Word("println(42)"), Word("```")) ++
+                        Seq(Word("and"), Word("some"), Word("text"))
+                    ),
+                    Seq(ListBlock("-", Seq(ListItem(Text(Seq(Word(list21)))))))
+                  ),
+                  ListItem(Text(Seq(Word(list13))))
+                )
+              )
+            )
+          )
+        )
+      )
+    )
+    assertEquals(result, expected)
+  }
+
+  test("list 7, non-indented markdown code") {
+    val list11 = "List11"
+    val list12 = "List12"
+    val list13 = "List13"
+    val list21 = "List21"
+
+    val result =
+      parseString(
+        s"""
+        /**
+          * Some text:
+          * - $list11
+          * - $list12
+          * continue text
+          *```scala
+          *println(42)
+          *```
+          * and some text
+          *   - $list21
+          * - $list13
+          */
+         """
+      )
+    val expected = Option(
+      Scaladoc(
+        Seq(
+          Paragraph(
+            Seq(
+              Text(Seq(Word("Some"), Word("text:"))),
+              ListBlock(
+                "-",
+                Seq(
+                  ListItem(Text(Seq(Word(list11)))),
+                  ListItem(Text(Seq(Word(list12), Word("continue"), Word("text"))))
+                )
+              ),
+              MdCodeBlock(Seq("scala"), Seq("println(42)"), "```"),
+              Text(Seq(Word("and"), Word("some"), Word("text"))),
+              ListBlock("-", Seq(ListItem(Text(Seq(Word(list21)))))),
+              ListBlock("-", Seq(ListItem(Text(Seq(Word(list13))))))
+            )
+          )
+        )
+      )
+    )
+    assertEquals(result, expected)
+  }
+
+  test("list 7, indented code") {
+    val list11 = "List11"
+    val list12 = "List12"
+    val list13 = "List13"
+    val list21 = "List21"
+
+    val result =
+      parseString(
+        s"""
+        /**
+          * Some text:
+          * - $list11
+          * - $list12
+          * continue text
+          *    {{{
+          *     println(42)
+          *    }}}
+          * and some text
+          *   - $list21
+          * - $list13
+          */
+         """
+      )
+    val expected = Option(
+      Scaladoc(
+        Seq(
+          Paragraph(
+            Seq(
+              Text(Seq(Word("Some"), Word("text:"))),
+              ListBlock(
+                "-",
+                Seq(
+                  ListItem(Text(Seq(Word(list11)))),
+                  ListItem(
+                    Text(Seq(Word(list12), Word("continue"), Word("text"))),
+                    Seq(
+                      CodeBlock(Seq("     println(42)")),
+                      Text(Seq(Word("and"), Word("some"), Word("text"))),
+                      ListBlock("-", Seq(ListItem(Text(Seq(Word(list21))))))
+                    )
+                  ),
+                  ListItem(Text(Seq(Word(list13))))
+                )
+              )
+            )
+          )
+        )
+      )
+    )
+    assertEquals(result, expected)
+  }
+
+  test("list 7, over-indented code") {
+    val list11 = "List11"
+    val list12 = "List12"
+    val list13 = "List13"
+    val list21 = "List21"
+
+    val result =
+      parseString(
+        s"""
+        /**
+          * Some text:
+          * - $list11
+          * - $list12
+          * continue text
+          *      {{{
+          *     println(42) }}}
+          * and some text
+          *   - $list21
+          * - $list13
+          *
+          * next para
+          */
+         """
+      )
+    val expected = Option(
+      Scaladoc(
+        Seq(
+          Paragraph(
+            Seq(
+              Text(Seq(Word("Some"), Word("text:"))),
+              ListBlock(
+                "-",
+                Seq(
+                  ListItem(Text(Seq(Word(list11)))),
+                  ListItem(
+                    Text(Seq(Word(list12), Word("continue"), Word("text"))),
+                    Seq(
+                      CodeBlock(Seq("     println(42)")),
+                      Text(Seq(Word("and"), Word("some"), Word("text"))),
+                      ListBlock("-", Seq(ListItem(Text(Seq(Word(list21))))))
+                    )
+                  ),
+                  ListItem(Text(Seq(Word(list13))))
+                )
+              )
+            )
+          ),
+          Paragraph(Seq(Text(Seq(Word("next"), Word("para")))))
+        )
+      )
+    )
+    assertEquals(result, expected)
+  }
+
+  test("list 7, non-indented code") {
+    val list11 = "List11"
+    val list12 = "List12"
+    val list13 = "List13"
+    val list21 = "List21"
+
+    val result =
+      parseString(
+        s"""
+        /**
+          * Some text:
+          * - $list11
+          * - $list12
+          * continue text
+          *{{{
+          * println(42)
+          *}}}
+          * and some text
+          *   - $list21
+          * - $list13
+          */
+         """
+      )
+    val expected = Option(
+      Scaladoc(
+        Seq(
+          Paragraph(
+            Seq(
+              Text(Seq(Word("Some"), Word("text:"))),
+              ListBlock(
+                "-",
+                Seq(
+                  ListItem(Text(Seq(Word(list11)))),
+                  ListItem(
+                    Text(Seq(Word(list12), Word("continue"), Word("text"))),
+                    Seq(
+                      CodeBlock(Seq(" println(42)")),
+                      Text(Seq(Word("and"), Word("some"), Word("text"))),
+                      ListBlock("-", Seq(ListItem(Text(Seq(Word(list21))))))
+                    )
+                  ),
+                  ListItem(Text(Seq(Word(list13))))
+                )
+              )
+            )
+          )
+        )
+      )
+    )
+    assertEquals(result, expected)
+  }
+
+  test("list 8, indented table") {
+    val list11 = "List11"
+    val list12 = "List12"
+
+    val result =
+      parseString(
+        s"""
+        /**
+          * Some text:
+          * - $list11
+          * - $list12
+          *   |one|two|
+          *   |---|---|
+          *   |1  | 2 |
+          * and some text
+          */
+         """
+      )
+    val expected = Option(
+      Scaladoc(
+        Seq(
+          Paragraph(
+            Seq(
+              Text(Seq(Word("Some"), Word("text:"))),
+              ListBlock(
+                "-",
+                Seq(
+                  ListItem(Text(Seq(Word(list11)))),
+                  ListItem(
+                    Text(Seq(Word(list12))),
+                    Seq(
+                      Table(
+                        Table.Row(Seq("one", "two")),
+                        Seq(Table.Left, Table.Left),
+                        Seq(Table.Row(Seq("1", "2")))
+                      ),
+                      Text(Seq(Word("and"), Word("some"), Word("text")))
+                    )
+                  )
+                )
+              )
+            )
+          )
+        )
+      )
+    )
+    assertEquals(result, expected)
+  }
+
+  test("list 8, over-indented table") {
+    val list11 = "List11"
+    val list12 = "List12"
+
+    val result =
+      parseString(
+        s"""
+        /**
+          * Some text:
+          * - $list11
+          * - $list12
+          *       |one|two|
+          *       |---|---|
+          *       |1  | 2 |
+          * and some text
+          */
+         """
+      )
+    val expected = Option(
+      Scaladoc(
+        Seq(
+          Paragraph(
+            Seq(
+              Text(Seq(Word("Some"), Word("text:"))),
+              ListBlock(
+                "-",
+                Seq(
+                  ListItem(Text(Seq(Word(list11)))),
+                  ListItem(
+                    Text(Seq(Word(list12))),
+                    Seq(
+                      Table(
+                        Table.Row(Seq("one", "two")),
+                        Seq(Table.Left, Table.Left),
+                        Seq(Table.Row(Seq("1", "2")))
+                      ),
+                      Text(Seq(Word("and"), Word("some"), Word("text")))
+                    )
+                  )
+                )
+              )
+            )
+          )
+        )
+      )
+    )
+    assertEquals(result, expected)
+  }
+
+  test("list 8, non-indented table") {
+    val list11 = "List11"
+    val list12 = "List12"
+
+    val result =
+      parseString(
+        s"""
+        /**
+          * Some text:
+          * - $list11
+          * - $list12
+          * |one|two|
+          * |---|---|
+          * |1  | 2 |
+          * and some text
+          */
+         """
+      )
+    val expected = Option(
+      Scaladoc(
+        Seq(
+          Paragraph(
+            Seq(
+              Text(Seq(Word("Some"), Word("text:"))),
+              ListBlock(
+                "-",
+                Seq(
+                  ListItem(Text(Seq(Word(list11)))),
+                  ListItem(
+                    Text(Seq(Word(list12))),
+                    Seq(
+                      Table(
+                        Table.Row(Seq("one", "two")),
+                        Seq(Table.Left, Table.Left),
+                        Seq(Table.Row(Seq("1", "2")))
+                      ),
+                      Text(Seq(Word("and"), Word("some"), Word("text")))
+                    )
+                  )
+                )
+              )
+            )
+          )
+        )
+      )
+    )
+    assertEquals(result, expected)
+  }
+
   test("label parsing/merging") {
     val testStringToMerge = "Test DocText"
     implicit val sb = new StringBuilder
@@ -746,8 +1219,12 @@ class ScaladocParserSuite extends FunSuite {
 
     // Inherit doc does not merge
     parsedScaladocTerms.foreach {
-      case t: Tag if t.tag.optDesc && t.desc.isDefined =>
-        assertEquals(t.desc.get.parts.takeRight(words.length), words)
+      case t: Tag if t.tag.optDesc && t.desc.nonEmpty =>
+        val actual = t.desc.map {
+          case x: Text => x.parts.takeRight(words.length)
+          case x => x
+        }
+        assertEquals(actual, Seq(words))
       case _ =>
     }
   }
@@ -769,7 +1246,7 @@ class ScaladocParserSuite extends FunSuite {
               Tag(
                 TagType.Param,
                 Some(Word("foo")),
-                Some(Text(Seq(Word("-"), Word("bar"), Word("baz"))))
+                Seq(Text(Seq(Word("-"), Word("bar"), Word("baz"))))
               ),
               Tag(TagType.Return)
             )
@@ -794,8 +1271,11 @@ class ScaladocParserSuite extends FunSuite {
         Seq(
           Paragraph(
             Seq(
-              Tag(TagType.Param, Some(Word("foo"))),
-              ListBlock("-", Seq(ListItem(Text(Seq(Word("bar"), Word("baz"))))))
+              Tag(
+                TagType.Param,
+                Some(Word("foo")),
+                Seq(ListBlock("-", Seq(ListItem(Text(Seq(Word("bar"), Word("baz")))))))
+              )
             )
           )
         )
@@ -818,8 +1298,10 @@ class ScaladocParserSuite extends FunSuite {
         Seq(
           Paragraph(
             Seq(
-              Tag(TagType.UnknownTag("@foo")),
-              ListBlock("-", Seq(ListItem(Text(Seq(Word("bar"), Word("baz"))))))
+              Tag(
+                TagType.UnknownTag("@foo"),
+                desc = Seq(ListBlock("-", Seq(ListItem(Text(Seq(Word("bar"), Word("baz")))))))
+              )
             )
           )
         )
@@ -847,12 +1329,12 @@ class ScaladocParserSuite extends FunSuite {
                 Tag(
                   TagType.Throws,
                   Some(Word("e1")),
-                  Some(Text(Seq(Word("foo"), Word("bar"))))
+                  Seq(Text(Seq(Word("foo"), Word("bar"))))
                 ),
                 Tag(
                   TagType.Throws,
                   Some(Word("e2")),
-                  Some(Text(Seq(Word("baz"), Word("qux"), Word("bar-baz"))))
+                  Seq(Text(Seq(Word("baz"), Word("qux"), Word("bar-baz"))))
                 )
               )
             )
@@ -883,7 +1365,7 @@ class ScaladocParserSuite extends FunSuite {
                 Tag(
                   TagType.Since,
                   Some(Word("1.0")),
-                  Some(Text(Seq(Word("baz"), Word("qux"), Word("bar-baz"))))
+                  Seq(Text(Seq(Word("baz"), Word("qux"), Word("bar-baz"))))
                 )
               )
             )
@@ -911,7 +1393,7 @@ class ScaladocParserSuite extends FunSuite {
                 Tag(
                   TagType.Define,
                   Some(Word("what")),
-                  Some(Text(Seq(Word("for"), Word("what"), Word("reason"), Word("bar-baz"))))
+                  Seq(Text(Seq(Word("for"), Word("what"), Word("reason"), Word("bar-baz"))))
                 )
               )
             )
@@ -934,7 +1416,7 @@ class ScaladocParserSuite extends FunSuite {
       Option(
         Scaladoc(
           Seq(
-            Paragraph(Seq(Tag(TagType.Param, Some(Word("foo")), Some(Text(Seq(Word("bar-baz")))))))
+            Paragraph(Seq(Tag(TagType.Param, Some(Word("foo")), Seq(Text(Seq(Word("bar-baz")))))))
           )
         )
       )
@@ -985,7 +1467,7 @@ class ScaladocParserSuite extends FunSuite {
               Seq(
                 Tag(
                   TagType.UnknownTag("@newtag"),
-                  desc = Some(Text(Seq(Word("newtag"), Word("text"))))
+                  desc = Seq(Text(Seq(Word("newtag"), Word("text"))))
                 )
               )
             )
@@ -1024,11 +1506,15 @@ class ScaladocParserSuite extends FunSuite {
           Paragraph(
             Seq(
               Text(Seq(Word("blah"))),
-              Tag(TagType.Example),
-              CodeBlock(complexCodeBlock),
-              Text(Seq(Word("bar"), Word("baz"))),
-              CodeBlock(complexCodeBlock),
-              Text(Seq(Word("baz"), Word("qux")))
+              Tag(
+                TagType.Example,
+                desc = Seq(
+                  CodeBlock(complexCodeBlock),
+                  Text(Seq(Word("bar"), Word("baz"))),
+                  CodeBlock(complexCodeBlock),
+                  Text(Seq(Word("baz"), Word("qux")))
+                )
+              )
             )
           )
         )
@@ -1067,11 +1553,15 @@ class ScaladocParserSuite extends FunSuite {
           Paragraph(
             Seq(
               Text(Seq(Word("blah"))),
-              Tag(TagType.Example),
-              MdCodeBlock(Seq("info1", "info2"), complexCodeBlock, "```"),
-              Text(Seq(Word("bar"), Word("baz"))),
-              MdCodeBlock(Seq("info3", "info4"), complexCodeBlock, "```"),
-              Text(Seq(Word("baz"), Word("qux")))
+              Tag(
+                TagType.Example,
+                desc = Seq(
+                  MdCodeBlock(Seq("info1", "info2"), complexCodeBlock, "```"),
+                  Text(Seq(Word("bar"), Word("baz"))),
+                  MdCodeBlock(Seq("info3", "info4"), complexCodeBlock, "```"),
+                  Text(Seq(Word("baz"), Word("qux")))
+                )
+              )
             )
           )
         )
@@ -1362,6 +1852,35 @@ class ScaladocParserSuite extends FunSuite {
     assertEquals(result, expectation)
   }
 
+  test("table - no leading spaces") {
+    val result = parseString(
+      """
+          /**
+            *|one|two|
+            *|---|---|
+            *|1  |  2|
+            */
+       """.stripMargin
+    )
+
+    val expectation = Option(
+      Scaladoc(
+        Seq(
+          Paragraph(
+            Seq(
+              Table(
+                Table.Row(Seq("one", "two")),
+                Seq(Table.Left, Table.Left),
+                Seq(Table.Row(Seq("1", "2")))
+              )
+            )
+          )
+        )
+      )
+    )
+    assertEquals(result, expectation)
+  }
+
   test("table alignment") {
     assertEquals(Table.Left.syntax(0), ":-")
     assertEquals(Table.Right.syntax(0), "-:")