Skip to content

Commit

Permalink
render descriptions on channel page with Markdown syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
memo33 committed Nov 30, 2024
1 parent fa7acde commit d8b1f81
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 23 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
- improved error message if channel-build fails randomly in case old files could not be removed (#6)
- improved `sc4pac` bash script to allow symlinking into path on Linux/macOS
- The progress spinner animation was switched to ASCII symbols for compatibility with non-Unicode fonts in some terminals.
- The metadata text fields like `description` etc. are now rendered as Markdown.
For correct text wrapping, multiline text blocks should start with `|` instead of `>`, from now on.


## [0.4.5] - 2024-10-17
Expand Down
14 changes: 11 additions & 3 deletions channel-testing/yaml/templates/package-template-basic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,23 @@ assets: # Optional list of assets from which to extract files (zero or more)
# Additional descriptive information, entirely optional.
info:
summary: "One-line summary"
warning: >
warning: |
Special instructions to display before installation, such as bulldozing requirements.
conflicts: "Notes about incompatibilities"
description: >
description: |
Longer text with some details.
Yet more text…
Use syntax `pkg=group:name` to reference packages in description text.
Use the syntax `pkg=group:name` to reference packages in description text.
### Markdown
Common **Markdown** syntaxes like *emphasis*, [URLs](https://memo33.github.io/sc4pac/)
or lists are supported:
- one
- two
- three
author: "" # original author of the content, if useful
images: [ "img1.png", "img2.png" ]
website: "example.org"
Expand Down
2 changes: 2 additions & 0 deletions web/channel/index-dev.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
</li>
</ul>
</nav>
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.min.js"></script>
<script type="text/javascript" src="../target/scala-3.4.2/sc4pac-web-fastopt/main.js"></script>
</body>
</html>
2 changes: 2 additions & 0 deletions web/channel/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
</li>
</ul>
</nav>
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.min.js"></script>
<script type="text/javascript" src="main.js"></script>
</body>
</html>
55 changes: 35 additions & 20 deletions web/src/main/scala/sc4pac/web/ChannelPage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,26 @@ object JsonData extends SharedData {
parseModule(_).left.map(e => throw new IllegalArgumentException(e)).merge)
}

@js.native
@js.annotation.JSGlobal
object DOMPurify extends js.Object {
def sanitize(text: String): String = js.native
}

@js.native
@js.annotation.JSGlobal("marked")
object Marked extends js.Object {
def parse(text: String): String = js.native
def use(extensions: js.Any*): Unit = js.native
}

@js.native
trait TokensCodespan extends js.Object { // see https://github.com/markedjs/marked/blob/7e4e3435eaca2b0f48f5aedb53d67a36170086f3/src/Tokens.ts#L57
val `type`: String // "codespan"
val raw: String // with quotes
val text: String // without quotes
}

object ChannelPage {

// val channelUrl = "http://localhost:8090/channel/"
Expand Down Expand Up @@ -88,38 +108,32 @@ object ChannelPage {
else H.code(module.orgName)

/** Highlights syntax `pkg=group:name` in metadata description text. */
def applyMarkdownPkg(line: String): H.Frag = {
val builder = Seq.newBuilder[H.Frag]
var idx = 0
for (m <- BareModule.pkgMarkdownRegex.findAllMatchIn(line)) {
if (idx < m.start) {
builder += line.substring(idx, m.start)
}
builder += pkgNameFrag(BareModule(Organization(m.group(1)), ModuleName(m.group(2))), link = true)
idx = m.end
}
if (idx < line.length) {
builder += line.substring(idx, line.length)
}
builder.result()
def renderCodespan(codespan: TokensCodespan): String = codespan.raw match
case BareModule.pkgMarkdownRegex(group, name) =>
val module = BareModule(Organization(group), ModuleName(name))
// a raw re-implementation of pkgNameFrag
s"""<code class="code-left">${module.group.value}:</code><a href="?pkg=${module.orgName}"><code class="code-right">${module.name.value}</code></a>"""
case _ => s"<code>${codespan.text}</code>"

def markdownFrag(text: String): H.Frag = {
H.raw(DOMPurify.sanitize(Marked.parse(text)))
}

def pkgInfoFrag(pkg: JsonData.Package) = {
val module = pkg.toBareDep
val b = Seq.newBuilder[H.Frag]
def add(label: String, child: H.Frag): Unit =
b += H.tr(H.th(label), H.td(child))
def mkPar(text: String) = text.trim.linesIterator.map(line => H.p(applyMarkdownPkg(line))).toSeq

// add("Name", pkg.name)
// add("Group", pkg.group)
add("Version", pkg.version)
add("Summary", if (pkg.info.summary.nonEmpty) applyMarkdownPkg(pkg.info.summary) else "-")
add("Summary", if (pkg.info.summary.nonEmpty) markdownFrag(pkg.info.summary) else "-")
if (pkg.info.description.nonEmpty)
add("Description", mkPar(pkg.info.description))
add("Description", markdownFrag(pkg.info.description))
if (pkg.info.warning.nonEmpty)
add("Warning", mkPar(pkg.info.warning))
add("Conflicts", if (pkg.info.conflicts.isEmpty) "None" else mkPar(pkg.info.conflicts))
add("Warning", markdownFrag(pkg.info.warning))
add("Conflicts", if (pkg.info.conflicts.isEmpty) "None" else markdownFrag(pkg.info.conflicts))
if (pkg.info.author.nonEmpty)
add("Author", pkg.info.author)
if (pkg.info.website.nonEmpty) {
Expand Down Expand Up @@ -193,7 +207,7 @@ object ChannelPage {
H.table(H.id := "channelcontents")(H.tbody(items.flatMap { item =>
if (displayCategory2.isEmpty || item.category == displayCategory2) { // either show all or filter by category
item.toBareDep match
case mod: BareModule => Some(H.tr(H.td(pkgNameFrag(mod, link = true)), H.td(applyMarkdownPkg(item.summary))))
case mod: BareModule => Some(H.tr(H.td(pkgNameFrag(mod, link = true)), H.td(markdownFrag(item.summary))))
case _: BareAsset => None
} else {
None
Expand All @@ -203,6 +217,7 @@ object ChannelPage {
}

def setupUI(): Unit = {
Marked.use(js.Dictionary("renderer" -> js.Dictionary("codespan" -> (renderCodespan: js.Function))))
val urlParams = new dom.URLSearchParams(dom.window.location.search)
val pkgName = urlParams.get("pkg")
if (pkgName == null) {
Expand Down

0 comments on commit d8b1f81

Please sign in to comment.