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

render descriptions on channel page with Markdown syntax #15

Merged
merged 2 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
2 changes: 1 addition & 1 deletion web/channel/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ a.btn:hover {
background-color: #707070;
text-decoration: none;
}
#pkginfo ul {
ul.unstyled-list {
list-style: none;
margin: 0;
padding: 0;
Expand Down
61 changes: 38 additions & 23 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 All @@ -134,7 +148,7 @@ object ChannelPage {
if (pkg.variants.length == 1 && pkg.variants.head.variant.isEmpty)
"None"
else
H.ul(
H.ul(H.cls := "unstyled-list")(
pkg.variants.map { vd =>
H.li(variantFrag(vd.variant, pkg.variantDescriptions))
}
Expand All @@ -147,12 +161,12 @@ object ChannelPage {
.distinct
add("Dependencies",
if (deps.isEmpty) "None"
else H.ul(deps.map(dep => H.li(pkgNameFrag(dep))))
else H.ul(H.cls := "unstyled-list")(deps.map(dep => H.li(pkgNameFrag(dep))))
)

add("Required By",
if (pkg.info.requiredBy.isEmpty) "None"
else H.ul(pkg.info.requiredBy.map(dep => H.li(pkgNameFrag(dep))))
else H.ul(H.cls := "unstyled-list")(pkg.info.requiredBy.map(dep => H.li(pkgNameFrag(dep))))
)

H.div(
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
Loading