mirror of
https://github.com/enso-org/enso.git
synced 2024-12-27 23:36:59 +03:00
Fix multiline code docparser (#3379)
Changelog: - fix: docparser handles multiline code sections correctly - feat: split paragraphs into keyed sections
This commit is contained in:
parent
42ac28d0de
commit
29e3f05f27
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user