Skip to content

Commit

Permalink
Scaladoc: implement parsing of tables
Browse files Browse the repository at this point in the history
  • Loading branch information
kitbellew committed Jun 1, 2020
1 parent 71cab51 commit 355e3f2
Show file tree
Hide file tree
Showing 3 changed files with 322 additions and 2 deletions.
Expand Up @@ -37,6 +37,10 @@ object ScaladocParser {

private val listPrefix = "-" | CharIn("1aiI") ~ "."

private val escape = P("\\")
private val tableSep = P("|")
private val tableSpaceSep = P(hspaces0 ~ tableSep)

private val codePrefix = P("{{{")
private val codeSuffix = P(hspaces0 ~ "}}}")

Expand Down Expand Up @@ -83,8 +87,8 @@ object ScaladocParser {
}

private val textParser: Parser[Text] = {
val anotherBeg = P(hspaces0 ~/ (CharIn("@=") | (codePrefix ~ nl) | listPrefix))
val end = P(End | nl ~/ anotherBeg)
val anotherBeg = P(CharIn("@=") | (codePrefix ~ nl) | listPrefix | tableSep | "+-")
val end = P(End | nl ~/ hspaces0 ~/ anotherBeg)
val part: Parser[TextPart] = P(codeExprParser | linkParser | wordParser)
val sep = P(!end ~ nlHspaces1)
val text = hspaces0 ~ part.rep(1, sep = sep)
Expand Down Expand Up @@ -130,13 +134,73 @@ object ScaladocParser {
P(startOrNl ~ listParser)
}

private val tableParser: Parser[Table] = {
def toRow(x: Iterable[String]): Table.Row = Table.Row(x.toSeq)
def toAlign(x: String): Option[Table.Align] = {
def isEnd(y: Char) = y match {
case ':' => Some(true)
case '-' => Some(false)
case _ => None
}
for {
isLeft <- isEnd(x.head)
isRight <- isEnd(x.last)
if x.slice(1, x.length - 1).forall(_ == '-')
} yield {
if (!isRight) Table.Left
else if (!isLeft) Table.Right
else Table.Center
}
}

val cellChar = escape ~ AnyChar | !(nl | escape | tableSep) ~ AnyChar
val row = (cellChar.rep.! ~ tableSpaceSep).rep(1)
// non-standard but frequently used delimiter line, e.g.: +-----+-------+
val delimLine = hspaces0 ~ CharsWhileIn("+-")
val sep = nl ~ (delimLine ~ nl).rep ~ tableSpaceSep
val table = row.rep(2, sep = sep).map { x =>
val rest = x.tail.map(_.map(_.trim))
val alignRow = rest.head
val align = alignRow.flatMap(toAlign).toSeq
if (align.length != alignRow.length) {
// determine alignment from the header row
val head = x.head
val headBuilder = Seq.newBuilder[String]
val alignBuilder = Seq.newBuilder[Table.Align]
headBuilder.sizeHint(head.length)
alignBuilder.sizeHint(head.length)
head.foreach { cell =>
val padLeft = cell.indexWhere(_ > ' ')
if (padLeft < 0) {
headBuilder += ""
alignBuilder += Table.Left
} else {
val idxRight = cell.lastIndexWhere(_ > ' ') + 1
headBuilder += cell.substring(padLeft, idxRight)
alignBuilder += {
val padRight = cell.length - idxRight
if (padLeft < math.max(padRight - 1, 2)) Table.Left
else if (padRight < math.max(padLeft - 1, 2)) Table.Right
else Table.Center
}
}
}
Table(toRow(headBuilder.result()), alignBuilder.result(), rest.toSeq.map(toRow))
} else
Table(toRow(x.head.map(_.trim)), align, rest.tail.toSeq.map(toRow))
}

P(startOrNl ~ (delimLine ~ nl).? ~ tableSpaceSep ~ table ~ (nl ~ delimLine).?)
}

/** Contains all scaladoc parsers */
private val parser: Parser[collection.Seq[Term]] = {
val allParsers = Seq(
listBlockParser(),
codeBlockParser,
headingParser,
tagParser,
tableParser,
leadTextParser // keep at the end, this is the fallback
)
P(allParsers.reduce(_ | _).rep(1) ~/ End)
Expand Down
Expand Up @@ -48,6 +48,45 @@ object Scaladoc {
/** A heading */
final case class Heading(level: Int, title: String) extends Term

/**
* A table
* [[https://www.scala-lang.org/blog/2018/10/04/scaladoc-tables.html]]
*/
final case class Table(
header: Table.Row,
align: Seq[Table.Align],
row: Seq[Table.Row]
) extends Term

object Table {

final case class Row(col: Seq[String])

sealed abstract class Align {
def leftPad(pad: Int): Int

/** formats a cell of len [[len + 2]] (for padding on either side) */
def syntax(len: Int): String
}

private def hyphens(len: Int): String = "-" * len

final case object Left extends Align {
override def leftPad(pad: Int): Int = 0
override def syntax(len: Int): String = ":" + hyphens(1 + len)
}

final case object Right extends Align {
override def leftPad(pad: Int): Int = pad
override def syntax(len: Int): String = hyphens(1 + len) + ":"
}

final case object Center extends Align {
override def leftPad(pad: Int): Int = pad / 2
override def syntax(len: Int): String = ":" + hyphens(len) + ":"
}
}

/* List blocks */

/** Represents a list item */
Expand Down
Expand Up @@ -623,4 +623,221 @@ class ScaladocParserSuite extends FunSuite {
)
}

test("table escaped pipe") {
val result = parseString(
"""
/**
* text1 text2
* |hdr1|hdr2|hdr3|h\|4|
* |----|:---|---:|:--:|
* |row1|row2|row3|row4|
* text3 text4
*/
""".stripMargin
)

val expectation = Option(
Scaladoc(
Seq(
Paragraph(
Seq(
Text(Seq(Word("text1"), Word("text2"))),
Table(
Table.Row(Seq("hdr1", "hdr2", "hdr3", "h\\|4")),
Seq(Table.Left, Table.Left, Table.Right, Table.Center),
Seq(Table.Row(Seq("row1", "row2", "row3", "row4")))
),
Text(Seq(Word("text3"), Word("text4")))
)
)
)
)
)
assertEquals(result, expectation)
}

test("table missing trailing pipe") {
val result = parseString(
"""
/**
* text1 text2
* |hdr1|hdr2|hdr3|hdr4|
* |----|:---|---:|:--:|
* |row1|row2|row3|row4
* text3 text4
*/
""".stripMargin
)

val expectation = Option(
Scaladoc(
Seq(
Paragraph(
Seq(
Unknown(
"""* text1 text2
* |hdr1|hdr2|hdr3|hdr4|
* |----|:---|---:|:--:|
* |row1|row2|row3|row4
* text3 text4""".stripMargin('*')
)
)
)
)
)
)
assertEquals(result, expectation)
}

test("table different number of cols") {
val result = parseString(
"""
/**
* text1 text2
* |hdr1|hdr2|hdr3|hdr4|
* |----|:---|---:|:--:|---:|:--:|
* |row1|row2|row3|row4|row5|
* text3 text4
*/
""".stripMargin
)

val expectation = Option(
Scaladoc(
Seq(
Paragraph(
Seq(
Text(Seq(Word("text1"), Word("text2"))),
Table(
Table.Row(Seq("hdr1", "hdr2", "hdr3", "hdr4")),
Seq(Table.Left, Table.Left, Table.Right, Table.Center, Table.Right, Table.Center),
Seq(Table.Row(Seq("row1", "row2", "row3", "row4", "row5")))
),
Text(Seq(Word("text3"), Word("text4")))
)
)
)
)
)
assertEquals(result, expectation)
}

test("table missing alignment row") {
val result = parseString(
"""
/**
* text1 text2
* | hdr1 | hdr2 | hdr3 | hdr4 |
* |row1|row2|row3|row4|
* text3 text4
*/
""".stripMargin
)

val expectation = Option(
Scaladoc(
Seq(
Paragraph(
Seq(
Text(Seq(Word("text1"), Word("text2"))),
Table(
Table.Row(Seq("hdr1", "hdr2", "hdr3", "hdr4")),
Seq(Table.Left, Table.Left, Table.Right, Table.Center),
Seq(Table.Row(Seq("row1", "row2", "row3", "row4")))
),
Text(Seq(Word("text3"), Word("text4")))
)
)
)
)
)
assertEquals(result, expectation)
}

test("table missing alignment row with +- delim line") {
val result = parseString(
"""
/**
* text1 text2
* +------+-------+-------+---------+
* | hdr1 | hdr2 | hdr3 | hdr4 |
* +------+-------+-------+---------+
* |row1 |row2 |row3 |row4 |
* +------+-------+-------+---------+
* text3 text4
*/
""".stripMargin
)

val expectation = Option(
Scaladoc(
Seq(
Paragraph(
Seq(
Text(Seq(Word("text1"), Word("text2"))),
Table(
Table.Row(Seq("hdr1", "hdr2", "hdr3", "hdr4")),
Seq(Table.Left, Table.Left, Table.Right, Table.Center),
Seq(Table.Row(Seq("row1", "row2", "row3", "row4")))
),
Text(Seq(Word("text3"), Word("text4")))
)
)
)
)
)
assertEquals(result, expectation)
}

test("table duplicate alignment row") {
val result = parseString(
"""
/**
* text1 text2
* |hdr1|hdr2|hdr3|hdr4|
* |----|:---|---:|:--:|
* |----|:---|---:|:--:|
* |row1|row2|row3|row4|
* text3 text4
*/
""".stripMargin
)

val expectation = Option(
Scaladoc(
Seq(
Paragraph(
Seq(
Text(Seq(Word("text1"), Word("text2"))),
Table(
Table.Row(Seq("hdr1", "hdr2", "hdr3", "hdr4")),
Seq(Table.Left, Table.Left, Table.Right, Table.Center),
Seq(
Table.Row(Seq("----", ":---", "---:", ":--:")),
Table.Row(Seq("row1", "row2", "row3", "row4"))
)
),
Text(Seq(Word("text3"), Word("text4")))
)
)
)
)
)
assertEquals(result, expectation)
}

test("table alignment") {
assertEquals(Table.Left.syntax(0), ":-")
assertEquals(Table.Right.syntax(0), "-:")
assertEquals(Table.Center.syntax(0), "::")

assertEquals(Table.Left.syntax(1), ":--")
assertEquals(Table.Right.syntax(1), "--:")
assertEquals(Table.Center.syntax(1), ":-:")

assertEquals(Table.Left.leftPad(7), 0)
assertEquals(Table.Right.leftPad(7), 7)
assertEquals(Table.Center.leftPad(7), 3)
}

}

0 comments on commit 355e3f2

Please sign in to comment.