Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite Docs Generator from Python to Scala/Java #1729

Merged
merged 45 commits into from
May 17, 2021
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
2734aba
save some WIP
BinarySoftware Apr 26, 2021
24fe6ae
save some WIP
BinarySoftware Apr 26, 2021
364eee3
26s is better than 4 mins in py+js
BinarySoftware Apr 27, 2021
e9dd954
Refactor DocParser into smaller organized files.
BinarySoftware Apr 27, 2021
171ce76
more maps
BinarySoftware Apr 28, 2021
ba92c4f
gen JS files.
BinarySoftware Apr 28, 2021
48603d2
gen JS files.
BinarySoftware Apr 28, 2021
b7d5a7a
save some content
BinarySoftware Apr 29, 2021
6926ba2
f
BinarySoftware Apr 29, 2021
0c42d0e
safe
BinarySoftware Apr 30, 2021
b8e2ef3
safe
BinarySoftware May 3, 2021
a9b36a7
Merge remote-tracking branch 'origin/main' into wip/mm/docs-gen
BinarySoftware May 3, 2021
8e3650e
save
BinarySoftware May 3, 2021
bc41d47
save
BinarySoftware May 4, 2021
96ae88b
save
BinarySoftware May 5, 2021
1455003
save
BinarySoftware May 5, 2021
183bdc0
save
BinarySoftware May 5, 2021
6cf5a2d
save
BinarySoftware May 5, 2021
35278c4
save
BinarySoftware May 5, 2021
9297007
works
BinarySoftware May 5, 2021
6d4a26e
Merge remote-tracking branch 'origin/main' into wip/mm/docs-gen
BinarySoftware May 5, 2021
def2669
Merge remote-tracking branch 'origin/main' into wip/mm/docs-gen
BinarySoftware May 6, 2021
e1b4d69
docs
BinarySoftware May 6, 2021
403a088
docs
BinarySoftware May 6, 2021
174a1cf
prettier
BinarySoftware May 6, 2021
4252284
changelog.
BinarySoftware May 6, 2021
290bb68
restructure
BinarySoftware May 7, 2021
7ae052d
Merge remote-tracking branch 'origin/main' into wip/mm/docs-gen
BinarySoftware May 7, 2021
b98206d
save
BinarySoftware May 7, 2021
85593cf
save
BinarySoftware May 7, 2021
4a11fb2
save
BinarySoftware May 10, 2021
da7cd4a
save
BinarySoftware May 10, 2021
3c1f8a5
save
BinarySoftware May 10, 2021
eed8859
save
BinarySoftware May 10, 2021
4fbc629
save
BinarySoftware May 10, 2021
5217343
save
BinarySoftware May 10, 2021
8099c71
accept parameters
BinarySoftware May 10, 2021
7eee64b
Merge remote-tracking branch 'origin/main' into wip/mm/docs-gen
BinarySoftware May 11, 2021
3b5bfff
CLI Argument parser.
BinarySoftware May 11, 2021
eb0be40
save
BinarySoftware May 11, 2021
af3d31e
save
BinarySoftware May 12, 2021
de0f3ce
Merge remote-tracking branch 'origin/main' into wip/mm/docs-gen
BinarySoftware May 12, 2021
2966177
save ci config
BinarySoftware May 13, 2021
e4adb46
Merge remote-tracking branch 'origin/main' into wip/mm/docs-gen
BinarySoftware May 14, 2021
44e97e4
Merge remote-tracking branch 'origin/main' into wip/mm/docs-gen
BinarySoftware May 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ package-lock.json

javadoc/
scaladoc/
distribution/std-lib/docs
distribution/std-lib/docs-js

#######################
## Benchmark Reports ##
Expand Down
4 changes: 4 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
- Overhauled the types we use for errors throughout the standard library
([#1734](https://github.com/enso-org/enso/pull/1734)). They are now much more
informative, and should provide more clarity when things go wrong.
- Re-wrote the documentation generator for the Enso website from Python into
Scala ([#1729](https://github.com/enso-org/enso/pull/1729)). This has greatly
improved the performance, enabling us to generate the documentation structure
for the entire standard library 8-10 times faster than before.

## Miscellaneous

Expand Down
11 changes: 11 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ lazy val enso = (project in file("."))
`json-rpc-server`,
`language-server`,
`parser-service`,
`docs-generator`,
`polyglot-api`,
`project-manager`,
`syntax-definition`.jvm,
Expand Down Expand Up @@ -539,6 +540,16 @@ lazy val `parser-service` = (project in file("lib/scala/parser-service"))
mainClass := Some("org.enso.ParserServiceMain")
)

lazy val `docs-generator` = (project in file("lib/scala/docs-generator"))
.dependsOn(syntax.jvm)
.dependsOn(cli)
.settings(
libraryDependencies ++= Seq(
"commons-cli" % "commons-cli" % commonsCliVersion
),
mainClass := Some("org.enso.docs.generator.Main")
)

lazy val `text-buffer` = project
.in(file("lib/scala/text-buffer"))
.configs(Test)
Expand Down
4 changes: 4 additions & 0 deletions lib/scala/docs-generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Docs Generator

A service generating docs from contents of library, and saving them as `.html`
files in a `docs` subdirectory.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.enso.docs.generator

import java.io.File
import org.enso.syntax.text.{DocParser, Parser}
import org.enso.syntax.text.docparser._

/** The Docs Generator class. Defines useful wrappers for Doc Parser.
*/
object DocsGenerator {

/** Generates HTML of docs from Enso program.
*/
def run(program: String): String = {
val parser = new Parser()
val module = parser.run(program)
val dropMeta = parser.dropMacroMeta(module)
val doc = DocParserRunner.createDocs(dropMeta)
val code = DocParserHTMLGenerator.generateHTMLForEveryDocumented(doc)
code
}

/** Generates HTML from Documentation string.
*/
def runOnPureDoc(comment: String): String = {
val doc = DocParser.runMatched(comment)
val html = DocParserHTMLGenerator.generateHTMLPureDoc(doc)
html
}

/** Called if file doesn't contain docstrings, to let user know that they
* won't find anything at this page, and that it is not a bug.
*/
def mapIfEmpty(doc: String): String = {
var tmp = doc
if (doc.replace("<div>", "").replace("</div>", "").length == 0) {
tmp =
"\n\n*Enso Reference Viewer.*\n\nNo documentation available for chosen source file."
tmp = runOnPureDoc(tmp).replace("style=\"font-size: 13px;\"", "")
}
tmp
}

/** Doc Parser may output file with many nested empty divs.
* This simple function will remove all unnecessary HTML tags.
*/
def removeUnnecessaryDivs(doc: String): String = {
var tmp = doc
while (tmp.contains("<div></div>"))
tmp = tmp.replace("<div></div>", "")
tmp
}

/** Traverses through root directory, outputs list of all accessible files.
*/
def traverse(root: File): LazyList[File] =
if (!root.exists) { LazyList.empty }
else {
LazyList.apply(root) ++ (root.listFiles match {
case null => LazyList.empty
case files => files.view.flatMap(traverse)
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package org.enso.docs.generator

import java.io._
import org.apache.commons.cli.{Option => CliOption, _}
import scala.util.{Try, Using}
import scala.io.Source
import scala.annotation.unused
import scalatags.Text.{all => HTML}
import TreeOfCommonPrefixes._
import DocsGenerator._
import HTML._

/** The entry point for the documentation generator.
*
* The documentation generator is responsible for creating HTML documentation
* for the Enso standard library.
* It also generates JavaScript files containing react components for the
* [[https://enso.org/docs/reference reference website]].
*/
object Main {

var libPath =
"./lib/scala/docs-generator/src/main/scala/org/enso/docs/generator/"
var path = "./distribution/std-lib/Standard/src"
var jsTempFileName = "template.js"
var cssFileName = "treeStyle.css"
var outDir = "docs-js"
BinarySoftware marked this conversation as resolved.
Show resolved Hide resolved

private val HELP_OPTION = "help"
private val INPUT_PATH_OPTION = "input_path"
private val OUTPUT_DIR_OPTION = "output_dir"
private val DOCS_LIB_PATH_OPTION = "docs_lib_path"
private val JS_TEMPLATE_OPTION = "js_template"
private val CSS_TEMPLATE_OPTION = "css_template"
BinarySoftware marked this conversation as resolved.
Show resolved Hide resolved

/** Builds the [[Options]] object representing the CLI syntax.
*
* @return an [[Options]] object representing the CLI syntax
*/
private def buildOptions = {
val help = CliOption
.builder("h")
.longOpt(HELP_OPTION)
.desc("Displays this message.")
.build
val input = CliOption.builder
.longOpt(INPUT_PATH_OPTION)
.numberOfArgs(1)
.argName("path")
.desc("Specifies working path.")
.build
val output = CliOption.builder
.hasArg(true)
.numberOfArgs(1)
.argName("directory")
.longOpt(OUTPUT_DIR_OPTION)
.desc("Specifies name of output directory.")
.build
val docsPath = CliOption.builder
.hasArg(true)
.numberOfArgs(1)
.argName("path")
.longOpt(DOCS_LIB_PATH_OPTION)
.desc("Specifies path of Docs Generator library.")
.build
val jsTemp = CliOption.builder
.hasArg(true)
.numberOfArgs(1)
.argName("file")
.longOpt(JS_TEMPLATE_OPTION)
.desc("Specifies name of file containing JS template.")
.build
val cssTemp = CliOption.builder
.hasArg(true)
.numberOfArgs(1)
.argName("file")
.longOpt(CSS_TEMPLATE_OPTION)
.desc("Specifies name of file containing CSS template.")
.build

val options = new Options
options
.addOption(help)
.addOption(input)
.addOption(output)
.addOption(docsPath)
.addOption(jsTemp)
.addOption(cssTemp)

options
}

/** Prints the help message to the standard output.
*
* @param options object representing the CLI syntax
*/
private def printHelp(options: Options): Unit =
new HelpFormatter().printHelp("Docs Generator", options)

/** Terminates the process with a failure exit code. */
private def exitFail(): Nothing = sys.exit(1)

/** Terminates the process with a success exit code. */
private def exitSuccess(): Nothing = sys.exit(0)

/** Starting point. */
def main(args: Array[String]): Unit = {
val options = buildOptions
val parser = new DefaultParser
val line = Try(parser.parse(options, args)).getOrElse {
printHelp(options)
exitFail()
}
if (line.hasOption(HELP_OPTION)) {
printHelp(options)
exitSuccess()
}
if (line.hasOption(INPUT_PATH_OPTION)) {
path = line.getOptionValue(INPUT_PATH_OPTION)
}
if (line.hasOption(OUTPUT_DIR_OPTION)) {
outDir = line.getOptionValue(OUTPUT_DIR_OPTION)
}
if (line.hasOption(DOCS_LIB_PATH_OPTION)) {
libPath = line.getOptionValue(DOCS_LIB_PATH_OPTION)
}
if (line.hasOption(JS_TEMPLATE_OPTION)) {
jsTempFileName = line.getOptionValue(JS_TEMPLATE_OPTION)
}
if (line.hasOption(CSS_TEMPLATE_OPTION)) {
cssFileName = line.getOptionValue(CSS_TEMPLATE_OPTION)
}

generateAllDocs()
}

/** Traverses through directory generating docs from every .enso file found.
*/
def generateAllDocs(): Unit = {
val allFiles = traverse(new File(path))
.filter(f => f.isFile && f.getName.endsWith(".enso"))
val allFileNames = allFiles.map(
_.getPath
.replace(path + "/", "")
.replace(".enso", "")
)
val allPrograms = allFiles
.map(f => Using(Source.fromFile(f, "UTF-8")) { _.mkString })
.toList
val allDocs = allPrograms
.map(s => run(s.getOrElse("")))
.map(mapIfEmpty)
.map(removeUnnecessaryDivs)
val treeNames =
groupByPrefix(allFileNames.toList, '/').filter(_.elems.nonEmpty)
val jsTemplate = new File(libPath + jsTempFileName)
val templateCode = Using(Source.fromFile(jsTemplate, "UTF-8")) {
_.mkString
}
val styleFile = new File(libPath + cssFileName)
val styleCode = Using(Source.fromFile(styleFile, "UTF-8")) { _.mkString }
val treeStyle = "<style jsx>{`" + styleCode.getOrElse("") + "`}</style>"
val allDocJSFiles = allFiles.map { x =>
val name = x.getPath
.replace(".enso", ".js")
.replace("Standard/src", outDir)
.replace("Main.js", "index.js")
val ending = name.split(outDir + "/").tail.head
name.replace(ending, ending.replace('/', '-'))
}
val dir = new File(allDocJSFiles.head.split(outDir).head + outDir + "/")
dir.mkdirs()
val zippedJS = allDocJSFiles.zip(allDocs)
zippedJS.foreach(
createDocJSFile(_, outDir, treeStyle, templateCode, treeNames)
)
}

/** Takes a tuple of file path and documented HTML code, saving the file
* in the given directory.
*/
@unused private def createDocHTMLFile(x: (String, String)): Unit = {
BinarySoftware marked this conversation as resolved.
Show resolved Hide resolved
val path = x._1
val file = new File(path)
val fileWithExtensionRegex = "\\/[a-zA-Z_]*\\.[a-zA-Z]*"
val dir = new File(path.replaceAll(fileWithExtensionRegex, ""))
dir.mkdirs()
file.createNewFile();
val bw = new BufferedWriter(new FileWriter(file))
bw.write(x._2)
bw.close()
}

/** Takes a tuple of file path and documented HTML code, and generates JS doc
* file with react components for Enso website.
*/
private def createDocJSFile(
x: (String, String),
BinarySoftware marked this conversation as resolved.
Show resolved Hide resolved
outDir: String,
treeStyle: String,
templateCode: Try[String],
treeNames: List[Node]
): Unit = {
val path = x._1
val htmlCode = x._2
val file = new File(path)
file.createNewFile();
val bw = new BufferedWriter(new FileWriter(file))
var treeCode =
"<div>" + treeStyle + HTML
.ul(treeNames.map(_.html()))
.render
.replace("class=", "className=") + "</div>"
if (path.contains("/index.js")) {
treeCode = treeCode.replace("a href=\"", "a href=\"reference/")
}
val partials = path
.split(outDir + "/")
.tail
.head
.replace(".js", "")
.split("-")
for (i <- 1 to partials.length) {
val id = partials.take(i).mkString("-")
val beg = "<input type=\"checkbox\" id=\"" + id + "\" "
treeCode = treeCode.replace(beg, beg + "checked=\"True\"")
}
val docCopyBtnClass = "doc-copy-btn"
bw.write(
templateCode
.getOrElse("")
.replace(
"{/*PAGE*/}",
htmlCode
.replace(docCopyBtnClass + " flex", docCopyBtnClass + " none")
.replace("{", "&#123;")
.replace("}", "&#125;")
)
.replace("{/*BREADCRUMBS*/}", treeCode)
)
bw.close()
}
}