From 9d402bd59928c188efbda2260af77d006d050b30 Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Mon, 21 Mar 2022 10:14:25 +0300 Subject: [PATCH] Split documentation comment into sections (#3347) --- build.sbt | 4 +- .../docs/generator/DocParserWrapper.scala | 3 +- .../docs/sections/ParsedSectionsBuilder.scala | 151 +++++ .../org/enso/docs/sections/Section.scala | 93 +++ .../org/enso/docs/sections/package.scala | 9 + .../sections/ParsedSectionsBuilderTest.scala | 480 +++++++++++++ .../scala/org/enso/syntax/text/ast/Doc.scala | 194 +++++- .../enso/syntax/text/spec/DocParserDef.scala | 347 ++++++---- .../org/enso/syntax/text/DocParserTests.scala | 635 ++++++++++++++++-- 9 files changed, 1688 insertions(+), 228 deletions(-) create mode 100644 lib/scala/docs-generator/src/main/scala/org/enso/docs/sections/ParsedSectionsBuilder.scala create mode 100644 lib/scala/docs-generator/src/main/scala/org/enso/docs/sections/Section.scala create mode 100644 lib/scala/docs-generator/src/main/scala/org/enso/docs/sections/package.scala create mode 100644 lib/scala/docs-generator/src/test/scala/org/enso/docs/sections/ParsedSectionsBuilderTest.scala diff --git a/build.sbt b/build.sbt index e3ac4ca25a..217832da9d 100644 --- a/build.sbt +++ b/build.sbt @@ -610,8 +610,10 @@ lazy val `docs-generator` = (project in file("lib/scala/docs-generator")) inConfig(Benchmark)(Defaults.testSettings), Benchmark / unmanagedSourceDirectories += baseDirectory.value.getParentFile / "bench" / "scala", - libraryDependencies += + libraryDependencies ++= Seq( "com.storm-enroute" %% "scalameter" % scalameterVersion % "bench", + "org.scalatest" %% "scalatest" % scalatestVersion % Test + ), testFrameworks := List( new TestFramework("org.scalatest.tools.Framework"), new TestFramework("org.scalameter.ScalaMeterFramework") diff --git a/lib/scala/docs-generator/src/main/scala/org/enso/docs/generator/DocParserWrapper.scala b/lib/scala/docs-generator/src/main/scala/org/enso/docs/generator/DocParserWrapper.scala index a08ddf0e47..0b4de92985 100644 --- a/lib/scala/docs-generator/src/main/scala/org/enso/docs/generator/DocParserWrapper.scala +++ b/lib/scala/docs-generator/src/main/scala/org/enso/docs/generator/DocParserWrapper.scala @@ -1,9 +1,10 @@ package org.enso.docs.generator -import java.io.File import org.enso.syntax.text.{DocParser, Parser} import org.enso.syntax.text.docparser._ +import java.io.File + /** Defines useful wrappers for Doc Parser. */ object DocParserWrapper { diff --git a/lib/scala/docs-generator/src/main/scala/org/enso/docs/sections/ParsedSectionsBuilder.scala b/lib/scala/docs-generator/src/main/scala/org/enso/docs/sections/ParsedSectionsBuilder.scala new file mode 100644 index 0000000000..38d4aa7b95 --- /dev/null +++ b/lib/scala/docs-generator/src/main/scala/org/enso/docs/sections/ParsedSectionsBuilder.scala @@ -0,0 +1,151 @@ +package org.enso.docs.sections + +import cats.kernel.Monoid +import cats.syntax.compose._ +import org.enso.syntax.text.ast.Doc + +/** Combine the documentation into a list of [[ParsedSection]]s. */ +final class ParsedSectionsBuilder { + + import ParsedSectionsBuilder._ + + /** Build the parsed sections from the provided documentation comment. + * + * @param doc the parsed documentation comment. + * @return the list of parsed sections. + */ + def build(doc: Doc): List[ParsedSection] = { + val tagSections = doc.tags.map(buildTags) + val synopsisSections = doc.synopsis.map(buildSynopsis) + val bodySections = doc.body.map(buildBody) + Monoid.combineAll(tagSections ++ synopsisSections ++ bodySections) + } + + /** Process the tags section of the documentation comment. + * + * @param tags the tags section + * @return the list of parsed sections + */ + private def buildTags(tags: Doc.Tags): List[ParsedSection] = + tags.elems.toList.map { tag => + Section.Tag(tag.name, tag.details.map(_.trim).map(Doc.Elem.Text)) + } + + /** Process the synopsis section of the documentation comment. + * + * @param synopsis the synopsis section + * @return the list of parsed sections + */ + private def buildSynopsis(synopsis: Doc.Synopsis): List[ParsedSection] = + (joinSections _ >>> buildSections)(synopsis.elems.toList) + + /** Process the body section of the documentation comment. + * + * @param body the body section + * @return the list of parsed sections + */ + private def buildBody(body: Doc.Body): List[ParsedSection] = + (joinSections _ >>> buildSections)(body.elems.toList) + + /** Process the list of [[Doc.Section]] documentation sections. + * + * @param sections the list of parsed documentation sections + * @return the list of parsed sections + */ + private def buildSections( + sections: List[Doc.Section] + ): List[ParsedSection] = + sections.map { + case Doc.Section.Raw(_, elems) => + elems match { + case Doc.Elem.Text(text) :: t => + val (key, value) = text.span(_ != const.COLON) + if (value.nonEmpty) { + val line = value.drop(1).stripPrefix(const.SPACE) + val body = if (line.isEmpty) t else Doc.Elem.Text(line) :: t + Section.Keyed(key, body) + } else { + Section.Paragraph(elems) + } + case _ => + Section.Paragraph(elems) + } + case Doc.Section.Marked(_, _, typ, elems) => + elems match { + case head :: tail => + val header = head match { + case header: Doc.Section.Header => + Some(header.repr.build()) + case _ => + None + } + Section.Marked(buildMark(typ), header, tail) + case Nil => + Section.Marked(buildMark(typ), None, elems) + } + } + + /** Create the [[Section.Mark]] from the [[Doc.Section.Marked.Type]] + * section type. + * + * @param typ the type of documentation section + * @return the corresponding section mark + */ + private def buildMark(typ: Doc.Section.Marked.Type): Section.Mark = + typ match { + case Doc.Section.Marked.Important => Section.Mark.Important + case Doc.Section.Marked.Info => Section.Mark.Info + case Doc.Section.Marked.Example => Section.Mark.Example + } + + /** Preprocess the list of documentation sections and join the paragraphs of + * the same offset with the marked section. + * + * ==Example== + * In the parsed [[Doc.Section]], the "Some paragraph" is a separate section, + * while having the same indentation. This pass joins them into a single + * section. + * + * {{{ + * ? Info + * Some info. + * + * Some paragraph. + * }}} + * + * @param sections the list of documentation sections + * @return preprocessed list of documentation sections with the + * paragraphs joined into the corresponding marked sections. + */ + private def joinSections(sections: List[Doc.Section]): List[Doc.Section] = { + val init: Option[Doc.Section.Marked] = None + val stack: List[Doc.Section] = Nil + + val (result, acc) = sections.foldLeft((stack, init)) { + case ((stack, acc), section) => + (section, acc) match { + case (marked: Doc.Section.Marked, _) => + (acc.toList ::: stack, Some(marked)) + case (raw: Doc.Section.Raw, None) => + (raw :: stack, acc) + case (raw: Doc.Section.Raw, Some(marked)) => + if (raw.indent == marked.indent) { + val newElems = marked.elems ::: Doc.Elem.Newline :: raw.elems + (stack, Some(marked.copy(elems = newElems))) + } else { + (raw :: marked :: stack, None) + } + } + } + + (acc.toList ::: result).reverse + } +} + +object ParsedSectionsBuilder { + + object const { + final val COLON = ':' + final val SPACE = " " + } +} diff --git a/lib/scala/docs-generator/src/main/scala/org/enso/docs/sections/Section.scala b/lib/scala/docs-generator/src/main/scala/org/enso/docs/sections/Section.scala new file mode 100644 index 0000000000..5ee8eede04 --- /dev/null +++ b/lib/scala/docs-generator/src/main/scala/org/enso/docs/sections/Section.scala @@ -0,0 +1,93 @@ +package org.enso.docs.sections + +/** The base trait for the section. */ +sealed trait Section[A] +object Section { + + /** The documentation tag. + * + * {{{ + * name text + * }}} + * + * ==Example== + * + * {{{ + * UNSTABLE + * DEPRECATED + * ALIAS Length + * }}} + * + * @param name the tag name + * @param text the tag text + */ + case class Tag[A](name: String, text: Option[A]) extends Section[A] + + /** The paragraph of the text. + * + * ==Example== + * + * {{{ + * Arbitrary text in the documentation comment. + * + * This is another paragraph. + * }}} + * + * @param body the elements that make up this paragraph + */ + case class Paragraph[A](body: List[A]) extends Section[A] + + /** The section that starts with the key followed by the colon and the body. + * + * {{{ + * key: body + * }}} + * + * ==Example== + * + * {{{ + * Arguments: + * - one: the first + * - two: the second + * }}} + * + * {{{ + * Icon: table-from-rows + * }}} + * + * @param key the section key + * @param body the elements the make up the body of the section + */ + case class Keyed[A](key: String, body: List[A]) extends Section[A] + + /** The section that starts with the mark followed by the header and the body. + * + * {{{ + * mark header + * body + * }}} + * + * ==Example== + * + * {{{ + * > Example + * This is how it's done. + * foo = bar baz + * }}} + * + * {{{ + * ! Notice + * This is important. + * }}} + */ + case class Marked[A](mark: Mark, header: Option[String], body: List[A]) + extends Section[A] + + /** The base trait for the section marks. */ + sealed trait Mark + object Mark { + case object Important extends Mark + case object Info extends Mark + case object Example extends Mark + } +} diff --git a/lib/scala/docs-generator/src/main/scala/org/enso/docs/sections/package.scala b/lib/scala/docs-generator/src/main/scala/org/enso/docs/sections/package.scala new file mode 100644 index 0000000000..5eaa1404c0 --- /dev/null +++ b/lib/scala/docs-generator/src/main/scala/org/enso/docs/sections/package.scala @@ -0,0 +1,9 @@ +package org.enso.docs + +import org.enso.syntax.text.ast.Doc + +package object sections { + + type ParsedSection = Section[Doc.Elem] + type RenderedSection = Section[String] +} diff --git a/lib/scala/docs-generator/src/test/scala/org/enso/docs/sections/ParsedSectionsBuilderTest.scala b/lib/scala/docs-generator/src/test/scala/org/enso/docs/sections/ParsedSectionsBuilderTest.scala new file mode 100644 index 0000000000..7922163685 --- /dev/null +++ b/lib/scala/docs-generator/src/test/scala/org/enso/docs/sections/ParsedSectionsBuilderTest.scala @@ -0,0 +1,480 @@ +package org.enso.docs.sections + +import org.enso.syntax.text.DocParser +import org.enso.syntax.text.ast.Doc +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class ParsedSectionsBuilderTest extends AnyWordSpec with Matchers { + + import ParsedSectionsBuilderTest._ + + "DocSectionsGenerator" should { + + "generate single tag" in { + val comment = + """ UNSTABLE + |""".stripMargin.linesIterator.mkString("\n") + val expected = List( + Section.Tag("UNSTABLE", None) + ) + + parseSections(comment) shouldEqual expected + } + + "generate multiple tags" in { + val comment = + """ UNSTABLE + | DEPRECATED + |""".stripMargin.linesIterator.mkString("\n") + val expected = List( + Section.Tag("UNSTABLE", None), + Section.Tag("DEPRECATED", None) + ) + + parseSections(comment) shouldEqual expected + } + + "generate tag with description" in { + val comment = + """ ALIAS Check Matches + |""".stripMargin.linesIterator.mkString("\n") + val expected = List( + Section.Tag("ALIAS", Some(Doc.Elem.Text("Check Matches"))) + ) + + parseSections(comment) shouldEqual expected + } + + "generate description single line" in { + val comment = + """ hello world + |""".stripMargin.linesIterator.mkString("\n") + val expected = List( + Section.Paragraph(List(Doc.Elem.Text("hello world"))) + ) + + parseSections(comment) shouldEqual expected + } + + "generate description multiline" in { + val comment = + """ hello world + | second line + |""".stripMargin.linesIterator.mkString("\n") + val expected = List( + Section.Paragraph( + List( + Doc.Elem.Text("hello world"), + Doc.Elem.Newline, + Doc.Elem.Text("second line") + ) + ) + ) + + parseSections(comment) shouldEqual expected + } + + "generate description multiple paragraphs" in { + val comment = + """ Hello world + | second line + | + | Second paragraph + | multiline + |""".stripMargin.linesIterator.mkString("\n") + val expected = List( + Section.Paragraph( + List( + Doc.Elem.Text("Hello world"), + Doc.Elem.Newline, + Doc.Elem.Text("second line"), + Doc.Elem.Newline + ) + ), + Section.Paragraph( + List( + Doc.Elem.Text("Second paragraph"), + Doc.Elem.Newline, + Doc.Elem.Text("multiline") + ) + ) + ) + + parseSections(comment) shouldEqual expected + } + + "generate keyed arguments" in { + val comment = + """ Description + | + | Arguments: + | - one: The first + | - two: The second + |""".stripMargin.linesIterator.mkString("\n") + val expected = List( + Section.Paragraph( + List(Doc.Elem.Text("Description"), Doc.Elem.Newline) + ), + Section.Keyed( + "Arguments", + List( + Doc.Elem.Newline, + Doc.Elem.List( + 1, + Doc.Elem.List.Unordered, + Doc.Elem.Text("one: The first"), + Doc.Elem.Text("two: The second") + ) + ) + ) + ) + + parseSections(comment) shouldEqual expected + } + + "generate keyed icon" in { + val comment = + """ Description + | + | Icon: my-icon + |""".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")) + ) + ) + + parseSections(comment) shouldEqual expected + } + + "generate keyed aliases" in { + val comment = + """ Description + | + | Aliases: foo, bar baz, redshift® + |""".stripMargin.linesIterator.mkString("\n") + val expected = List( + Section.Paragraph( + List(Doc.Elem.Text("Description"), Doc.Elem.Newline) + ), + Section.Keyed( + "Aliases", + List(Doc.Elem.Text("foo, bar baz, redshift"), Doc.Elem.Text("®")) + ) + ) + + parseSections(comment) shouldEqual expected + } + + "generate marked example" in { + val comment = + """ Description + | + | > Example + | Simple program + | main = 42 + |""".stripMargin.linesIterator.mkString("\n") + val expected = List( + Section.Paragraph( + List(Doc.Elem.Text("Description"), Doc.Elem.Newline) + ), + Section.Marked( + Section.Mark.Example, + Some("Example"), + List( + Doc.Elem.Newline, + Doc.Elem.Text("Simple program"), + Doc.Elem.Newline, + Doc.Elem.CodeBlock(Doc.Elem.CodeBlock.Line(7, "main = 42")) + ) + ) + ) + + parseSections(comment) shouldEqual expected + } + + "generate marked multiple examples" in { + val comment = + """ Description + | + | > Example + | Simple program + | Multiline + | main = 42 + | + | > Example + | Another example + | + | import Foo.Bar + | + | main = + | 42 + |""".stripMargin.linesIterator.mkString("\n") + val expected = List( + Section.Paragraph( + List(Doc.Elem.Text("Description"), Doc.Elem.Newline) + ), + Section.Marked( + Section.Mark.Example, + Some("Example"), + List( + Doc.Elem.Newline, + Doc.Elem.Text("Simple program"), + Doc.Elem.Newline, + Doc.Elem.Text("Multiline"), + Doc.Elem.Newline, + Doc.Elem.CodeBlock(Doc.Elem.CodeBlock.Line(7, "main = 42")), + Doc.Elem.Newline + ) + ), + Section.Marked( + Section.Mark.Example, + Some("Example"), + List( + Doc.Elem.Newline, + Doc.Elem.Text("Another example"), + Doc.Elem.Newline, + Doc.Elem.CodeBlock( + Doc.Elem.CodeBlock.Line(7, "import Foo.Bar"), + Doc.Elem.CodeBlock.Line(7, "main ="), + Doc.Elem.CodeBlock.Line(11, "42") + ) + ) + ) + ) + + parseSections(comment) shouldEqual expected + } + + "generate marked important" in { + val comment = + """ Description + | + | ! This is important + | Beware of nulls. + |""".stripMargin.linesIterator.mkString("\n") + val expected = List( + Section.Paragraph( + List(Doc.Elem.Text("Description"), Doc.Elem.Newline) + ), + Section.Marked( + Section.Mark.Important, + Some("This is important"), + List( + Doc.Elem.Newline, + Doc.Elem.Text("Beware of nulls.") + ) + ) + ) + + parseSections(comment) shouldEqual expected + } + + "generate marked info" in { + val comment = + """ Description + | + | ? Out of curiosity + | FYI. + |""".stripMargin.linesIterator.mkString("\n") + val expected = List( + Section.Paragraph( + List(Doc.Elem.Text("Description"), Doc.Elem.Newline) + ), + Section.Marked( + Section.Mark.Info, + Some("Out of curiosity"), + List( + Doc.Elem.Newline, + Doc.Elem.Text("FYI.") + ) + ) + ) + + parseSections(comment) shouldEqual expected + } + + "generate marked info multiple sections" in { + val comment = + """ Description + | + | ? Out of curiosity + | FYI. + | + | Another section. + | + | And another. + |""".stripMargin.linesIterator.mkString("\n") + val expected = List( + Section.Paragraph( + List(Doc.Elem.Text("Description"), Doc.Elem.Newline) + ), + Section.Marked( + Section.Mark.Info, + Some("Out of curiosity"), + List( + Doc.Elem.Newline, + Doc.Elem.Text("FYI."), + Doc.Elem.Newline, + Doc.Elem.Newline, + Doc.Elem.Text("Another section."), + Doc.Elem.Newline, + Doc.Elem.Newline, + Doc.Elem.Text("And another.") + ) + ) + ) + + parseSections(comment) shouldEqual expected + } + + "generate marked info and paragraph sections" in { + val comment = + """ Description + | + | ? Out of curiosity + | FYI. + | + | Another section. + | + | This is paragraph. + |""".stripMargin.linesIterator.mkString("\n") + val expected = List( + Section.Paragraph( + List(Doc.Elem.Text("Description"), Doc.Elem.Newline) + ), + Section.Marked( + Section.Mark.Info, + Some("Out of curiosity"), + List( + Doc.Elem.Newline, + Doc.Elem.Text("FYI."), + Doc.Elem.Newline, + Doc.Elem.Newline, + Doc.Elem.Text("Another section."), + Doc.Elem.Newline + ) + ), + Section.Paragraph( + List(Doc.Elem.Text("This is paragraph.")) + ) + ) + + parseSections(comment) shouldEqual expected + } + + "generate marked info and important sections" in { + val comment = + """ Description + | + | ? Out of curiosity + | FYI. + | + | ! Warning + | Pretty important. + |""".stripMargin.linesIterator.mkString("\n") + val expected = List( + Section.Paragraph( + List(Doc.Elem.Text("Description"), Doc.Elem.Newline) + ), + Section.Marked( + Section.Mark.Info, + Some("Out of curiosity"), + List( + Doc.Elem.Newline, + Doc.Elem.Text("FYI."), + Doc.Elem.Newline + ) + ), + Section.Marked( + Section.Mark.Important, + Some("Warning"), + List( + Doc.Elem.Newline, + Doc.Elem.Text("Pretty important.") + ) + ) + ) + + parseSections(comment) shouldEqual expected + } + + "generate multiple sections" in { + val comment = + """ DEPRECATED + | + | Some paragraph + | Second line + | + | Arguments: + | - one: The first + | - two: The second + | + | ! This is important + | Boo. + | + | ? Out of curiosity + | FYI. + |""".stripMargin.linesIterator.mkString("\n") + val expected = List( + Section.Tag("DEPRECATED", None), + Section.Paragraph(List(Doc.Elem.Newline)), + Section.Paragraph( + List( + Doc.Elem.Text("Some paragraph"), + Doc.Elem.Newline, + Doc.Elem.Text("Second line"), + Doc.Elem.Newline + ) + ), + Section.Keyed( + "Arguments", + List( + Doc.Elem.Newline, + Doc.Elem.List( + 1, + Doc.Elem.List.Unordered, + Doc.Elem.Text("one: The first"), + Doc.Elem.Text("two: The second") + ), + Doc.Elem.Newline + ) + ), + Section.Marked( + Section.Mark.Important, + Some("This is important"), + List( + Doc.Elem.Newline, + Doc.Elem.Text("Boo."), + Doc.Elem.Newline + ) + ), + Section.Marked( + Section.Mark.Info, + Some("Out of curiosity"), + List( + Doc.Elem.Newline, + Doc.Elem.Text("FYI.") + ) + ) + ) + + parseSections(comment) shouldEqual expected + } + } + +} +object ParsedSectionsBuilderTest { + + val parsedSectionsBuilder = new ParsedSectionsBuilder + + def parseSections(comment: String): List[ParsedSection] = { + val doc = DocParser.runMatched(comment) + parsedSectionsBuilder.build(doc) + } +} diff --git a/lib/scala/syntax/definition/src/main/scala/org/enso/syntax/text/ast/Doc.scala b/lib/scala/syntax/definition/src/main/scala/org/enso/syntax/text/ast/Doc.scala index b2eb4ff003..52b993b64c 100644 --- a/lib/scala/syntax/definition/src/main/scala/org/enso/syntax/text/ast/Doc.scala +++ b/lib/scala/syntax/definition/src/main/scala/org/enso/syntax/text/ast/Doc.scala @@ -284,57 +284,179 @@ object Doc { //// List - Ordered & Unordered, Invalid Indent //////////////////////////// //////////////////////////////////////////////////////////////////////////// + /** An element of the list. + * + * The list can contain following elements: + * - [[ListItem]] a plain list element + * - [[List]] a sublist + * - [[MisalignedItem]] a list item that is not aligned correctly with the + * previous list item + */ + sealed trait ListElem extends Elem { + + /** Append elements to this list item. + * + * @param xs elements to append + */ + def append(xs: scala.List[Elem]): ListElem + } + + /** A list item that can hold a complex element structure. + * + * @param elems the elements that make up this list item + */ + final case class ListItem(elems: scala.List[Elem]) extends ListElem { + + override val repr: Repr.Builder = R + elems + + override def html: HTML = elems.map(_.html) + + /** @inheritdoc */ + override def append(xs: scala.List[Elem]): ListItem = + copy(elems = elems :++ xs) + } + object ListItem { + + /** Create a list item from the provided elemenets + * + * @param elems the elements that make up the list item + * @return the list item + */ + def apply(elems: Elem*): ListItem = + ListItem(elems.toList) + } + + /** The list item that is not aligned correctly with the previous list item. + * + * @param indent the indentation of this list item + * @param typ the list type + * @param elems the elements that make up this list item + */ + final case class MisalignedItem( + indent: Int, + typ: List.Type, + elems: scala.List[Elem] + ) extends ListElem { + + override val repr: Repr.Builder = + R + indent + typ.marker + List.ElemIndent + elems + + override def html: HTML = + elems.map(_.html) + + /** @inheritdoc */ + override def append(xs: scala.List[Elem]): MisalignedItem = + copy(elems = elems :++ xs) + } + object MisalignedItem { + + /** Create a misaligned item from the provided elements. + * + * @param indent the indentation of this list item + * @param typ the list type + * @param elems the elements that make up the list item + * @return the new misaligned item + */ + def apply(indent: Int, typ: List.Type, elems: Elem*): MisalignedItem = + new MisalignedItem(indent, typ, elems.toList) + } + /** List - block used to hold ordered and unordered lists - * - * @param indent - specifies indentation of list - * @param typ - type of list - * @param elems - elements which make up list * * Indent.Invalid - holds list element with invalid indent + * + * @param indent specifies indentation of list + * @param typ the list type + * @param elems the elements that make up this list */ - final case class List(indent: Int, typ: List.Type, elems: List1[Elem]) - extends Elem { - val repr: Repr.Builder = R + indent + typ.marker + elems.head + elems.tail - .map { - case elem @ (_: Elem.Invalid) => R + Newline + elem - case elem @ (_: List) => R + Newline + elem - case elem => - R + Newline + indent + typ.marker + elem + final case class List(indent: Int, typ: List.Type, elems: List1[ListElem]) + extends ListElem { + + val repr: Repr.Builder = { + val listElems = elems.reverse + R + indent + typ.marker + List.ElemIndent + listElems.head + + listElems.tail.map { + case elem: List => + R + Newline + elem + case elem: ListItem => + R + Newline + indent + typ.marker + List.ElemIndent + elem + case elem: MisalignedItem => + R + Newline + elem } + } val html: HTML = { - val elemsHTML = elems.toList.map { - case elem @ (_: List) => elem.html - case elem => Seq(HTML.li(elem.html)) + val elemsHTML = elems.reverse.toList.map { + case elem: List => elem.html + case elem: ListElem => Seq(HTML.li(elem.html)) } Seq(typ.HTMLMarker(elemsHTML)) } + + /** @inheritdoc */ + override def append(xs: scala.List[Elem]): List = { + val newElems = List1(elems.head.append(xs), elems.tail) + this.copy(elems = newElems) + } + + /** Add a new list item. + * + * @param item the list item to add + * @return the new list with this item added + */ + def addItem(item: ListElem): List = { + val newElems = elems.prepend(item) + this.copy(elems = newElems) + } + + /** Add an empty list item. + * + * @return the new list with an empty list item added + */ + def addItem(): List = + this.copy(elems = elems.prepend(ListItem(Nil))) } object List { - def apply(indent: Int, listType: Type, elem: Elem): List = - List(indent, listType, List1(elem)) - def apply(indent: Int, listType: Type, elems: Elem*): List = - List(indent, listType, List1(elems.head, elems.tail.toList)) + val ElemIndent: Int = 1 + + /** Create an empty list. + * + * @param indent the list indentation + * @param typ the list type + * @return the new list + */ + def empty(indent: Int, typ: Type): List = + new List(indent, typ, List1(ListItem(Nil))) + + /** Create a new list. + * + * @param indent the list indentation + * @param typ the list type + * @param elem the first elements of this list + * @param elems the rest of the list elements + * @return the new list + */ + def apply(indent: Int, typ: Type, elem: Elem, elems: Elem*): List = { + val listItems = (elem :: elems.toList).reverse.map { + case list: List => list + case elem: ListItem => elem + case elem: MisalignedItem => elem + case elem => ListItem(elem) + } + new List( + indent, + typ, + List1.fromListOption(listItems).get + ) + } + + /** The list type. */ abstract class Type(val marker: Char, val HTMLMarker: HTMLTag) final case object Unordered extends Type('-', HTML.ul) final case object Ordered extends Type('*', HTML.ol) - object Indent { - final case class Invalid(indent: Int, typ: Type, elem: Elem) - extends Elem.Invalid { - val repr: Repr.Builder = R + indent + typ.marker + elem - val html: HTML = { - val className = this.productPrefix - val htmlCls = HTML.`class` := className + getObjectName - Seq(HTML.div(htmlCls)(elem.html)) - } - } - } - - def getObjectName: String = - getClass.toString.split('$').last } } @@ -352,7 +474,7 @@ object Doc { */ sealed trait Section extends Symbol { def indent: Int - var elems: List[Elem] + def elems: List[Elem] def reprOfNormalText(elem: Elem, prevElem: Elem): Repr.Builder = { prevElem match { @@ -387,7 +509,7 @@ object Doc { indentBeforeMarker: Int, indentAfterMarker: Int, typ: Marked.Type, - var elems: List[Elem] + elems: List[Elem] ) extends Section { val marker: String = typ.marker.toString val firstIndentRepr: Repr.Builder = @@ -443,7 +565,7 @@ object Doc { case object Example extends Type('>') } - final case class Raw(indent: Int, var elems: List[Elem]) extends Section { + final case class Raw(indent: Int, elems: List[Elem]) extends Section { val dummyElem = Elem.Text("") val newLn: Elem = Elem.Newline val elemsRepr: List[Repr.Builder] = elems.zip(dummyElem :: elems).map { diff --git a/lib/scala/syntax/definition/src/main/scala/org/enso/syntax/text/spec/DocParserDef.scala b/lib/scala/syntax/definition/src/main/scala/org/enso/syntax/text/spec/DocParserDef.scala index 6807bfd5ef..031977f4cd 100644 --- a/lib/scala/syntax/definition/src/main/scala/org/enso/syntax/text/spec/DocParserDef.scala +++ b/lib/scala/syntax/definition/src/main/scala/org/enso/syntax/text/spec/DocParserDef.scala @@ -56,7 +56,8 @@ case class DocParserDef() extends Parser[Doc] { val lowerChar: Pattern = range('a', 'z') val upperChar: Pattern = range('A', 'Z') val digit: Pattern = range('0', '9') - val whitespace: Pattern = ' '.many1 + val space: Pattern = ' ' + val whitespace: Pattern = space.many1 val newline: Char = '\n' val char: Pattern = lowerChar | upperChar @@ -205,18 +206,6 @@ case class DocParserDef() extends Parser[Doc] { } case Some(_) | None => result.push() } - if (result.stack.tail.head.isInstanceOf[Elem.List]) { - val code = result.current.get.asInstanceOf[Elem.CodeBlock] - result.pop() - result.pop() - val list = result.current.get.asInstanceOf[Elem.List] - val last = list.elems.toList.last.repr + newline + code.elems.repr - val newElems = - list.elems.reverse.tail.reverse :+ Elem.stringToText(last.build()) - val nElems = List1(newElems).get - val newList = Elem.List(list.indent, list.typ, nElems) - result.current = Some(newList) - } result.push() } @@ -226,7 +215,7 @@ case class DocParserDef() extends Parser[Doc] { } val notNewLine: Pattern = not(newline).many1 - val CODE: State = state.define("Code") + lazy val CODE: State = state.define("Code") ROOT || code.inlinePattern || code.onPushingInline(currentMatch) CODE || newline || { state.end(); state.begin(NEWLINE) } @@ -453,24 +442,31 @@ case class DocParserDef() extends Parser[Doc] { var stack: List[Int] = Nil def current: Int = stack match { - case Nil => 0 - case ::(head, _) => head + case h :: _ => h + case Nil => 0 } def onIndent(): Unit = logger.trace { val diff = currentMatch.length - current - if (diff < 0 && list.inListFlag) { - list.appendInnerToOuter() - stack = stack.tail - } else if ( - currentMatch.length > section.currentIndentRaw && result.stack.nonEmpty - ) { - tryToFindCodeInStack() - stack +:= currentMatch.length - state.begin(CODE) + if (list.isInList) { + if (diff > list.minIndent) { + tryToFindCodeInStack() + stack +:= currentMatch.length + state.begin(CODE) + } else { + text.push(currentMatch) + } } else { - section.currentIndentRaw = currentMatch.length + if ( + currentMatch.length > section.currentIndentRaw && result.stack.nonEmpty + ) { + tryToFindCodeInStack() + stack +:= currentMatch.length + state.begin(CODE) + } else { + section.currentIndentRaw = currentMatch.length + } } } @@ -487,51 +483,36 @@ case class DocParserDef() extends Parser[Doc] { result.push() } - def onIndentForListCreation( - indent: Int, - typ: Elem.List.Type, - content: String - ): Unit = + def onIndentForListCreation(indent: Int, typ: Elem.List.Type): Unit = logger.trace { - val diff = indent - current - if (diff > 0) { - /* NOTE - * Used to push new line before pushing first list - */ - if (!list.inListFlag) onPushingNewLine() + val diff = indent - list.current + + if (!list.isInList) { + onPushingNewLine() + stack +:= indent - list.inListFlag = true - list.addNew(indent, typ, content) - } else if (diff == 0 && list.inListFlag) { - list.addContent(content) - } else if (diff < 0 && list.inListFlag) { - if (stack.tail.head != indent) { - onInvalidIndent(indent, typ, content) - } else { - list.appendInnerToOuter() - list.addContent(content) - stack = stack.tail - } + list.startNewList(indent, typ) + } else if (diff == 0) { + list.endListItem() + list.startListItem() + } else if (diff > 0) { + stack +:= indent + list.endListItem() + list.startNewList(indent, typ) } else { - onInvalidIndent(indent, typ, content) + if (indent > list.prev) { + list.endListItem() + list.startMisalignedListItem(indent, typ) + } else { + do { + list.endListItem() + list.endSublist() + } while (indent < list.current) + list.startListItem() + } } } - def onInvalidIndent( - indent: Int, - typ: Elem.List.Type, - content: String - ): Unit = { - if (!list.inListFlag && typ == Elem.List.Ordered) { - onPushingNewLine() - formatter.onPushing(Elem.Formatter.Bold) - result.current = Some(content) - result.push() - } else { - list.addContent(Elem.List.Indent.Invalid(indent, typ, content)) - } - } - def onPushingNewLine(): Unit = logger.trace { result.current = Some(Elem.Newline) @@ -540,9 +521,8 @@ case class DocParserDef() extends Parser[Doc] { def onEmptyLine(): Unit = logger.trace { - if (list.inListFlag) { - list.appendInnerToOuter() - list.inListFlag = false + if (list.isInList) { + list.endListItem(endList = true) } onPushingNewLine() section.onEOS() @@ -569,7 +549,7 @@ case class DocParserDef() extends Parser[Doc] { val EOFPattern: Pattern = indentPattern >> eof } - val NEWLINE: State = state.define("Newline") + lazy val NEWLINE: State = state.define("Newline") ROOT || newline || state.begin(NEWLINE) NEWLINE || indent.EOFPattern || indent.onEOFPattern() @@ -584,79 +564,180 @@ case class DocParserDef() extends Parser[Doc] { * there are 2 possible types of lists - ordered and unordered. */ final object list { - var inListFlag: Boolean = false - def addNew(indent: Int, listType: Elem.List.Type, content: Elem): Unit = + /** The minimum list indentation consisting of a list symbol and a space + * character. + */ + val minIndent: Int = 2 + + var stack: List[Int] = Nil + def current: Int = + stack match { + case h :: _ => h + case Nil => 0 + } + def prev: Int = + stack match { + case _ :: p :: _ => p + case _ => 0 + } + + def isInList: Boolean = stack.nonEmpty + + def startNewList(indent: Int, listType: Elem.List.Type): Unit = logger.trace { - result.current = Some(Elem.List(indent, listType, content)) + list.stack +:= indent + result.current = Some(Elem.List.empty(indent, listType)) result.push() } - def addContent(content: Elem): Unit = + def startListItem(): Unit = logger.trace { result.pop() result.current match { - case Some(list @ (_: Elem.List)) => - var currentContent = list.elems - currentContent = currentContent.append(content) - result.current = - Some(Elem.List(list.indent, list.typ, currentContent)) - case _ => + case Some(l: Elem.List) => + result.current = Some(l.addItem()) + result.push() + case elem => + throw new IllegalStateException( + s"Illegal startListItem state [current=$elem]" + ) } - result.push() } - def appendInnerToOuter(): Unit = + def startMisalignedListItem(indent: Int, typ: Elem.List.Type): Unit = logger.trace { result.pop() - val innerList = result.current.orNull - result.stack.head match { - case outerList @ (_: Elem.List) => - var outerContent = outerList.elems - innerList match { - case Elem.Newline => - case _ => outerContent = outerContent.append(innerList) - } - result.pop() - result.current = - Some(Elem.List(outerList.indent, outerList.typ, outerContent)) - case _ => - } - result.push() - innerList match { - case Elem.Newline => indent.onPushingNewLine() - case _ => + result.current match { + case Some(l: Elem.List) => + val item = Elem.MisalignedItem(indent, typ, List()) + result.current = Some(l.addItem(item)) + result.push() + case elem => + throw new IllegalStateException( + s"Illegal startMisalignedListItem state [current=$elem]" + ) } } + def endListItem(endList: Boolean = false): Unit = + logger.trace { + section.checkForUnclosedFormattersOnEOS() + val elems = stackUnwind() + result.current match { + case Some(l: Elem.List) => + if (endList) { + list.stack = list.stack.tail + } + result.current = Some(l.append(elems)) + result.push() + case elem => + throw new IllegalStateException( + s"Illegal endListItem state [current=$elem]" + ) + } + } + + def endSublist(): Unit = + logger.trace { + result.current match { + case None => + result.pop() + result.current match { + case Some(sublist: Elem.List) => + result.pop() + result.current match { + case Some(l: Elem.List) => + list.stack = list.stack.tail + result.current = Some(l.addItem(sublist)) + result.push() + case elem => + throw new IllegalStateException( + s"Illegal endSublist stack [List,$elem,...]" + ) + } + case elem => + throw new IllegalStateException( + s"Illegal endSublist stack [$elem,...]" + ) + } + case elem => + throw new IllegalStateException( + s"Illegal endSublist state [current=$elem]" + ) + } + } + + def addLastItem(): Unit = + logger.trace { + val elems = stackUnwind() + result.current match { + case Some(l: Elem.List) => + list.stack = list.stack.tail + if (elems.last == Elem.Newline) { + result.current = Some(l.append(elems.init)) + result.push() + result.current = Some(Elem.Newline) + result.push() + } else { + result.current = Some(l.append(elems)) + result.push() + } + while (stack.nonEmpty) { + list.endListItem() + list.endSublist() + } + case elem => + throw new IllegalStateException( + s"Illegal addLastItem state [current=$elem]" + ) + } + } + + /** Get all elements from the stack that were added after the [[Elem.List]] + * node was pushed. + */ + def stackUnwind(): List[Elem] = { + @scala.annotation.tailrec + def go(elems: List[Elem]): List[Elem] = { + result.pop() + result.current match { + case Some(_: Elem.List) | None => + elems + case Some(elem) => + go(elem :: elems) + } + } + + val init = result.current.toList + go(init) + } + def onOrdered(): Unit = logger.trace { state.end() - val matchedContent = currentMatch.split(orderedListTrigger) - val listIndent = matchedContent(0).length - val listElems = matchedContent(1) - indent.onIndentForListCreation(listIndent, Elem.List.Ordered, listElems) + val listIndent = currentMatch + .takeWhile(_ != orderedListTrigger) + .length + indent.onIndentForListCreation(listIndent, Elem.List.Ordered) } + def onUnordered(): Unit = logger.trace { state.end() - val matchedContent = currentMatch.split(unorderedListTrigger) - val listIndent = matchedContent(0).length - val listElems = matchedContent(1) - indent.onIndentForListCreation( - listIndent, - Elem.List.Unordered, - listElems - ) + val listIndent = currentMatch + .takeWhile(_ != unorderedListTrigger) + .length + indent.onIndentForListCreation(listIndent, Elem.List.Unordered) } val orderedListTrigger: Char = Elem.List.Ordered.marker val unorderedListTrigger: Char = Elem.List.Unordered.marker val orderedPattern: Pattern = - indent.indentPattern >> orderedListTrigger >> notNewLine + indent.indentPattern >> orderedListTrigger >> space val unorderedPattern: Pattern = - indent.indentPattern >> unorderedListTrigger >> notNewLine + indent.indentPattern >> unorderedListTrigger >> space } NEWLINE || list.orderedPattern || list.onOrdered() @@ -742,6 +823,11 @@ case class DocParserDef() extends Parser[Doc] { formatter.checkForUnclosed(Elem.Formatter.Strikeout) } + def checkForUnclosedListsOnEOS(): Unit = + if (list.isInList) { + list.addLastItem() + } + def reverseStackOnEOS(): Unit = logger.trace { result.stack = result.stack.reverse @@ -794,6 +880,7 @@ case class DocParserDef() extends Parser[Doc] { def onEOS(): Unit = logger.trace { checkForUnclosedFormattersOnEOS() + checkForUnclosedListsOnEOS() reverseStackOnEOS() header.create() push() @@ -850,13 +937,17 @@ case class DocParserDef() extends Parser[Doc] { def transformOverlyIndentedRawIntoCode( baseIndent: Int ): Unit = { - var newStack = List[Section]() + var newStack = List[Section]() + var currentIndent = baseIndent while (section.stack.nonEmpty) { var current = section.pop().get - if (current.indent > baseIndent && current.isInstanceOf[Section.Raw]) { + if ( + current.indent > currentIndent && + current.isInstanceOf[Section.Raw] + ) { var stackOfCodeSections: List[Section] = List[Section]() while ( - section.stack.nonEmpty && current.indent > baseIndent && current + section.stack.nonEmpty && current.indent > currentIndent && current .isInstanceOf[Section.Raw] && section.stack.head .isInstanceOf[Section.Raw] ) { @@ -866,20 +957,30 @@ case class DocParserDef() extends Parser[Doc] { } } stackOfCodeSections = stackOfCodeSections :+ current - val codeLines = stackOfCodeSections.flatMap(s => { - val inLines = s.repr.build().split("\n").map(_.trim) - inLines.map(Doc.Elem.CodeBlock.Line(s.indent, _)) - }) + val codeLines = stackOfCodeSections.flatMap { + _.repr + .build() + .split("\n") + .map { line => + val (indent, text) = line.span(_ == ' ') + Doc.Elem.CodeBlock.Line(indent.length, text) + } + } if (codeLines.nonEmpty) { val l1CodeLines = List1(codeLines.head, codeLines.tail) val codeBlock = Doc.Elem.CodeBlock(l1CodeLines) - val s = newStack.head - val sElems = newStack.head.elems :+ codeBlock - s.elems = sElems + val newElems = newStack.head.elems :+ codeBlock + val newSection = newStack.head match { + case marked: Doc.Section.Marked => + marked.copy(elems = newElems) + case raw: Doc.Section.Raw => + raw.copy(elems = newElems) + } newStack = newStack.drop(1) - newStack +:= s + newStack +:= newSection } } else { + currentIndent = current.indent newStack +:= current } } diff --git a/lib/scala/syntax/specialization/shared/src/test/scala/org/enso/syntax/text/DocParserTests.scala b/lib/scala/syntax/specialization/shared/src/test/scala/org/enso/syntax/text/DocParserTests.scala index 69d9d9dfb7..31439e219a 100644 --- a/lib/scala/syntax/specialization/shared/src/test/scala/org/enso/syntax/text/DocParserTests.scala +++ b/lib/scala/syntax/specialization/shared/src/test/scala/org/enso/syntax/text/DocParserTests.scala @@ -43,7 +43,7 @@ class DocParserTests extends AnyFlatSpec with Matchers { assertExpr(input, out) } def ?==(out: Doc): Unit = testBase in { - assertExpr(input, out, false) + assertExpr(input, out, assertShow = false) } } @@ -258,6 +258,151 @@ class DocParserTests extends AnyFlatSpec with Matchers { .Marked(1, 4, Section.Marked.Important, Section.Header("Important")) ) ) + """ ! Important + | This is important.""".stripMargin.replaceAll( + System.lineSeparator(), + "\n" + ) ?= Doc( + Synopsis( + Section.Marked( + 1, + 1, + Section.Marked.Important, + Section.Header("Important"), + Doc.Elem.Newline, + "This is important." + ) + ) + ) + """! Synopsis + | This _is_ important""".stripMargin.replaceAll( + System.lineSeparator(), + "\n" + ) ?= Doc( + Synopsis( + Section.Marked( + 0, + 1, + Section.Marked.Important, + Section.Header("Synopsis"), + Doc.Elem.Newline, + "This ", + Formatter(Formatter.Italic, "is"), + " important" + ) + ) + ) + """Synopsis + |This _is1_ important""".stripMargin.replaceAll( + System.lineSeparator(), + "\n" + ) ?= Doc( + Synopsis( + Section.Raw( + "Synopsis", + Doc.Elem.Newline, + "This ", + Formatter(Formatter.Italic, "is1"), + " important" + ) + ) + ) + """ Synopsis + | + | ! Important + | This is important.""".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, + "This is important." + ) + ) + ) + """ Synopsis + | + | ! Important + | This is important. + | + | And this""".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, + "This is important.", + Doc.Elem.Newline + ), + Section.Raw(3, "And this") + ) + ) + """ Synopsis + | + | !Important + | This is a code + | + | And this is not""".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(2, "And this is not") + ) + ) + """Synopsis + | + |! Important + | This is important + | + | And this is a code""".stripMargin.replaceAll( + System.lineSeparator(), + "\n" + ) ?== Doc( + Synopsis( + Section.Raw("Synopsis", Doc.Elem.Newline) + ), + Body( + Section.Marked( + 0, + 1, + Section.Marked.Important, + Section.Header("Important"), + Doc.Elem.Newline, + "This is important", + Doc.Elem.Newline, + CodeBlock(CodeBlock.Line(4, "And this is a code")) + ) + ) + ) "?Info" ?= Doc( Synopsis(Section.Marked(Section.Marked.Info, Section.Header("Info"))) ) @@ -293,6 +438,45 @@ class DocParserTests extends AnyFlatSpec with Matchers { Section.Marked(Section.Marked.Example, Section.Header("Example")) ) ) + """Synopsis + | + | ! Important + | This is important + | And this is a code + | + |> Example + | This is example + | More code""".stripMargin.replaceAll( + System.lineSeparator(), + "\n" + ) ?= Doc( + Synopsis( + Section.Raw("Synopsis", Doc.Elem.Newline) + ), + Body( + Section.Marked( + 1, + 1, + Section.Marked.Important, + Section.Header("Important"), + Doc.Elem.Newline, + "This is important", + Doc.Elem.Newline, + CodeBlock(CodeBlock.Line(5, "And this is a code")), + Doc.Elem.Newline + ), + Section.Marked( + 0, + 1, + Section.Marked.Example, + Section.Header("Example"), + Doc.Elem.Newline, + "This is example", + Doc.Elem.Newline, + CodeBlock(CodeBlock.Line(6, "More code")) + ) + ) + ) """Foo *Foo* ~*Bar~ `foo bar baz bo` | | @@ -339,7 +523,7 @@ class DocParserTests extends AnyFlatSpec with Matchers { Section.Raw( "ul:", Newline, - List(2, List.Unordered, " Foo", " Bar") + List(2, List.Unordered, "Foo", "Bar") ) ) ) @@ -348,25 +532,25 @@ class DocParserTests extends AnyFlatSpec with Matchers { Section.Raw( "ol:", Newline, - List(2, List.Ordered, " Foo", " Bar") + List(2, List.Ordered, "Foo", "Bar") ) ) ) """List - | - First unordered item - | - Second unordered item - | - Third unordered item""".stripMargin + |- First + |- Second + |- Third""".stripMargin .replaceAll(System.lineSeparator(), "\n") ?= Doc( Synopsis( Section.Raw( "List", Newline, List( - 2, + 0, List.Unordered, - " First unordered item", - " Second unordered item", - " Third unordered item" + "First", + "Second", + "Third" ) ) ) @@ -384,14 +568,234 @@ class DocParserTests extends AnyFlatSpec with Matchers { List( 2, List.Unordered, - " First unordered item", - " Second unordered item", - " Third unordered item" + "First unordered item", + "Second unordered item", + "Third unordered item" ), Newline ) ) ) + """List + |- _First_ + |- *Second*""".stripMargin + .replaceAll(System.lineSeparator(), "\n") ?= Doc( + Synopsis( + Section.Raw( + "List", + Newline, + List( + 0, + List.Unordered, + Formatter(Formatter.Italic, "First"), + Formatter(Formatter.Bold, "Second") + ) + ) + ) + ) + """List + |- _First_ list `item` + |- *Second* list ~item""".stripMargin + .replaceAll(System.lineSeparator(), "\n") ?= Doc( + Synopsis( + Section.Raw( + "List", + Newline, + List( + 0, + List.Unordered, + ListItem( + Formatter(Formatter.Italic, "First"), + " list ", + CodeBlock.Inline("item") + ), + ListItem( + Formatter(Formatter.Bold, "Second"), + " list ", + Formatter.Unclosed(Formatter.Strikeout, "item") + ) + ) + ) + ) + ) + """List + |- _First_ list `item` + |- *Second* list ~item + |""".stripMargin + .replaceAll(System.lineSeparator(), "\n") ?= Doc( + Synopsis( + Section.Raw( + "List", + Newline, + List( + 0, + List.Unordered, + ListItem( + Formatter(Formatter.Italic, "First"), + " list ", + CodeBlock.Inline("item") + ), + ListItem( + Formatter(Formatter.Bold, "Second"), + " list ", + Formatter.Unclosed(Formatter.Strikeout, "item", Newline) + ) + ) + ) + ) + ) + """ List + | - unclosed_formatter + | - second""".stripMargin.stripMargin + .replaceAll(System.lineSeparator(), "\n") ?== Doc( + Synopsis( + Section.Raw( + 1, + "List", + Newline, + List( + 3, + List.Unordered, + ListItem( + "unclosed", + Formatter.Unclosed(Formatter.Italic, "formatter") + ), + "second" + ) + ) + ) + ) + + """List + | - First + | - Second + | * First1 + | * Second1 + | - Third""".stripMargin + .replaceAll(System.lineSeparator(), "\n") ?= Doc( + Synopsis( + Section.Raw( + "List", + Newline, + List( + 2, + List.Unordered, + "First", + "Second", + List( + 4, + List.Ordered, + "First1", + "Second1" + ), + "Third" + ) + ) + ) + ) + """List + | - First + | - First1 + | - First2 + | - First3""".stripMargin + .replaceAll(System.lineSeparator(), "\n") ?= Doc( + Synopsis( + Section.Raw( + "List", + Newline, + List( + 2, + List.Unordered, + "First", + List( + 4, + List.Unordered, + "First1", + List( + 6, + List.Unordered, + "First2", + List( + 8, + List.Unordered, + "First3" + ) + ) + ) + ) + ) + ) + ) + """List + | - First + | - First1 + | - First2 + | - First3 + |""".stripMargin + .replaceAll(System.lineSeparator(), "\n") ?= Doc( + Synopsis( + Section.Raw( + "List", + Newline, + List( + 2, + List.Unordered, + "First", + List( + 4, + List.Unordered, + "First1", + List( + 6, + List.Unordered, + "First2", + List( + 8, + List.Unordered, + ListItem("First3", Newline) + ) + ) + ) + ) + ) + ) + ) + """List + | - First + | - First1 + | - First2 + | - First3 + | - Second""".stripMargin + .replaceAll(System.lineSeparator(), "\n") ?= Doc( + Synopsis( + Section.Raw( + "List", + Newline, + List( + 2, + List.Unordered, + "First", + List( + 4, + List.Unordered, + "First1", + List( + 6, + List.Unordered, + "First2", + List( + 8, + List.Unordered, + "First3" + ) + ) + ), + "Second" + ) + ) + ) + ) + """List | - First unordered item | - Second unordered item @@ -406,15 +810,15 @@ class DocParserTests extends AnyFlatSpec with Matchers { List( 2, List.Unordered, - " First unordered item", - " Second unordered item", + "First unordered item", + "Second unordered item", List( 4, List.Ordered, - " First ordered sub item", - " Second ordered sub item" + "First ordered sub item", + "Second ordered sub item" ), - " Third unordered item" + "Third unordered item" ) ) ) @@ -433,15 +837,15 @@ class DocParserTests extends AnyFlatSpec with Matchers { List( 2, List.Unordered, - " First unordered item", - " Second unordered item", + "First unordered item", + "Second unordered item", List( 4, List.Ordered, - " First ordered sub item", - " Second ordered sub item" + "First ordered sub item", + " Second ordered sub item" ), - " Third unordered item" + "Third unordered item" ) ) ) @@ -466,29 +870,29 @@ class DocParserTests extends AnyFlatSpec with Matchers { List( 2, List.Unordered, - " First unordered item", - " Second unordered item", + "First unordered item", + "Second unordered item", List( 4, List.Ordered, - " First ordered sub item", - " Second ordered sub item" + "First ordered sub item", + "Second ordered sub item" ), - " Third unordered item", + "Third unordered item", List( 4, List.Ordered, - " First ordered sub item", - " Second ordered sub item", + "First ordered sub item", + "Second ordered sub item", List( 6, List.Unordered, - " First unordered sub item", - " Second unordered sub item" + "First unordered sub item", + "Second unordered sub item" ), - " Third ordered sub item" + "Third ordered sub item" ), - " Fourth unordered item" + "Fourth unordered item" ) ) ) @@ -499,18 +903,12 @@ class DocParserTests extends AnyFlatSpec with Matchers { ////////////////////////////////////////////////////////////////////////////// """List - | - First unordered item - | - Second unordered item - | * First ordered sub item - | * Second ordered sub item - | - Third unordered item - | * First ordered sub item - | * Second ordered sub item - | - First unordered sub item - | - Second unordered sub item - | * Third ordered sub item - | * Wrong Indent Item - | - Fourth unordered item""".stripMargin + | - First + | * Aligned + | * Misaligned + | * Misaligned _styled_ + | * Correct + | - Second""".stripMargin .replaceAll(System.lineSeparator(), "\n") ?= Doc( Synopsis( Section.Raw( @@ -519,30 +917,53 @@ class DocParserTests extends AnyFlatSpec with Matchers { List( 2, List.Unordered, - " First unordered item", - " Second unordered item", + "First", List( - 4, + 5, List.Ordered, - " First ordered sub item", - " Second ordered sub item" + "Aligned", + Elem.MisalignedItem(4, List.Ordered, "Misaligned"), + Elem.MisalignedItem( + 3, + List.Ordered, + "Misaligned ", + Formatter(Formatter.Italic, "styled") + ), + "Correct" ), - " Third unordered item", + "Second" + ) + ) + ) + ) + + """List + | - First + | - First1 + | - Second1 + | - First2 + | - Second""".stripMargin + .replaceAll(System.lineSeparator(), "\n") ?= Doc( + Synopsis( + Section.Raw( + "List", + Newline, + List( + 2, + List.Unordered, + "First", List( 4, - List.Ordered, - " First ordered sub item", - " Second ordered sub item", + List.Unordered, + "First1", + Elem.MisalignedItem(3, List.Unordered, "Second1"), List( 6, List.Unordered, - " First unordered sub item", - " Second unordered sub item" - ), - " Third ordered sub item", - List.Indent.Invalid(3, List.Ordered, " Wrong Indent Item") + "First2" + ) ), - " Fourth unordered item" + "Second" ) ) ) @@ -770,7 +1191,23 @@ class DocParserTests extends AnyFlatSpec with Matchers { Synopsis( Section.Raw( Newline, - List(1, List.Unordered, " bar\n baz"), + List(1, List.Unordered, ListItem("bar", Newline, " ", "baz")), + Newline + ) + ) + ) + """ + | - bar + | baz + |""".stripMargin.replaceAll(System.lineSeparator(), "\n") ?= Doc( + Synopsis( + Section.Raw( + Newline, + List( + 1, + List.Unordered, + ListItem("bar", Newline, CodeBlock(CodeBlock.Line(5, "baz"))) + ), Newline ) ) @@ -784,7 +1221,31 @@ class DocParserTests extends AnyFlatSpec with Matchers { Synopsis( Section.Raw( Newline, - List(1, List.Unordered, " bar\n baz", " bar\n baz"), + List( + 1, + List.Unordered, + ListItem("bar", Newline, " ", "baz"), + ListItem("bar", Newline, " ", "baz") + ), + Newline + ) + ) + ) + """ + | - bar + | baz + | - bar + | baz + |""".stripMargin.replaceAll(System.lineSeparator(), "\n") ?= Doc( + Synopsis( + Section.Raw( + Newline, + List( + 1, + List.Unordered, + ListItem("bar", Newline, CodeBlock(CodeBlock.Line(5, "baz"))), + ListItem("bar", Newline, " ", "baz") + ), Newline ) ) @@ -800,8 +1261,41 @@ class DocParserTests extends AnyFlatSpec with Matchers { 1, "This does foo:", Newline, - List(1, List.Unordered, " bar\n baz"), + List( + 1, + List.Unordered, + ListItem( + "bar", + Newline, + " ", + "baz", + Newline, + " ", + "Another raw text." + ) + ), + Newline + ) + ) + ) + """ This does foo: + | - bar + | baz + | + | Another raw text. + |""".stripMargin.replaceAll(System.lineSeparator(), "\n") ?= Doc( + Synopsis( + Section.Raw( + 1, + "This does foo:", Newline, + List(1, List.Unordered, ListItem("bar", Newline, " ", "baz")), + Newline + ) + ), + Body( + Section.Raw( + 1, "Another raw text.", Newline ) @@ -822,7 +1316,7 @@ class DocParserTests extends AnyFlatSpec with Matchers { Newline, "This does foo:", Newline, - List(4, List.Unordered, " bar\n baz"), + List(4, List.Unordered, ListItem("bar", Newline, " ", "baz")), Newline ) ) @@ -928,7 +1422,10 @@ class DocParserTests extends AnyFlatSpec with Matchers { | import Standard.Base.System.File | import Standard.Examples | - | example_new = File.new Examples.csv_path + | example_new = + | path = + | Examples.csv_path + | File.new path |""".stripMargin.replaceAll(System.lineSeparator(), "\n") ?== Doc( Tags(Tags.Tag(0, Tags.Tag.Type.Alias, " New File")), Synopsis(Section.Raw(0, Newline)), @@ -951,9 +1448,13 @@ class DocParserTests extends AnyFlatSpec with Matchers { CodeBlock( CodeBlock.Line(6, "import Standard.Base.System.File"), CodeBlock.Line(6, "import Standard.Examples"), - CodeBlock.Line(6, "example_new = File.new Examples.csv_path") + CodeBlock.Line(6, "example_new ="), + CodeBlock.Line(10, "path ="), + CodeBlock.Line(14, "Examples.csv_path"), + CodeBlock.Line(10, "File.new path") ) ) ) ) + }