Skip to content

Commit 23ba6c3

Browse files
authored
Merge pull request #11021 from pikinier20/scala3doc-searchbar
Replace default dokka searchbar with new implemented in Scala.js
2 parents bbfff61 + d0a8992 commit 23ba6c3

17 files changed

+386
-34
lines changed

build.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ val `tasty-core-bootstrapped` = Build.`tasty-core-bootstrapped`
2222
val `tasty-core-scala2` = Build.`tasty-core-scala2`
2323
val scala3doc = Build.scala3doc
2424
val `scala3doc-testcases` = Build.`scala3doc-testcases`
25+
val `scala3doc-js` = Build.`scala3doc-js`
2526
val `scala3-bench-run` = Build.`scala3-bench-run`
2627
val dist = Build.dist
2728
val `community-build` = Build.`community-build`

project/Build.scala

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import sbtbuildinfo.BuildInfoPlugin.autoImport._
2828

2929
import scala.util.Properties.isJavaAtLeast
3030

31+
import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._
32+
3133
object MyScalaJSPlugin extends AutoPlugin {
3234
import Build._
3335

@@ -1230,6 +1232,8 @@ object Build {
12301232
lazy val `scala3doc` = project.in(file("scala3doc")).asScala3doc
12311233
lazy val `scala3doc-testcases` = project.in(file("scala3doc-testcases")).asScala3docTestcases
12321234

1235+
lazy val `scala3doc-js` = project.in(file("scala3doc-js")).asScala3docJs
1236+
12331237
// sbt plugin to use Dotty in your own build, see
12341238
// https://github.com/lampepfl/scala3-example-project for usage.
12351239
lazy val `sbt-dotty` = project.in(file("sbt-dotty")).
@@ -1643,6 +1647,19 @@ object Build {
16431647
),
16441648
Compile / buildInfoKeys := Seq[BuildInfoKey](version),
16451649
Compile / buildInfoPackage := "dotty.dokka",
1650+
Compile / resourceGenerators += Def.task {
1651+
val jsDestinationFile = (Compile / resourceManaged).value / "dotty_res" / "scripts" / "searchbar.js"
1652+
sbt.IO.copyFile((fullOptJS in Compile in `scala3doc-js`).value.data, jsDestinationFile)
1653+
Seq(jsDestinationFile)
1654+
}.taskValue,
1655+
Compile / resourceGenerators += Def.task {
1656+
val cssDesitnationFile = (Compile / resourceManaged).value / "dotty_res" / "styles" / "scala3doc-searchbar.css"
1657+
val cssSourceFile = (resourceDirectory in Compile in `scala3doc-js`).value / "scala3doc-searchbar.css"
1658+
FileFunction.cached(streams.value.cacheDirectory / "css-cache") { (in: Set[File]) =>
1659+
in.headOption.map(sbt.IO.copyFile(_, cssDesitnationFile))
1660+
Set(cssDesitnationFile)
1661+
}.apply(Set(cssSourceFile)).toSeq
1662+
}.taskValue,
16461663
testDocumentationRoot := (baseDirectory.value / "test-documentations").getAbsolutePath,
16471664
buildInfoPackage in Test := "dotty.dokka.test",
16481665
BuildInfoPlugin.buildInfoScopedSettings(Test),
@@ -1656,6 +1673,17 @@ object Build {
16561673
def asScala3docTestcases: Project =
16571674
project.dependsOn(`scala3-compiler-bootstrapped`).settings(commonBootstrappedSettings)
16581675

1676+
def asScala3docJs: Project =
1677+
project.
1678+
enablePlugins(MyScalaJSPlugin).
1679+
dependsOn(`scala3-library-bootstrappedJS`).
1680+
settings(
1681+
fork in Test := false,
1682+
scalaJSUseMainModuleInitializer := true,
1683+
libraryDependencies += ("org.scala-js" %%% "scalajs-dom" % "1.1.0").withDottyCompat(scalaVersion.value)
1684+
)
1685+
1686+
16591687
def asDist(implicit mode: Mode): Project = project.
16601688
enablePlugins(PackPlugin).
16611689
withCommonSettings.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/* button */
2+
.search span {
3+
background: #ED3522;
4+
fill: #fff;
5+
cursor: pointer;
6+
border: none;
7+
padding: 9px;
8+
border-radius: 24px;
9+
box-shadow: 0 0 16px #F27264;
10+
}
11+
.search span:hover {
12+
fill: #F27264;
13+
}
14+
15+
@media(max-width: 576px) {
16+
.search span {
17+
background: none;
18+
fill: var(--icon-color);
19+
cursor: pointer;
20+
border: none;
21+
padding: 0;
22+
box-shadow: none;
23+
margin-top: 2px;
24+
}
25+
.search span:hover {
26+
fill: var(--link-hover-fg);
27+
}
28+
}
29+
30+
#scala3doc-search {
31+
margin-top: 10px;
32+
cursor: pointer;
33+
position: fixed;
34+
top: 0;
35+
right: 20px;
36+
z-index: 5;
37+
}
38+
39+
#scala3doc-searchbar.hidden {
40+
display: none;
41+
}
42+
43+
#scala3doc-searchbar {
44+
position: absolute;
45+
top: 50px;
46+
right: 40px;
47+
width: calc(100% - 360px);
48+
box-shadow: 0 2px 16px 0 rgba(0, 42, 76, 0.15);
49+
font-size: 13px;
50+
font-family: system-ui, -apple-system, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Arial, sans-serif;
51+
}
52+
53+
#scala3doc-searchbar-input {
54+
width: 100%;
55+
min-height: 32px;
56+
border: none;
57+
border-bottom: 1px solid #bbb;
58+
padding: 10px;
59+
}
60+
61+
#scala3doc-searchbar-input:focus {
62+
outline: none;
63+
}
64+
65+
#scala3doc-searchbar-results {
66+
background: white;
67+
display: flex;
68+
flex-direction: column;
69+
max-height: 500px;
70+
overflow: auto;
71+
}
72+
73+
.scala3doc-searchbar-result {
74+
line-height: 32px;
75+
padding-left: 10px;
76+
padding-right: 10px;
77+
}
78+
79+
.scala3doc-searchbar-result:first-of-type {
80+
margin-top: 10px;
81+
}
82+
83+
.scala3doc-searchbar-result:hover {
84+
background-color: #d4edff;
85+
}
86+
87+
.scala3doc-searchbar-result a {
88+
color: #1f2326;
89+
}
90+
91+
.scala3doc-searchbar-result .scala3doc-searchbar-location {
92+
color: gray;
93+
}
94+
95+
#searchBar {
96+
display: inline-flex;
97+
}
98+
99+
.pull-right {
100+
float: right;
101+
margin-left: auto
102+
}

scala3doc-js/src/Globals.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package dotty.dokka
2+
3+
import scala.scalajs.js
4+
import scala.scalajs.js.annotation.JSGlobalScope
5+
6+
@js.native
7+
@JSGlobalScope
8+
object Globals extends js.Object {
9+
val pathToRoot: String = js.native
10+
}

scala3doc-js/src/Main.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package dotty.dokka
2+
3+
object Main extends App {
4+
Searchbar()
5+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package dotty.dokka
2+
3+
import scala.scalajs.js
4+
5+
@js.native
6+
trait PageEntryJS extends js.Object {
7+
val name: String = js.native
8+
val description: String = js.native
9+
val location: String = js.native
10+
val searchKeys: js.Array[String] = js.native
11+
}
12+
13+
case class PageEntry(
14+
fullName: String,
15+
description: String,
16+
location: String,
17+
shortName: String,
18+
acronym: Option[String]
19+
)
20+
21+
object PageEntry {
22+
private def createAcronym(s: String): Option[String] =
23+
s.headOption.map(firstLetter => firstLetter.toString ++ s.tail.filter(_.isUpper))
24+
25+
def apply(jsObj: PageEntryJS): PageEntry = PageEntry(
26+
jsObj.name,
27+
jsObj.description,
28+
jsObj.location,
29+
jsObj.searchKeys.head.toLowerCase,
30+
createAcronym(jsObj.searchKeys.head)
31+
)
32+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package dotty.dokka
2+
3+
class Searchbar {
4+
val pages = SearchbarGlobals.pages.toList.map(PageEntry.apply)
5+
val engine = SearchbarEngine(pages)
6+
val parser = QueryParser()
7+
val component = SearchbarComponent(q => engine.query(parser.parse(q)))
8+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package dotty.dokka
2+
3+
import org.scalajs.dom._
4+
import org.scalajs.dom.html.Input
5+
6+
class SearchbarComponent(val callback: (String) => List[PageEntry]):
7+
val resultsChunkSize = 100
8+
extension (p: PageEntry)
9+
def toHTML =
10+
val wrapper = document.createElement("div").asInstanceOf[html.Div]
11+
wrapper.classList.add("scala3doc-searchbar-result")
12+
wrapper.classList.add("monospace")
13+
14+
val resultA = document.createElement("a").asInstanceOf[html.Anchor]
15+
resultA.href = Globals.pathToRoot + p.location
16+
resultA.text = s"${p.fullName}"
17+
18+
val location = document.createElement("span")
19+
location.classList.add("pull-right")
20+
location.classList.add("scala3doc-searchbar-location")
21+
location.textContent = p.description
22+
23+
wrapper.appendChild(resultA)
24+
wrapper.appendChild(location)
25+
wrapper
26+
27+
def handleNewQuery(query: String) =
28+
val result = callback(query).map(_.toHTML)
29+
resultsDiv.scrollTop = 0
30+
while (resultsDiv.hasChildNodes()) resultsDiv.removeChild(resultsDiv.lastChild)
31+
val fragment = document.createDocumentFragment()
32+
result.take(resultsChunkSize).foreach(fragment.appendChild)
33+
resultsDiv.appendChild(fragment)
34+
def loadMoreResults(result: List[raw.HTMLElement]): Unit = {
35+
resultsDiv.onscroll = (event: Event) => {
36+
if (resultsDiv.scrollHeight - resultsDiv.scrollTop == resultsDiv.clientHeight)
37+
{
38+
val fragment = document.createDocumentFragment()
39+
result.take(resultsChunkSize).foreach(fragment.appendChild)
40+
resultsDiv.appendChild(fragment)
41+
loadMoreResults(result.drop(resultsChunkSize))
42+
}
43+
}
44+
}
45+
loadMoreResults(result.drop(resultsChunkSize))
46+
47+
private val searchIcon: html.Div =
48+
val span = document.createElement("span").asInstanceOf[html.Span]
49+
span.innerHTML = """<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"><path d="M19.64 18.36l-6.24-6.24a7.52 7.52 0 10-1.28 1.28l6.24 6.24zM7.5 13.4a5.9 5.9 0 115.9-5.9 5.91 5.91 0 01-5.9 5.9z"></path></svg>"""
50+
span.id = "scala3doc-search"
51+
span.onclick = (event: Event) =>
52+
if (document.body.contains(rootDiv)) {
53+
document.body.removeChild(rootDiv)
54+
}
55+
else document.body.appendChild(rootDiv)
56+
57+
val element = createNestingDiv("search-content")(
58+
createNestingDiv("search-container")(
59+
createNestingDiv("search")(
60+
span
61+
)
62+
)
63+
)
64+
document.getElementById("scala3doc-searchBar").appendChild(element)
65+
element
66+
67+
68+
private val input: html.Input =
69+
val element = document.createElement("input").asInstanceOf[html.Input]
70+
element.id = "scala3doc-searchbar-input"
71+
element.addEventListener("input", (e) => handleNewQuery(e.target.asInstanceOf[html.Input].value))
72+
element
73+
74+
private val resultsDiv: html.Div =
75+
val element = document.createElement("div").asInstanceOf[html.Div]
76+
element.id = "scala3doc-searchbar-results"
77+
element
78+
79+
private val rootHiddenClasses = "hidden"
80+
private val rootShowClasses = ""
81+
82+
private def createNestingDiv(className: String)(innerElement: html.Element): html.Div =
83+
val element = document.createElement("div").asInstanceOf[html.Div]
84+
element.className = className
85+
element.appendChild(innerElement)
86+
element
87+
88+
private val rootDiv: html.Div =
89+
val element = document.createElement("div").asInstanceOf[html.Div]
90+
element.addEventListener("mousedown", (e: Event) => e.stopPropagation())
91+
searchIcon.addEventListener("mousedown", (e: Event) => e.stopPropagation())
92+
document.body.addEventListener("mousedown", (e: Event) =>
93+
if (document.body.contains(element)) {
94+
document.body.removeChild(element)
95+
}
96+
)
97+
element.id = "scala3doc-searchbar"
98+
element.appendChild(input)
99+
element.appendChild(resultsDiv)
100+
element
101+
102+
handleNewQuery("")
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package dotty.dokka
2+
3+
import scala.scalajs.js
4+
import scala.scalajs.js.annotation.JSGlobalScope
5+
6+
@js.native
7+
@JSGlobalScope
8+
object SearchbarGlobals extends js.Object {
9+
val pages: js.Array[PageEntryJS] = js.native
10+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package dotty.dokka
2+
3+
enum Matchers extends Function1[PageEntry, Int]:
4+
case ByName(query: String)
5+
case ByKind(kind: String)
6+
7+
def apply(p: PageEntry): Int = this match {
8+
case ByName(query) => {
9+
val nameOption = Option(p.shortName)
10+
val acronym = p.acronym
11+
//Edge case for empty query string
12+
if query == "" then 1
13+
else {
14+
val results = List(
15+
nameOption.filter(_.contains(query.toLowerCase)).fold(-1)(_.size - query.size),
16+
acronym.filter(_.contains(query)).fold(-1)(_.size - query.size + 1)
17+
)
18+
if results.forall(_ == -1) then -1 else results.filter(_ != -1).min
19+
}
20+
}
21+
case ByKind(kind) => p.fullName.split(" ").headOption.filter(_.equalsIgnoreCase(kind)).fold(-1)(_ => 1)
22+
}
23+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package dotty.dokka
2+
3+
import scala.util.matching.Regex._
4+
import scala.util.matching._
5+
6+
class QueryParser:
7+
val kinds = Seq(
8+
"class",
9+
"trait",
10+
"enum",
11+
"object",
12+
"def",
13+
"val",
14+
"var",
15+
"package",
16+
"given",
17+
"type"
18+
)
19+
val kindRegex = ("(?i)" + kinds.mkString("(","|",")") + " (.*)").r
20+
val restRegex = raw"(.*)".r
21+
val escapedRegex = raw"`(.*)`".r
22+
23+
def parse(query: String): List[Matchers] = query match {
24+
case escapedRegex(rest) => List(Matchers.ByName(rest))
25+
case kindRegex(kind, rest) => List(Matchers.ByKind(kind)) ++ parse(rest)
26+
case restRegex(name) => List(Matchers.ByName(name))
27+
case _ => List()
28+
}

0 commit comments

Comments
 (0)