Fix multiline code docparser (#3379)

Changelog:
- fix: docparser handles multiline code sections correctly
- feat: split paragraphs into keyed sections
This commit is contained in:
Dmitry Bushev 2022-04-06 07:39:58 +03:00 committed by GitHub
parent 42ac28d0de
commit 29e3f05f27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 331 additions and 25 deletions

View File

@ -147,6 +147,7 @@
[3364]: https://github.com/enso-org/enso/pull/3364
[3377]: https://github.com/enso-org/enso/pull/3377
[3366]: https://github.com/enso-org/enso/pull/3366
[3379]: https://github.com/enso-org/enso/pull/3379
[3381]: https://github.com/enso-org/enso/pull/3381
#### Enso Compiler

View File

@ -192,6 +192,55 @@ class DocSectionsBuilderTest extends AnyWordSpec with Matchers {
build(comment) shouldEqual expected
}
"build example 6" in {
val comment =
""" UNSTABLE
|
| Creates a new table from a vector of column names and a vector of vectors
| specifying row contents.
|
| Arguments:
| - header: A list of texts specifying the column names
| - rows: A vector of vectors, specifying the contents of each table row. The
| length of each element of `rows` must be equal in length to `header`.
|
| > Example
| Create a table with 3 columns, named `foo`, `bar`, and `baz`, containing
| `[1, 2, 3]`, `[True, False, True]`, and `['a', 'b', 'c']`, respectively.
|
| import Standard.Table
|
| example_from_rows =
| header = [ 'foo' , 'bar' , 'baz' ]
| row_1 = [ 1 , True , 'a' ]
| row_2 = [ 2 , False , 'b' ]
| row_3 = [ 3 , True , 'c' ]
| Table.from_rows header [row_1, row_2, row_3]
|
| Icon: table-from-rows
| Aliases: foo, bar baz, redshift®
|""".stripMargin.linesIterator.mkString("\n")
val expected = Seq(
DocSection.Tag("UNSTABLE", ""),
DocSection.Paragraph(
"Creates a new table from a vector of column names and a vector of vectors specifying row contents. "
),
DocSection.Keyed(
"Arguments",
" <ul><li>header: A list of texts specifying the column names</li><li>rows: A vector of vectors, specifying the contents of each table row. The length of each element of <code>rows</code> must be equal in length to <code>header</code>.</li></ul> "
),
DocSection.Marked(
DocSection.Mark.Example(),
Some("Example"),
" Create a table with 3 columns, named <code>foo</code>, <code>bar</code>, and <code>baz</code>, containing <code>[1, 2, 3]</code>, <code>[True, False, True]</code>, and <code>['a', 'b', 'c']</code>, respectively. <pre><code>import Standard.Table</code><br /><code>example_from_rows =</code><br /><code> header = [ 'foo' , 'bar' , 'baz' ]</code><br /><code> row_1 = [ 1 , True , 'a' ]</code><br /><code> row_2 = [ 2 , False , 'b' ]</code><br /><code> row_3 = [ 3 , True , 'c' ]</code><br /><code> Table.from_rows header [row_1, row_2, row_3]</code><br /></pre>"
),
DocSection.Keyed("Icon", "table-from-rows"),
DocSection.Keyed("Aliases", "foo, bar baz, redshift®")
)
build(comment) shouldEqual expected
}
}
}
object DocSectionsBuilderTest {

View File

@ -4,6 +4,8 @@ import cats.kernel.Monoid
import cats.syntax.compose._
import org.enso.syntax.text.ast.Doc
import scala.collection.mutable
/** Combine the documentation into a list of [[Section]]s. */
final class ParsedSectionsBuilder {
@ -53,21 +55,10 @@ final class ParsedSectionsBuilder {
* @return the list of parsed sections
*/
private def buildSections(sections: List[Doc.Section]): List[Section] =
sections.map {
sections.flatMap {
case Doc.Section.Raw(_, elems) =>
elems match {
case Doc.Elem.Text(text) :: tail =>
val (key, value) = text.span(_ != const.COLON)
if (value.nonEmpty) {
val line = value.drop(1).stripPrefix(const.SPACE)
val body = if (line.isEmpty) tail else Doc.Elem.Text(line) :: tail
Section.Keyed(key, body)
} else {
Section.Paragraph(elems)
}
case _ =>
Section.Paragraph(elems)
}
buildRaw(elems)
case Doc.Section.Marked(_, _, typ, elems) =>
elems match {
case head :: tail =>
@ -77,9 +68,9 @@ final class ParsedSectionsBuilder {
case _ =>
None
}
Section.Marked(buildMark(typ), header, tail)
List(Section.Marked(buildMark(typ), header, tail))
case Nil =>
Section.Marked(buildMark(typ), None, elems)
List(Section.Marked(buildMark(typ), None, elems))
}
}
@ -159,6 +150,50 @@ final class ParsedSectionsBuilder {
case Doc.Section.Raw(_, List(Doc.Elem.Newline)) => false
case _ => true
}
/** Builds sections from the [[Doc.Section.Raw]] raw section.
*
* @param elems the elements of the raw section
* @return the resulting list of sections
*/
private def buildRaw(elems: List[Doc.Elem]): List[Section] =
elems match {
case Doc.Elem.Text(text) :: tail =>
val (key, value) = text.span(_ != const.COLON)
if (value.nonEmpty) {
val line = value.drop(1).stripPrefix(const.SPACE)
val body = if (line.isEmpty) tail else Doc.Elem.Text(line) :: tail
val (keyBody, rest) = splitKeyed(body)
Section.Keyed(key, keyBody) :: buildRaw(rest)
} else {
List(Section.Paragraph(elems))
}
case Nil =>
Nil
case elems =>
List(Section.Paragraph(elems))
}
private def splitKeyed(
elems: List[Doc.Elem]
): (List[Doc.Elem], List[Doc.Elem]) = {
@scala.annotation.tailrec
def go(
elems: List[Doc.Elem],
b: mutable.Builder[Doc.Elem, List[Doc.Elem]]
): (List[Doc.Elem], List[Doc.Elem]) =
elems match {
case Doc.Elem.Newline :: Doc.Elem.Text(text) :: tail
if text.contains(const.COLON) =>
(b.result(), Doc.Elem.Text(text) :: tail)
case elem :: tail =>
go(tail, b += elem)
case Nil =>
(b.result(), Nil)
}
go(elems, List.newBuilder)
}
}
object ParsedSectionsBuilder {

View File

@ -171,6 +171,45 @@ class ParsedSectionsBuilderTest extends AnyWordSpec with Matchers {
parseSections(comment) shouldEqual expected
}
"generate multiple keyed" in {
val comment =
""" Description
|
| Icon: my-icon
| icon
| Aliases: foo, bar baz, redshift®
| and other
|
| Paragraph
|""".stripMargin.linesIterator.mkString("\n")
val expected = List(
Section.Paragraph(
List(Doc.Elem.Text("Description"), Doc.Elem.Newline)
),
Section.Keyed(
"Icon",
List(
Doc.Elem.Text("my-icon"),
Doc.Elem.Newline,
Doc.Elem.Text("icon")
)
),
Section.Keyed(
"Aliases",
List(
Doc.Elem.Text("foo, bar baz, redshift"),
Doc.Elem.Text("®"),
Doc.Elem.Newline,
Doc.Elem.Text("and other"),
Doc.Elem.Newline
)
),
Section.Paragraph(List(Doc.Elem.Text("Paragraph")))
)
parseSections(comment) shouldEqual expected
}
"generate marked example" in {
val comment =
""" Description

View File

@ -946,18 +946,16 @@ case class DocParserDef() extends Parser[Doc] {
current.indent > currentIndent &&
current.isInstanceOf[Section.Raw]
) {
var stackOfCodeSections: List[Section] = List[Section]()
val codeIndent = current.indent
var stackOfCodeSections: List[Section] = List(current)
while (
section.stack.nonEmpty && current.indent > currentIndent && current
.isInstanceOf[Section.Raw] && section.stack.head
.isInstanceOf[Section.Raw]
section.stack.nonEmpty &&
section.stack.head.indent >= codeIndent &&
section.stack.head.isInstanceOf[Section.Raw]
) {
stackOfCodeSections = stackOfCodeSections :+ current
if (section.stack.head.isInstanceOf[Section.Raw]) {
current = section.pop().get
}
current = section.pop().get
stackOfCodeSections :+= current
}
stackOfCodeSections = stackOfCodeSections :+ current
val codeLines = stackOfCodeSections.flatMap {
_.repr
.build()

View File

@ -378,6 +378,61 @@ class DocParserTests extends AnyFlatSpec with Matchers {
Section.Raw(2, "And this is not")
)
)
""" Synopsis
|
| !Important
| This is a code
|
| Other: And this is a section""".stripMargin.replaceAll(
System.lineSeparator(),
"\n"
) ?= Doc(
Synopsis(
Section.Raw(1, "Synopsis", Doc.Elem.Newline)
),
Body(
Section.Marked(
1,
0,
Section.Marked.Important,
Section.Header("Important"),
Doc.Elem.Newline,
CodeBlock(CodeBlock.Line(4, "This is a code")),
Doc.Elem.Newline
),
Section.Raw(1, "Other: And this is a section")
)
)
""" Synopsis
|
| ! Important
|
| This is a multiline code
|
| ? Info""".stripMargin.replaceAll(
System.lineSeparator(),
"\n"
) ?== Doc(
Synopsis(
Section.Raw(1, "Synopsis", Doc.Elem.Newline)
),
Body(
Section.Marked(
1,
1,
Section.Marked.Important,
Section.Header("Important"),
Doc.Elem.Newline,
CodeBlock(CodeBlock.Line(4, "This is a multiline code"))
),
Section.Marked(
1,
1,
Section.Marked.Info,
Section.Header("Info")
)
)
)
"""Synopsis
|
|! Important
@ -403,6 +458,36 @@ class DocParserTests extends AnyFlatSpec with Matchers {
)
)
)
""" Synopsis
|
| ! Important
|
| This is a multiline code
|
| Other: And this *is* a section""".stripMargin.replaceAll(
System.lineSeparator(),
"\n"
) ?== Doc(
Synopsis(
Section.Raw(1, "Synopsis", Doc.Elem.Newline)
),
Body(
Section.Marked(
1,
1,
Section.Marked.Important,
Section.Header("Important"),
Doc.Elem.Newline,
CodeBlock(CodeBlock.Line(4, "This is a multiline code"))
),
Section.Raw(
1,
"Other: And this ",
Formatter(Formatter.Bold, "is"),
" a section"
)
)
)
"?Info" ?= Doc(
Synopsis(Section.Marked(Section.Marked.Info, Section.Header("Info")))
)
@ -1502,4 +1587,103 @@ class DocParserTests extends AnyFlatSpec with Matchers {
)
)
""" UNSTABLE
|
| Creates a new table from a vector of column names and a vector of vectors
| specifying row contents.
|
| Arguments:
| - header: A list of texts specifying the column names
| - rows: A vector of vectors, specifying the contents of each table row. The
| length of each element of `rows` must be equal in length to `header`.
|
| > Example
| Create a table with 3 columns, named `foo`, `bar`, and `baz`, containing
| `[1, 2, 3]`, `[True, False, True]`, and `['a', 'b', 'c']`, respectively.
|
| import Standard.Table
|
| example_from_rows =
| header = [ 'foo' , 'bar' , 'baz' ]
| row_1 = [ 1 , True , 'a' ]
| row_2 = [ 2 , False , 'b' ]
| row_3 = [ 3 , True , 'c' ]
| Table.from_rows header [row_1, row_2, row_3]
|
| Icon: table-from-rows
| Aliases: foo, bar baz, redshift®
|""".stripMargin.replaceAll(System.lineSeparator(), "\n") ?== Doc(
Tags(Tag(1, Tags.Tag.Type.Unstable, None)),
Synopsis(Section.Raw(1, Newline)),
Body(
Section.Raw(
1,
"Creates a new table from a vector of column names and a vector of vectors",
Newline,
"specifying row contents.",
Newline
),
Section.Raw(
1,
"Arguments:",
Newline,
List(
1,
List.Unordered,
ListItem("header: A list of texts specifying the column names"),
ListItem(
"rows: A vector of vectors, specifying the contents of each table row. The",
Newline,
" ",
"length of each element of ",
CodeBlock.Inline("rows"),
" must be equal in length to ",
CodeBlock.Inline("header"),
"."
)
),
Newline
),
Section.Marked(
1,
1,
Section.Marked.Example,
Section.Header("Example"),
Newline,
"Create a table with 3 columns, named ",
CodeBlock.Inline("foo"),
", ",
CodeBlock.Inline("bar"),
", and ",
CodeBlock.Inline("baz"),
", containing",
Newline,
CodeBlock.Inline("[1, 2, 3]"),
Text(", "),
CodeBlock.Inline("[True, False, True]"),
", and ",
CodeBlock.Inline("['a', 'b', 'c']"),
", respectively.",
Newline,
CodeBlock(
CodeBlock.Line(7, "import Standard.Table"),
CodeBlock.Line(7, "example_from_rows ="),
CodeBlock.Line(11, "header = [ 'foo' , 'bar' , 'baz' ]"),
CodeBlock.Line(11, "row_1 = [ 1 , True , 'a' ]"),
CodeBlock.Line(11, "row_2 = [ 2 , False , 'b' ]"),
CodeBlock.Line(11, "row_3 = [ 3 , True , 'c' ]"),
CodeBlock.Line(11, "Table.from_rows header [row_1, row_2, row_3]")
)
),
Section.Raw(
1,
"Icon: table-from-rows",
Newline,
"Aliases: foo, bar baz, redshift",
"®",
Newline
)
)
)
}