Skip to content

Commit 99c2b96

Browse files
authored
Merge pull request #14595 from dotty-staging/scaladoc/js-html-utils
Add JS DOM utils to Scaladoc-js
2 parents 90766d4 + 39e3fcf commit 99c2b96

File tree

7 files changed

+290
-300
lines changed

7 files changed

+290
-300
lines changed

project/Build.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1279,7 +1279,7 @@ object Build {
12791279

12801280
lazy val `scaladoc-js-contributors` = project.in(file("scaladoc-js/contributors")).
12811281
enablePlugins(DottyJSPlugin).
1282-
dependsOn(`scala3-library-bootstrappedJS`).
1282+
dependsOn(`scaladoc-js-common`).
12831283
settings(
12841284
Test / fork := false,
12851285
scalaJSUseMainModuleInitializer := true,

scaladoc-js/common/src/code-snippets/CodeSnippets.scala

Lines changed: 45 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import scala.scalajs.js
44
import org.scalajs.dom._
55
import org.scalajs.dom.ext._
66

7+
import utils.HTML._
8+
import scala.util.chaining._
9+
710
import CodeSnippetsGlobals._
811

912
class CodeSnippets:
@@ -35,22 +38,13 @@ class CodeSnippets:
3538
case _ =>
3639
}
3740
def createShowHideButton(toggleRoot: html.Element) = {
38-
val div = document.createElement("div")
39-
div.classList.add("snippet-showhide")
40-
val p = document.createElement("p")
41-
p.textContent = "Show collapsed lines"
42-
val showHideButton = document.createElement("label")
43-
showHideButton.classList.add("snippet-showhide-button")
44-
val checkbox = document.createElement("input").asInstanceOf[html.Input]
45-
checkbox.`type` = "checkbox"
46-
val slider = document.createElement("span")
47-
slider.classList.add("slider")
48-
showHideButton.appendChild(checkbox)
49-
showHideButton.appendChild(slider)
50-
checkbox.addEventListener("change", _ => toggleHide(toggleRoot))
51-
div.appendChild(showHideButton)
52-
div.appendChild(p)
53-
div
41+
div(cls := "snippet-showhide")(
42+
label(cls := "snippet-showhide-button")(
43+
input("type" := "checkbox").tap(_.addEventListener("change", _ => toggleHide(toggleRoot))),
44+
span(cls := "slider")
45+
),
46+
p("Show collapsed lines")
47+
)
5448
}
5549

5650
toggleHide(snippet)
@@ -65,8 +59,7 @@ class CodeSnippets:
6559
private def snippetAnchor(snippet: html.Element): Unit = snippet.querySelector(".snippet-meta .snippet-label") match {
6660
case e: html.Element =>
6761
val name = e.textContent.trim
68-
val anchor = document.createElement("a").asInstanceOf[html.Anchor]
69-
anchor.id = s"snippet-$name"
62+
val anchor = a(id := s"snippet-$name")
7063
snippet.insertBefore(anchor, snippet.firstChild)
7164
case _ =>
7265
}
@@ -75,54 +68,42 @@ class CodeSnippets:
7568
val included = snippet.querySelectorAll("code span.include")
7669
val pre = snippet.querySelector("pre")
7770
if included != null && included.nonEmpty && pre != null then {
78-
val includesDiv = document.createElement("div")
79-
includesDiv.classList.add("included-section")
80-
includesDiv.classList.add("hideable")
81-
included
71+
val includes = included
8272
.collect { case e: html.Element => e }
8373
.toList
8474
.filter(_.hasAttribute("name"))
8575
.map(_.getAttribute("name"))
8676
.distinct
8777
.map { name =>
88-
val a = document.createElement("a").asInstanceOf[html.Anchor]
89-
a.classList.add("unselectable")
90-
a.href = s"#snippet-$name"
91-
a.innerHTML = s"included <b>$name</b>"
92-
a
78+
a(cls := "unselectable", href := s"#snippet-$name")(
79+
"included",
80+
b(name)
81+
)
9382
}
94-
.foreach(a => includesDiv.appendChild(a))
83+
84+
val includesDiv = div(cls := "included-section hideable")(includes)
9585

9686
snippet.insertBefore(includesDiv, pre)
9787
}
9888
}
9989

10090
private def copyRunButtons(snippet: html.Element) = {
10191
def copyButton = {
102-
val div = document.createElement("div")
103-
val button = document.createElement("button")
104-
val icon = document.createElement("i")
105-
icon.classList.add("far")
106-
icon.classList.add("fa-clone")
107-
button.appendChild(icon)
108-
button.classList.add("copy-button")
109-
button.addEventListener("click", _ => {
110-
val code = snippet.querySelectorAll("code>span:not(.hidden)")
111-
.map(_.textContent)
112-
.mkString
113-
window.navigator.clipboard.writeText(code)
114-
})
115-
div.appendChild(button)
116-
div
92+
div(
93+
button(cls := "copy-button")(
94+
i(cls := "far fa-clone")
95+
).tap(_.addEventListener("click", _ => {
96+
val code = snippet.querySelectorAll("code>span:not(.hidden)")
97+
.map(_.textContent)
98+
.mkString
99+
window.navigator.clipboard.writeText(code)
100+
}))
101+
)
117102
}
118103
def runButton = {
119-
val div = document.createElement("div").asInstanceOf[html.Div]
120-
val runButton = document.createElement("button").asInstanceOf[html.Button]
121-
val runIcon = document.createElement("i")
122-
runIcon.classList.add("fas")
123-
runIcon.classList.add("fa-play")
124-
runButton.classList.add("run-button")
125-
runButton.appendChild(runIcon)
104+
val runButton = button(cls := "run-button")(
105+
i(cls := "fas fa-play")
106+
)
126107

127108
runButton.addEventListener("click", _ =>
128109
if !runButton.hasAttribute("opened") then {
@@ -148,18 +129,14 @@ class CodeSnippets:
148129
}
149130
)
150131

151-
div.appendChild(runButton)
152-
div
132+
div(runButton)
153133
}
154134
def exitButton = {
155-
val div = document.createElement("div").asInstanceOf[html.Div]
156-
val exitButton = document.createElement("button").asInstanceOf[html.Element]
157-
val exitIcon = document.createElement("i")
158-
exitIcon.classList.toggle("fas")
159-
exitIcon.classList.toggle("fa-times")
160-
exitButton.classList.add("exit-button")
161-
div.style = "display:none;"
162-
exitButton.appendChild(exitIcon)
135+
val exitButton = button(cls := "exit-button")(
136+
i(cls := "fas fa-times")
137+
)
138+
139+
val bdiv = div(style := "display:none;")(exitButton)
163140

164141
exitButton.addEventListener("click", _ =>
165142
snippet.querySelector("pre") match {
@@ -178,22 +155,16 @@ class CodeSnippets:
178155
case btn: html.Element => btn.parentElement.style = "display:none;"
179156
case _ =>
180157
}
181-
div.style = "display:none;"
158+
bdiv.style = "display:none;"
182159
)
183160

184-
div.appendChild(exitButton)
185-
div
161+
bdiv
186162
}
187-
def toScastieButton = {
188-
val div = document.createElement("div").asInstanceOf[html.Div]
189-
val toScastieButton = document.createElement("button").asInstanceOf[html.Element]
190-
val toScastieIcon = document.createElement("i").asInstanceOf[html.Image]
191163

192-
toScastieIcon.classList.add("fas")
193-
toScastieIcon.classList.add("fa-external-link-alt")
194-
toScastieButton.classList.add("to-scastie-button")
195-
div.style = "display:none;"
196-
toScastieButton.appendChild(toScastieIcon)
164+
def toScastieButton = {
165+
val toScastieButton = button(cls := "to-scastie-button")(
166+
i(cls := "fas fa-external-link-alt")
167+
)
197168

198169
toScastieButton.addEventListener("click", _ =>
199170
snippet.querySelector(".embedded-menu li.logo") match {
@@ -202,9 +173,9 @@ class CodeSnippets:
202173
}
203174
)
204175

205-
div.appendChild(toScastieButton)
206-
div
176+
div("style" := "display:none;")(toScastieButton)
207177
}
178+
208179
val buttonsSection = getButtonsSection(snippet)
209180
buttonsSection.foreach(s =>
210181
s.appendChild(copyButton)
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package dotty.tools.scaladoc
2+
package utils
3+
4+
import scala.scalajs.js
5+
import org.scalajs.dom.{html => domhtml, _}
6+
7+
object HTML {
8+
type TagArg = domhtml.Element | Seq[domhtml.Element | String] | String
9+
10+
type AttrArg = AppliedAttr | Seq[AppliedAttr]
11+
12+
case class Tag[T <: domhtml.Element](private val elemFactory: () => T):
13+
private def textNode(s: String): Text = document.createTextNode(s)
14+
15+
def apply(tags: TagArg*): T = apply()(tags:_*)
16+
def apply(first: AttrArg, rest: AttrArg*): T = apply((first +: rest):_*)()
17+
def apply(attrs: AttrArg*)(tags: TagArg*): T =
18+
val elem: T = elemFactory()
19+
def unpackTags(tags: TagArg*): Unit = tags.foreach {
20+
case e: domhtml.Element => elem.appendChild(e)
21+
case s: String => elem.appendChild(textNode(s))
22+
case elemSeq: (Seq[domhtml.Element | String] @unchecked) => unpackTags(elemSeq*)
23+
}
24+
25+
def unpackAttributes(attrs: AttrArg*): Unit = attrs.foreach {
26+
case ("id", id) => elem.id = id
27+
case ("class", value) => value.split("\\s+").foreach(cls => elem.classList.add(cls))
28+
case (attr, value) => elem.setAttribute(attr, value)
29+
case s: Seq[AppliedAttr] => unpackAttributes(s*)
30+
}
31+
32+
unpackTags(tags:_*)
33+
unpackAttributes(attrs:_*)
34+
elem
35+
36+
object Tag:
37+
def apply[T <: domhtml.Element](s: String): Tag[T] =
38+
Tag[T](() => document.createElement(s).asInstanceOf[T])
39+
40+
extension (s: String) def escapeReservedTokens: String =
41+
s.replace("&", "&amp;")
42+
.replace("<", "&lt;")
43+
.replace(">", "&gt;")
44+
.replace("\"", "&quot;")
45+
.replace("'", "&apos;")
46+
47+
case class Attr(name: String):
48+
def :=(value: String): AppliedAttr = (name, value)
49+
50+
extension (key: String) def :=(value: String): AppliedAttr =
51+
(key, value)
52+
53+
opaque type AppliedAttr = (String, String)
54+
55+
val div = Tag[domhtml.Div]("div")
56+
val span = Tag[domhtml.Span]("span")
57+
val a = Tag[domhtml.Anchor]("a")
58+
val p = Tag[domhtml.Paragraph]("p")
59+
val h1 = Tag[domhtml.Heading]("h1")
60+
val h2 = Tag[domhtml.Heading]("h2")
61+
val h3 = Tag[domhtml.Heading]("h3")
62+
val h4 = Tag[domhtml.Heading]("h4")
63+
val h5 = Tag[domhtml.Heading]("h5")
64+
val h6 = Tag[domhtml.Heading]("h6")
65+
val dl = Tag[domhtml.DList]("dl")
66+
val dd = Tag[domhtml.Element]("dd")
67+
val dt = Tag[domhtml.Element]("dt")
68+
val svg = Tag[domhtml.Element]("svg")
69+
val button = Tag[domhtml.Button]("button")
70+
val input = Tag[domhtml.Input]("input")
71+
val label = Tag[domhtml.Label]("label")
72+
val script = Tag[domhtml.Script]("script")
73+
val link = Tag[domhtml.Link]("link")
74+
val footer = Tag[domhtml.Element]("footer")
75+
val htmlelem = Tag[domhtml.Html]("html")
76+
val head = Tag[domhtml.Head]("head")
77+
val meta = Tag[domhtml.Element]("meta")
78+
val main = Tag[domhtml.Element]("main")
79+
val title = Tag[domhtml.Title]("title")
80+
val body = Tag[domhtml.Body]("body")
81+
val nav = Tag[domhtml.Element]("nav")
82+
val img = Tag[domhtml.Image]("img")
83+
val ul = Tag[domhtml.UList]("ul")
84+
val ol = Tag[domhtml.OList]("ol")
85+
val li = Tag[domhtml.LI]("li")
86+
val code = Tag[domhtml.Element]("code")
87+
val pre = Tag[domhtml.Pre]("pre")
88+
val table = Tag[domhtml.Table]("table")
89+
val thead = Tag[domhtml.Element]("thead")
90+
val tbody = Tag[domhtml.Element]("tbody")
91+
val th = Tag[domhtml.TableCell]("th")
92+
val tr = Tag[domhtml.TableRow]("tr")
93+
val td = Tag[domhtml.TableCell]("td")
94+
val b = Tag[domhtml.Element]("b")
95+
val i = Tag[domhtml.Element]("i")
96+
97+
val cls = Attr("class")
98+
val href = Attr("href")
99+
val style = Attr("style")
100+
val id = Attr("id")
101+
val `type` = Attr("type")
102+
val placeholder = Attr("placeholder")
103+
val defer = Attr("defer")
104+
val src = Attr("src")
105+
val rel = Attr("rel")
106+
val charset = Attr("charset")
107+
val name = Attr("name")
108+
val content = Attr("content")
109+
val testId = Attr("data-test-id")
110+
val alt = Attr("alt")
111+
val value = Attr("value")
112+
val onclick=Attr("onclick")
113+
val titleAttr =Attr("title")
114+
val onkeyup = Attr("onkeyup")
115+
116+
}

scaladoc-js/contributors/src/content-contributors/ContentContributors.scala

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import scala.concurrent.ExecutionContext.Implicits.global
1313
import scala.concurrent.Future
1414
import scala.util.{Success,Failure}
1515

16+
import utils.HTML._
17+
1618
// Contributors widget
1719
// see https://stackoverflow.com/a/19200303/4496364
1820
// Copied from https://github.com/scala/docs.scala-lang/blob/main/resources/js/functions.js and rewritten to Scala.js
@@ -90,21 +92,17 @@ class ContentContributors:
9092
getAuthorsForFilename(Globals.githubContributorsFilename.stripPrefix("/")).onComplete {
9193
case Success(authors) =>
9294
val maybeDiv = Option(document.getElementById("documentation-contributors"))
93-
maybeDiv.foreach { div =>
94-
authors.foreach { case FullAuthor(name, url, img) =>
95-
val divN = document.createElement("div")
96-
val imgN = document.createElement("img").asInstanceOf[html.Image]
97-
imgN.src = img
98-
val autN = document.createElement("a").asInstanceOf[html.Anchor]
99-
autN.href = url
100-
autN.text = name
101-
divN.appendChild(imgN)
102-
divN.appendChild(autN)
103-
div.appendChild(divN)
95+
maybeDiv.foreach { mdiv =>
96+
authors.foreach { case FullAuthor(name, url, imgUrl) =>
97+
val inner = div(
98+
img(src := imgUrl)(),
99+
a(href := url)(name)
100+
)
101+
mdiv.appendChild(inner)
104102
}
105103

106104
if authors.nonEmpty then
107-
div.asInstanceOf[html.Div].parentElement.classList.toggle("hidden")
105+
mdiv.asInstanceOf[html.Div].parentElement.classList.toggle("hidden")
108106
}
109107
case Failure(err) =>
110108
println(s"Couldn't fetch contributors. $err")

0 commit comments

Comments
 (0)