Skip to content

Commit 8ec44e0

Browse files
committed
Add scaladoc feature for preprocessing markdown files and publishing docs.scala-lang
1 parent 3109226 commit 8ec44e0

File tree

12 files changed

+312
-145
lines changed

12 files changed

+312
-145
lines changed

project/Build.scala

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import sbtbuildinfo.BuildInfoPlugin.autoImport._
2929
import scala.util.Properties.isJavaAtLeast
3030

3131
import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._
32+
import org.scalajs.linker.interface.ModuleInitializer
3233

3334
object DottyJSPlugin extends AutoPlugin {
3435
import Build._
@@ -329,7 +330,6 @@ object Build {
329330
".*java.*::javadoc::https://docs.oracle.com/javase/8/docs/api/",
330331
"-skip-by-regex:.+\\.internal($|\\..+)",
331332
"-skip-by-regex:.+\\.impl($|\\..+)",
332-
"-format", "md",
333333
"-project-logo", "docs/logo.svg",
334334
"-social-links:" +
335335
"github::https://github.com/lampepfl/dotty," +
@@ -1245,6 +1245,7 @@ object Build {
12451245
// Note: the two tasks below should be one, but a bug in Tasty prevents that
12461246
val generateScalaDocumentation = inputKey[Unit]("Generate documentation for dotty lib")
12471247
val generateTestcasesDocumentation = taskKey[Unit]("Generate documentation for testcases, usefull for debugging tests")
1248+
val copyTheScaladocJsOutput = inputKey[Unit]("Copy the output of the scaladoc js files")
12481249

12491250
lazy val `scaladoc-testcases` = project.in(file("scaladoc-testcases")).
12501251
dependsOn(`scala3-compiler-bootstrapped`).
@@ -1253,8 +1254,12 @@ object Build {
12531254
enablePlugins(DottyJSPlugin).
12541255
dependsOn(`scala3-library-bootstrappedJS`).
12551256
settings(
1257+
Compile / scalaJSMainModuleInitializer := (sys.env.get("scaladoc.projectFormat") match {
1258+
case Some("md") => Some(ModuleInitializer.mainMethod("dotty.tools.scaladoc.Main", "markdownMain"))
1259+
case _ => Some(ModuleInitializer.mainMethod("dotty.tools.scaladoc.Main", "main"))
1260+
}),
12561261
Test / fork := false,
1257-
scalaJSUseMainModuleInitializer := true,
1262+
Compile / scalaJSUseMainModuleInitializer := true,
12581263
libraryDependencies += ("org.scala-js" %%% "scalajs-dom" % "1.1.0").cross(CrossVersion.for3Use2_13)
12591264
)
12601265

@@ -1285,7 +1290,6 @@ object Build {
12851290
scalaSrcLink(stdLibVersion, srcManaged(dottyNonBootstrappedVersion, "scala") + "="),
12861291
dottySrcLink(referenceVersion, srcManaged(dottyNonBootstrappedVersion, "dotty") + "=", "#library/src"),
12871292
dottySrcLink(referenceVersion),
1288-
"-Ygenerate-inkuire",
12891293
) ++ scalacOptionsDocSettings ++ revision ++ params ++ targets
12901294
import _root_.scala.sys.process._
12911295
val escapedCmd = cmd.map(arg => if(arg.contains(" ")) s""""$arg"""" else arg)
@@ -1398,6 +1402,13 @@ object Build {
13981402
)
13991403
}.value,
14001404

1405+
copyTheScaladocJsOutput := Def.inputTask {
1406+
val extraArgs = spaceDelimited("<arg>").parsed
1407+
val dest = extraArgs.lift(0).getOrElse("output")
1408+
val jsDestinationFile: File = Paths.get(dest).toFile
1409+
sbt.IO.copyFile((`scaladoc-js` / Compile / fullOptJS).value.data, jsDestinationFile)
1410+
}.evaluated,
1411+
14011412
Test / buildInfoKeys := Seq[BuildInfoKey](
14021413
(Test / Build.testcasesOutputDir),
14031414
(Test / Build.testcasesSourceRoot),

project/scripts/genDocsScalaLang

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env bash
2+
3+
set -e
4+
shopt -s extglob # needed for rm everything but x
5+
echo "Working directory: $PWD"
6+
7+
GENDOC_EXTRA_ARGS=$@
8+
GIT_HEAD=$(git rev-parse HEAD) # save current head for commit message in gh-pages
9+
PREVIOUS_SNAPSHOTS_DIR="$PWD/../prev_snapshots"
10+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >& /dev/null && pwd)"
11+
SITE_OUT_DIR="$PWD/docs/_site"
12+
13+
DOCS_SCALA_LANG_DIR="$PWD/docsScalaLang"
14+
15+
rm -rf $DOCS_SCALA_LANG_DIR
16+
mkdir -pv $DOCS_SCALA_LANG_DIR
17+
git clone "https://github.com/scala/docs.scala-lang.git" $DOCS_SCALA_LANG_DIR
18+
19+
SBT="$SCRIPT_DIR/sbt"
20+
mkdir -pv $SITE_OUT_DIR
21+
env "scaladoc.projectFormat=md" "$SBT" "scaladoc/copyTheScaladocJsOutput $DOCS_SCALA_LANG_DIR/scripts/searchbar.js"
22+
"dist/target/pack/bin/scaladoc" "-d" "$SITE_OUT_DIR" "-format" "md" "-siteroot" "docs" "/dev/null"
23+
24+
if [ ! -d "$SITE_OUT_DIR" ]; then
25+
echo "Output directory did not exist: $SITE_OUT_DIR" 1>&2
26+
exit 1
27+
fi
28+
29+
cp -rf "$SITE_OUT_DIR/docs/reference"/* "$DOCS_SCALA_LANG_DIR/_scala3-reference"
30+
cp -f "$SITE_OUT_DIR/resources/my.css" "$DOCS_SCALA_LANG_DIR/scripts/my.css"
31+
cp -f "$SITE_OUT_DIR/resources/footer.html" "$DOCS_SCALA_LANG_DIR/_includes/footer.html"

scaladoc-js/src/Main.scala

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
package dotty.tools.scaladoc
22

3-
object Main extends App {
4-
Searchbar()
5-
SocialLinks()
6-
CodeSnippets()
7-
DropdownHandler()
8-
Ux()
9-
}
3+
object Main:
4+
5+
private def common(): Unit =
6+
CodeSnippets()
7+
8+
def main(): Unit =
9+
Searchbar()
10+
SocialLinks()
11+
DropdownHandler()
12+
Ux()
13+
common()
14+
15+
/**
16+
* This main is conditionally enabled by system env variable `scaladoc.projectFormat=md`
17+
* passed in ./projects/scripts/genDocsScalaLang
18+
* The reason why we have to pass the condition by env variable is because js is build before scaladoc,
19+
* so we cannot access its args
20+
*/
21+
def markdownMain(): Unit =
22+
common()

scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,10 @@ object Scaladoc:
234234
given docContext: DocContext = new DocContext(args, ctx)
235235
val module = ScalaModuleProvider.mkModule()
236236

237-
new dotty.tools.scaladoc.renderers.HtmlRenderer(module.rootPackage, module.members).render()
237+
val renderer = args.projectFormat match
238+
case "html" => new dotty.tools.scaladoc.renderers.HtmlRenderer(module.rootPackage, module.members)
239+
case "md" => new dotty.tools.scaladoc.renderers.MarkdownRenderer(module.rootPackage, module.members)
240+
241+
renderer.render()
238242
report.inform("generation completed successfully")
239243
docContext

scaladoc/src/dotty/tools/scaladoc/renderers/HtmlRenderer.scala

Lines changed: 14 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -14,122 +14,21 @@ import java.nio.file.Files
1414
import java.nio.file.FileVisitOption
1515
import java.io.File
1616

17-
case class Page(link: Link, content: Member | ResolvedTemplate | String, children: Seq[Page]):
18-
def withNewChildren(newChildren: Seq[Page]) = copy(children = children ++ newChildren)
19-
20-
def withTitle(newTitle: String) = copy(link = link.copy(name = newTitle))
21-
22-
def hasFrame = content match
23-
case t: ResolvedTemplate => t.hasFrame
24-
case _ => true
25-
26-
class HtmlRenderer(rootPackage: Member, val members: Map[DRI, Member])(using ctx: DocContext)
27-
extends SiteRenderer, Resources, Locations, Writer:
28-
private val args = summon[DocContext].args
29-
val staticSite = summon[DocContext].staticSiteContext
30-
31-
val effectiveMembers = members
32-
33-
private def memberPage(member: Member): Page =
34-
val childrenPages = member.members.filter(_.needsOwnPage)
35-
Page(Link(member.name, member.dri), member, childrenPages.map(memberPage))
36-
37-
val navigablePage: Page =
38-
val rootPckPage = memberPage(rootPackage)
39-
staticSite match
40-
case None => rootPckPage.withTitle(args.name)
41-
case Some(siteContext) =>
42-
val (indexes, templates) = siteContext.templates.partition(f =>
43-
f.templateFile.isIndexPage() && f.file.toPath.getParent() == siteContext.docsPath)
44-
if (indexes.size > 1)
45-
val msg = s"ERROR: Multiple index pages for doc found ${indexes.map(_.file)}"
46-
report.error(msg)
47-
48-
val templatePages = templates.map(templateToPage(_, siteContext))
49-
50-
indexes.headOption match
51-
case None if templatePages.isEmpty=>
52-
rootPckPage.withTitle(args.name)
53-
case None =>
54-
Page(Link(args.name, docsRootDRI),"", templatePages :+ rootPckPage.withTitle("API"))
55-
case Some(indexPage) =>
56-
val newChildren = templatePages :+ rootPckPage.withTitle("API")
57-
templateToPage(indexPage, siteContext).withNewChildren(newChildren)
58-
59-
val hiddenPages: Seq[Page] =
60-
staticSite match
61-
case None =>
62-
Seq(navigablePage.copy( // Add index page that is a copy of api/index.html
63-
link = navigablePage.link.copy(dri = docsRootDRI),
64-
children = Nil
65-
))
66-
case Some(siteContext) =>
67-
// In case that we do not have an index page and we do not have any API entries
68-
// we want to create empty index page, so there is one
69-
val actualIndexTemplate = siteContext.indexTemplate() match {
70-
case None if effectiveMembers.isEmpty => Seq(siteContext.emptyIndexTemplate)
71-
case templates => templates.toSeq
72-
}
73-
74-
(siteContext.orphanedTemplates ++ actualIndexTemplate).map(templateToPage(_, siteContext))
75-
76-
/**
77-
* Here we have to retrive index pages from hidden pages and replace fake index pages in navigable page tree.
78-
*/
79-
val allPages: Seq[Page] =
80-
def traversePages(page: Page): (Page, Seq[Page]) =
81-
val (newChildren, newPagesToRemove): (Seq[Page], Seq[Page]) = page.children.map(traversePages(_)).foldLeft((Seq[Page](), Seq[Page]())) {
82-
case ((pAcc, ptrAcc), (p, ptr)) => (pAcc :+ p, ptrAcc ++ ptr)
83-
}
84-
hiddenPages.find(_.link == page.link) match
85-
case None =>
86-
(page.copy(children = newChildren), newPagesToRemove)
87-
case Some(newPage) =>
88-
(newPage.copy(children = newChildren), newPagesToRemove :+ newPage)
89-
90-
val (newNavigablePage, pagesToRemove) = traversePages(navigablePage)
91-
92-
val all = newNavigablePage +: hiddenPages.filterNot(pagesToRemove.contains)
93-
// We need to check for conflicts only if we have top-level member called blog or docs
94-
val hasPotentialConflict =
95-
rootPackage.members.exists(m => m.name.startsWith("docs") || m.name.startsWith("blog"))
96-
97-
if hasPotentialConflict then
98-
def walk(page: Page): Unit =
99-
if page.link.dri.isStaticFile then
100-
val dest = absolutePath(page.link.dri)
101-
if apiPaths.contains(dest) then
102-
report.error(s"Conflict between static page and API member for $dest. $pathsConflictResoultionMsg")
103-
page.children.foreach(walk)
104-
105-
all.foreach (walk)
106-
107-
all
108-
109-
def renderContent(page: Page) = page.content match
110-
case m: Member =>
111-
val signatureRenderer = new SignatureRenderer:
112-
def currentDri: DRI = page.link.dri
113-
def link(dri: DRI): Option[String] =
114-
Some(pathToPage(currentDri, dri)).filter(_ != UnresolvedLocationLink)
115-
116-
MemberRenderer(signatureRenderer).fullMember(m)
117-
case t: ResolvedTemplate => siteContent(page.link.dri, t)
118-
case a: String => raw(a)
119-
120-
121-
def renderPage(page: Page, parents: Vector[Link]): Seq[String] =
122-
val newParents = parents :+ page.link
123-
val content = ctx.args.projectFormat match
124-
case "html" => html(
125-
mkHead(page),
126-
body(
127-
if !page.hasFrame then renderContent(page)
128-
else mkFrame(page.link, newParents, renderContent(page))
129-
)
17+
class HtmlRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx: DocContext)
18+
extends Renderer(rootPackage, members, extension = "html"):
19+
20+
override def pageContent(page: Page, parents: Vector[Link]): AppliedTag =
21+
html(
22+
mkHead(page),
23+
body(
24+
if !page.hasFrame then renderContent(page)
25+
else mkFrame(page.link, parents, renderContent(page))
13026
)
131-
case "md" => renderContent(page)
132-
write(page.link.dri, content) +: page.children.flatMap(renderPage(_, newParents))
27+
)
28+
29+
override def render(): Unit =
30+
val renderedResources = renderResources()
31+
super.render()
13332

13433
private def specificResources(page: Page): Set[String] =
13534
page.children.toSet.flatMap(specificResources) ++ (page.content match
@@ -159,10 +58,6 @@ class HtmlRenderer(rootPackage: Member, val members: Map[DRI, Member])(using ctx
15958
val resources = siteResourcesPaths.toSeq.map(pathToResource) ++ allResources(allPages) ++ onlyRenderedResources
16059
resources.flatMap(renderResource)
16160

162-
def render(): Unit =
163-
val renderedResources = renderResources()
164-
val sites = allPages.map(renderPage(_, Vector.empty))
165-
16661
def mkHead(page: Page): AppliedTag =
16762
val resources = page.content match
16863
case t: ResolvedTemplate =>
@@ -226,14 +121,6 @@ class HtmlRenderer(rootPackage: Member, val members: Map[DRI, Member])(using ctx
226121

227122
renderNested(navigablePage, toplevel = true)._2
228123

229-
private def canonicalUrl(l: String): AppliedTag | String =
230-
val canon = args.docCanonicalBaseUrl
231-
if !canon.isEmpty then
232-
val canonicalUrl = if canon.endsWith("/") then canon else canon + "/"
233-
link(rel := "canonical", href := canonicalUrl + l)
234-
else
235-
"" // return empty tag
236-
237124
private def hasSocialLinks = !args.socialLinks.isEmpty
238125

239126
private def socialLinks(whiteIcon: Boolean = true) =

scaladoc/src/dotty/tools/scaladoc/renderers/Locations.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ trait Locations(using ctx: DocContext):
2121

2222
// We generate this collection only if there may be a conflict with resources.
2323
// Potentially can be quite big.
24-
lazy val apiPaths = effectiveMembers.keySet.filterNot(_.isStaticFile).map(absolutePath)
24+
lazy val apiPaths = effectiveMembers.keySet.filterNot(_.isStaticFile).map(absolutePath(_))
2525

2626
var cache = new JHashMap[DRI, Seq[String]]()
2727

@@ -80,7 +80,7 @@ trait Locations(using ctx: DocContext):
8080
pathToRaw(from, to.split("/").toList)
8181

8282
def resolveRoot(dri: DRI, path: String): String = resolveRoot(rawLocation(dri), path)
83-
def absolutePath(dri: DRI): String = rawLocation(dri).mkString("", "/", ".html")
83+
def absolutePath(dri: DRI, extension: String = "html"): String = rawLocation(dri).mkString("", "/", s".$extension")
8484

8585
def resolveLink(dri: DRI, url: String): String =
8686
if URI(url).isAbsolute then url else resolveRoot(dri, url)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package dotty.tools.scaladoc
2+
package renderers
3+
4+
import util.HTML._
5+
import collection.JavaConverters._
6+
import java.net.URI
7+
import java.net.URL
8+
import dotty.tools.scaladoc.site._
9+
import scala.util.Try
10+
import org.jsoup.Jsoup
11+
import java.nio.file.Paths
12+
import java.nio.file.Path
13+
import java.nio.file.Files
14+
import java.nio.file.FileVisitOption
15+
import java.io.File
16+
17+
class MarkdownRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx: DocContext)
18+
extends Renderer(rootPackage, members, extension = "md"):
19+
20+
override def render(): Unit =
21+
renderResources()
22+
super.render()
23+
24+
override def pageContent(page: Page, parents: Vector[Link]): AppliedTag =
25+
renderContent(page)
26+
27+
private def renderResources(): Seq[String] =
28+
29+
// TODO REMOVE THIS CODE AND RESOURCES UNDER docs/resources
30+
// when we sort out the css classes
31+
def siteRoot = staticSite.get.root.toPath
32+
def pathToResource(p: String) = Resource.File(p, siteRoot.resolve(p))
33+
34+
def harvestResources(path: String) =
35+
val siteImgPath = siteRoot.resolve(path)
36+
if !Files.exists(siteImgPath) then Nil
37+
else
38+
val allPaths = Files.walk(siteImgPath, FileVisitOption.FOLLOW_LINKS)
39+
val files = allPaths.filter(Files.isRegularFile(_)).iterator().asScala
40+
files.map(p => siteRoot.relativize(p).toString).toList
41+
42+
val staticResources = staticSite.toSeq.flatMap { _ =>
43+
harvestResources("images") ++ harvestResources("resources")
44+
}
45+
staticResources.map(pathToResource).flatMap(renderResource)
46+
// END TODO REMOVE THIS CODE AND RESOURCES UNDER docs/resources
47+
48+
allResources(Nil).flatMap(renderResource)

0 commit comments

Comments
 (0)