~jan0sch/smederee
Showing details for patch ef026d73e765585726fc31da8b4e61b59e013787.
diff -rN -u old-smederee/build.sbt new-smederee/build.sbt --- old-smederee/build.sbt 2025-01-11 17:45:25.731881668 +0000 +++ new-smederee/build.sbt 2025-01-11 17:45:25.731881668 +0000 @@ -110,14 +110,9 @@ scalacOptions += "-Xfatal-warnings", libraryDependencies ++= Seq( library.catsCore, - library.commonMark, - library.commonMarkExtHeadingAnchor, - library.commonMarkExtImageAttrs, - library.commonMarkExtStrikethrough, - library.commonMarkExtTables, - library.commonMarkExtTaskListItems, library.http4sCore, library.ip4sCore, + library.laikaCore, library.logback, library.munit % Test, library.munitScalaCheck % Test, @@ -282,7 +277,6 @@ val cats = "2.12.0" val catsEffect = "3.5.4" val circe = "0.14.7" - val commonMark = "0.22.0" val doobie = "1.0.0-RC5" val flyway = "10.13.0" val fs2 = "3.5.0" @@ -290,6 +284,7 @@ val ip4s = "3.5.0" val jansi = "2.4.2" val jclOverSlf4j = "2.0.13" + val laika = "1.1.0" val log4cats = "2.7.0" val logback = "1.5.6" val munit = "1.0.0" @@ -301,48 +296,43 @@ val simpleJavaMail = "8.11.1" val springSecurity = "6.3.0" } - val apacheSshdCore = "org.apache.sshd" % "sshd-core" % Version.apacheSshd - val apacheSshdSftp = "org.apache.sshd" % "sshd-sftp" % Version.apacheSshd - val apacheSshdScp = "org.apache.sshd" % "sshd-scp" % Version.apacheSshd - val bouncyCastleProvider = "org.bouncycastle" % "bcprov-jdk15to18" % Version.bouncyCastle - val catsCore = "org.typelevel" %% "cats-core" % Version.cats - val catsEffect = "org.typelevel" %% "cats-effect" % Version.catsEffect - val circeCore = "io.circe" %% "circe-core" % Version.circe - val circeGeneric = "io.circe" %% "circe-generic" % Version.circe - val circeParser = "io.circe" %% "circe-parser" % Version.circe - val commonMark = "org.commonmark" % "commonmark" % Version.commonMark - val commonMarkExtHeadingAnchor = "org.commonmark" % "commonmark-ext-heading-anchor" % Version.commonMark - val commonMarkExtImageAttrs = "org.commonmark" % "commonmark-ext-image-attributes" % Version.commonMark - val commonMarkExtStrikethrough = "org.commonmark" % "commonmark-ext-gfm-strikethrough" % Version.commonMark - val commonMarkExtTables = "org.commonmark" % "commonmark-ext-gfm-tables" % Version.commonMark - val commonMarkExtTaskListItems = "org.commonmark" % "commonmark-ext-task-list-items" % Version.commonMark - val doobieCore = "org.tpolecat" %% "doobie-core" % Version.doobie - val doobieHikari = "org.tpolecat" %% "doobie-hikari" % Version.doobie - val doobiePostgres = "org.tpolecat" %% "doobie-postgres" % Version.doobie - val doobieScalaTest = "org.tpolecat" %% "doobie-scalatest" % Version.doobie - val ed25519Java = "net.i2p.crypto" % "eddsa" % "0.3.0" - val flywayCore = "org.flywaydb" % "flyway-core" % Version.flyway - val flywayPostgreSQL = "org.flywaydb" % "flyway-database-postgresql" % Version.flyway - val fs2Core = "co.fs2" %% "fs2-core" % Version.fs2 - val fs2IO = "co.fs2" %% "fs2-io" % Version.fs2 - val http4sCirce = "org.http4s" %% "http4s-circe" % Version.http4s - val http4sCore = "org.http4s" %% "http4s-core" % Version.http4s - val http4sDsl = "org.http4s" %% "http4s-dsl" % Version.http4s - val http4sEmberServer = "org.http4s" %% "http4s-ember-server" % Version.http4s - val http4sEmberClient = "org.http4s" %% "http4s-ember-client" % Version.http4s - val ip4sCore = "com.comcast" %% "ip4s-core" % Version.ip4s - val jansi = "com.github.Osiris-Team" % "jansi" % Version.jansi - val jclOverSlf4j = "org.slf4j" % "jcl-over-slf4j" % Version.jclOverSlf4j - val log4catsSlf4j = "org.typelevel" %% "log4cats-slf4j" % Version.log4cats - val logback = "ch.qos.logback" % "logback-classic" % Version.logback - val munit = "org.scalameta" %% "munit" % Version.munit - val munitCatsEffect = "org.typelevel" %% "munit-cats-effect" % Version.munitCatsEffect - val munitScalaCheck = "org.scalameta" %% "munit-scalacheck" % Version.munit - val osLib = "com.lihaoyi" %% "os-lib" % Version.osLib - val postgresql = "org.postgresql" % "postgresql" % Version.postgresql - val pureConfig = "com.github.pureconfig" %% "pureconfig-core" % Version.pureConfig - val scalaCheck = "org.scalacheck" %% "scalacheck" % Version.scalaCheck - val simpleJavaMail = "org.simplejavamail" % "simple-java-mail" % Version.simpleJavaMail + val apacheSshdCore = "org.apache.sshd" % "sshd-core" % Version.apacheSshd + val apacheSshdSftp = "org.apache.sshd" % "sshd-sftp" % Version.apacheSshd + val apacheSshdScp = "org.apache.sshd" % "sshd-scp" % Version.apacheSshd + val bouncyCastleProvider = "org.bouncycastle" % "bcprov-jdk15to18" % Version.bouncyCastle + val catsCore = "org.typelevel" %% "cats-core" % Version.cats + val catsEffect = "org.typelevel" %% "cats-effect" % Version.catsEffect + val circeCore = "io.circe" %% "circe-core" % Version.circe + val circeGeneric = "io.circe" %% "circe-generic" % Version.circe + val circeParser = "io.circe" %% "circe-parser" % Version.circe + val doobieCore = "org.tpolecat" %% "doobie-core" % Version.doobie + val doobieHikari = "org.tpolecat" %% "doobie-hikari" % Version.doobie + val doobiePostgres = "org.tpolecat" %% "doobie-postgres" % Version.doobie + val doobieScalaTest = "org.tpolecat" %% "doobie-scalatest" % Version.doobie + val ed25519Java = "net.i2p.crypto" % "eddsa" % "0.3.0" + val flywayCore = "org.flywaydb" % "flyway-core" % Version.flyway + val flywayPostgreSQL = "org.flywaydb" % "flyway-database-postgresql" % Version.flyway + val fs2Core = "co.fs2" %% "fs2-core" % Version.fs2 + val fs2IO = "co.fs2" %% "fs2-io" % Version.fs2 + val http4sCirce = "org.http4s" %% "http4s-circe" % Version.http4s + val http4sCore = "org.http4s" %% "http4s-core" % Version.http4s + val http4sDsl = "org.http4s" %% "http4s-dsl" % Version.http4s + val http4sEmberServer = "org.http4s" %% "http4s-ember-server" % Version.http4s + val http4sEmberClient = "org.http4s" %% "http4s-ember-client" % Version.http4s + val ip4sCore = "com.comcast" %% "ip4s-core" % Version.ip4s + val jansi = "com.github.Osiris-Team" % "jansi" % Version.jansi + val jclOverSlf4j = "org.slf4j" % "jcl-over-slf4j" % Version.jclOverSlf4j + val laikaCore = "org.typelevel" %% "laika-core" % Version.laika + val log4catsSlf4j = "org.typelevel" %% "log4cats-slf4j" % Version.log4cats + val logback = "ch.qos.logback" % "logback-classic" % Version.logback + val munit = "org.scalameta" %% "munit" % Version.munit + val munitCatsEffect = "org.typelevel" %% "munit-cats-effect" % Version.munitCatsEffect + val munitScalaCheck = "org.scalameta" %% "munit-scalacheck" % Version.munit + val osLib = "com.lihaoyi" %% "os-lib" % Version.osLib + val postgresql = "org.postgresql" % "postgresql" % Version.postgresql + val pureConfig = "com.github.pureconfig" %% "pureconfig-core" % Version.pureConfig + val scalaCheck = "org.scalacheck" %% "scalacheck" % Version.scalaCheck + val simpleJavaMail = "org.simplejavamail" % "simple-java-mail" % Version.simpleJavaMail val springSecurityCrypto = "org.springframework.security" % "spring-security-crypto" % Version.springSecurity } diff -rN -u old-smederee/modules/html-utils/src/main/scala/de/smederee/html/ContentRenderer.scala new-smederee/modules/html-utils/src/main/scala/de/smederee/html/ContentRenderer.scala --- old-smederee/modules/html-utils/src/main/scala/de/smederee/html/ContentRenderer.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/html-utils/src/main/scala/de/smederee/html/ContentRenderer.scala 2025-01-11 17:45:25.735881676 +0000 @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.html + +import java.util.Locale + +import cats.Eq +import cats.data.* +import cats.syntax.all.* +import laika.api.MarkupParser +import laika.api.Renderer +import laika.ast.* +import laika.ast.InternalTarget.Resolved +import laika.config.LinkValidation +import laika.config.MessageFilter +import laika.format.HTML +import laika.format.Markdown +import laika.format.ReStructuredText +import org.http4s.Uri +import org.slf4j.LoggerFactory + +import scala.util.matching.Regex + +/** An enumeration of content that may be passed into a [[ContentRenderer]]. + */ +enum RenderableContent { + case Markdown + case ReStructuredText +} + +object RenderableContent { + given Eq[RenderableContent] = Eq.fromUniversalEquals + + private val mappedExtensions: Map[RenderableContent, NonEmptyList[String]] = Map( + Markdown -> NonEmptyList.of(".md", ".markdown"), + ReStructuredText -> NonEmptyList.of(".rst") + ) + + /** Parse the given URI that should point to a file that might be renderable and return the matching renderable + * content type. + * + * @param fileUri + * URI to a file that might be renderable. + * @return + * An option to the renderable content type of the file. + */ + def fromUri(fileUri: Uri): Option[RenderableContent] = { + val path = fileUri.path.toString.toLowerCase(Locale.ROOT) + mappedExtensions.find((_, extensions) => extensions.exists(ext => path.endsWith(ext))).map(_._1) + } +} + +object ContentRenderer { + private val log = LoggerFactory.getLogger(getClass) + + /** Render the given source using the given content type into HTML. + * + * @param localUrlPathPrefix + * An optional URL path that shall be prepended before any local (relative) links. + * @param contentType + * The renderable content type that defines which renderer implementation will be used. + * @param source + * Markup source code that shall be rendered. + * @return + * Either the string containing the rendered HTML or an error message. + */ + def render( + localUrlPathPrefix: Option[Uri.Path] + )(contentType: RenderableContent)(source: String): Either[String, String] = { + val parserBuilder = contentType match { + case RenderableContent.Markdown => + log.debug("Rendering markdown.") + MarkupParser.of(Markdown).using(Markdown.GitHubFlavor).failOnMessages(MessageFilter.None) + case RenderableContent.ReStructuredText => + log.debug("Rendering reStructuredText") + MarkupParser.of(ReStructuredText).failOnMessages(MessageFilter.None) + } + val config = parserBuilder.config.withConfigValue(LinkValidation.Local) + val parser = parserBuilder.build + + @SuppressWarnings(Array("scalafix:DisableSyntax.null")) + val renderer = Renderer + .of(HTML) + .withConfig(config) + .rendering { case (fmt, link: SpanLink) => + val modifiedLink = link.target match { + case Resolved(absolute, relative, formats) => + log.debug(s"Found possible to correct local URI: $absolute, $relative, $formats") + val renderedLink = link.target.render(internalTargetsAbsolute = false) + localUrlPathPrefix.match { + case None => link + case Some(pathPrefix) => + if (renderedLink.startsWith(pathPrefix.toString)) + link + else { + val correctedLink = + link.withTarget(Target.parse(pathPrefix.toString |+| "/" |+| renderedLink)) + log.debug(s"Corrected URI for local link rendering: $link -> $correctedLink") + correctedLink + } + } + case _ => + link + } + fmt.element("a", modifiedLink, "href" -> modifiedLink.target.render(internalTargetsAbsolute = false)) + } + .rendering { case (fmt, text: Text) => + ToDoTextCssMapping.isToDoItem.findFirstMatchIn(text.content) match { + case None => text.content + case Some(matchedItem) => + if (matchedItem.group(4) === null) { + val prefix = matchedItem.group(1) + val item = matchedItem.group(2) + val suffix = matchedItem.group(3) + log.debug( + s"Found TODO item: $text with extracted prefix: $prefix, item: $item and suffix: $suffix." + ) + val cssClass = ToDoTextCssMapping.todoCssClasses(item.toLowerCase(Locale.ROOT)) + fmt.text(prefix) |+| fmt.textElement( + "span", + text.copy(content = item |+| ":"), + "class" -> cssClass + ) |+| fmt.text(suffix) + } else { + val item = matchedItem.group(4) + log.debug(s"Found TODO item: $text with extracted item: $item.") + val cssClass = ToDoTextCssMapping.todoCssClasses(item.toLowerCase(Locale.ROOT)) + fmt.textElement("span", text.copy(content = item), "class" -> cssClass) + } + } + } + .build + parser.parse(source).flatMap(renderer.render).leftMap(_.message) + } +} + +object ToDoTextCssMapping { + /* We want to match either on specific words followed by a colon (`:`) and also on complete text nodes + * which can be created if markdown rendering is involved. + * For example a `**WORD** some text` will produce two text nodes (`WORD` and ` some text`) + * while `WORD: some text` will only produce one. + */ + val isToDoItem: Regex = "(?i)(.*)(DEBUG|FIXME|HACK|TODO):(.*)|^(DEBUG|FIXME|HACK|TODO)$".r + + // Mapping of todo item (words) to css classes for highlighting. + val todoCssClasses: Map[String, String] = Map( + "todo" -> "todo-default", + "fixme" -> "todo-error", + "debug" -> "todo-info", + "hack" -> "todo-warning" + ).withDefaultValue("todo-default") +} diff -rN -u old-smederee/modules/html-utils/src/main/scala/de/smederee/html/MarkdownRenderer.scala new-smederee/modules/html-utils/src/main/scala/de/smederee/html/MarkdownRenderer.scala --- old-smederee/modules/html-utils/src/main/scala/de/smederee/html/MarkdownRenderer.scala 2025-01-11 17:45:25.731881668 +0000 +++ new-smederee/modules/html-utils/src/main/scala/de/smederee/html/MarkdownRenderer.scala 1970-01-01 00:00:00.000000000 +0000 @@ -1,192 +0,0 @@ -/* - * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -package de.smederee.html - -import java.util.Locale - -import cats.syntax.all.* -import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension -import org.commonmark.ext.gfm.tables.TablesExtension -import org.commonmark.ext.heading.anchor.HeadingAnchorExtension -import org.commonmark.ext.task.list.items.TaskListItemsExtension -import org.commonmark.node.* -import org.commonmark.parser.Parser -import org.commonmark.renderer.NodeRenderer -import org.commonmark.renderer.html.* -import org.http4s.Uri -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -import scala.annotation.nowarn -import scala.jdk.CollectionConverters.* -import scala.util.matching.Regex - -object MarkdownRenderer { - private val log = LoggerFactory.getLogger(getClass()) - - private val MarkdownExtensions = - List( - HeadingAnchorExtension.create(), - StrikethroughExtension.create(), - TablesExtension.create(), - TaskListItemsExtension.create() - ) - - /** Render the given markdown content into HTML. - * - * @param markdownSource - * Markdown source code that shall be rendered. - * @return - * A string containing the rendered markdown (HTML). - */ - def render(markdownSource: String): String = { - val parser = Parser.builder().extensions(MarkdownExtensions.asJava).build() - val markdown = parser.parse(markdownSource) - val renderer = HtmlRenderer - .builder() - .escapeHtml(true) - .extensions(MarkdownExtensions.asJava) - .sanitizeUrls(true) - .build() - renderer.render(markdown) - } - - /** Render the given markdown sources and adjust all relative links by prefixing them with the path for the - * repostiory file browsing (`repo-name/files`). - * - * @param repositoryName - * The name of the repository which contains the markdown sources. If the option is empty then no URI path prefix - * will be set. - * @param markdownSource - * A string containing the markdown sources to be rendered (usually the content of the README.md file in the - * repository root). - * @return - * A string containing the rendered markdown (HTML). - */ - def renderRepositoryMarkdownFile(repositoryName: Option[String])(markdownSource: String): String = { - val parser = Parser.builder().extensions(MarkdownExtensions.asJava).build() - val markdown = parser.parse(markdownSource) - val renderer = HtmlRenderer - .builder() - .attributeProviderFactory(new AttributeProviderFactory { - override def create(context: AttributeProviderContext): AttributeProvider = - new LinkHrefCorrector(repositoryName) - }) - .escapeHtml(true) - .extensions(MarkdownExtensions.asJava) - .nodeRendererFactory(new HtmlNodeRendererFactory { - override def create(context: HtmlNodeRendererContext): NodeRenderer = new ToDoTextRenderer(context) - }) - .sanitizeUrls(true) - .build() - renderer.render(markdown) - } - - /** A helper class used by the `renderRepositoryOverviewReadme` function to adjust the `href` attribute of links - * that are not absolute. - * - * @param repositoryName - * The name of the repository which contains the markdown sources. If the option is empty then no URI path prefix - * will be set. - */ - @SuppressWarnings( - Array("scalafix:DisableSyntax.isInstanceOf") - ) // TODO: Find a way to get rid of the isInstanceOf below. - @nowarn("msg=discarded non-Unit value.*") - class LinkHrefCorrector(private val repositoryName: Option[String]) extends AttributeProvider { - override def setAttributes(node: Node, tagName: String, attributes: java.util.Map[String, String]): Unit = - if (node.isInstanceOf[Link]) { - (repositoryName, attributes.asScala.get("href").flatMap(href => Uri.fromString(href).toOption)).mapN { - case (repositoryName, uri) => - if (uri.scheme.isEmpty) { - val pathPrefix = - Uri.Path(Vector(Uri.Path.Segment(repositoryName), Uri.Path.Segment("files"))) - val correctedUri = - if (uri.path.startsWith(pathPrefix)) - uri - else - uri.copy(path = pathPrefix |+| uri.path) - log.debug(s"Corrected URI for repository overview README rendering: $uri -> $correctedUri") - val _ = attributes.put("href", correctedUri.toString) - } - } - } - } - - /** A custom text node renderer which is supposed to highlight several words which are considered "todo items". This - * is currently a very limited approach with a fixed list of matching words. - * - * @param context - * A context for an html node renderer that is needed to extract the html writer from it. - */ - class ToDoTextRenderer(context: HtmlNodeRendererContext) extends NodeRenderer { - import ToDoTextCssMapping.* - - private final val htmlWriter: HtmlWriter = context.getWriter() - private final val log: Logger = LoggerFactory.getLogger(getClass()) - - override def getNodeTypes(): java.util.Set[Class[? <: Node]] = Set(classOf[Text]).asJava - - @SuppressWarnings(Array("scalafix:DisableSyntax.null", "scalafix:DisableSyntax.asInstanceOf")) - override def render(node: Node): Unit = { - val text = node.asInstanceOf[Text] // We only receive text nodes (see `getNodeTypes`). - isToDoItem.findFirstMatchIn(text.getLiteral()) match { - case Some(matchedItem) => - log.debug(s"Matched TODO item: ${text.getLiteral()} (${matchedItem.groupCount})") - if (matchedItem.group(4) === null) { - val prefix = matchedItem.group(1) - val item = matchedItem.group(2) - val suffix = matchedItem.group(3) - val cssClass = todoCssClasses(item.toLowerCase(Locale.ROOT)) - htmlWriter.text(prefix) - htmlWriter.tag("span", Map("class" -> cssClass).asJava) - htmlWriter.text(item + ":") - htmlWriter.tag("/span") - htmlWriter.text(suffix) - } else { - val item = matchedItem.group(4) - val cssClass = todoCssClasses(item.toLowerCase(Locale.ROOT)) - htmlWriter.tag("span", Map("class" -> cssClass).asJava) - htmlWriter.text(item) - htmlWriter.tag("/span") - } - case _ => - htmlWriter.text(text.getLiteral()) - } - } - - } -} - -object ToDoTextCssMapping { - /* We want to match either on specific words followed by a colon (`:`) and also on complete text nodes - * which can be created if markdown rendering is involved. - * For example a `**WORD** some text` will produce two text nodes (`WORD` and ` some text`) - * while `WORD: some text` will only produce one. - */ - val isToDoItem: Regex = "(?i)(.*)(DEBUG|FIXME|HACK|TODO):(.*)|^(DEBUG|FIXME|HACK|TODO)$".r - - // Mapping of todo item (words) to css classes for highlighting. - val todoCssClasses: Map[String, String] = Map( - "todo" -> "todo-default", - "fixme" -> "todo-error", - "debug" -> "todo-info", - "hack" -> "todo-warning" - ).withDefaultValue("todo-default") - -} diff -rN -u old-smederee/modules/html-utils/src/test/scala/de/smederee/html/RenderableContentTest.scala new-smederee/modules/html-utils/src/test/scala/de/smederee/html/RenderableContentTest.scala --- old-smederee/modules/html-utils/src/test/scala/de/smederee/html/RenderableContentTest.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/html-utils/src/test/scala/de/smederee/html/RenderableContentTest.scala 2025-01-11 17:45:25.735881676 +0000 @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.html + +import cats.syntax.all.* +import org.http4s.Uri +import org.http4s.implicits.* + +import munit.* + +final class RenderableContentTest extends ScalaCheckSuite { + private val baseUri = uri"https://www.example.com/some/path" + private val markdownExtensions = List(".md", ".MD", ".markdown", ".MARKDOWN") + private val rstExtensions = List(".rst", ".RST") + + test("fromUri must recognize markdown files") { + markdownExtensions.foreach(ext => + assert( + RenderableContent.fromUri(baseUri.addSegment(s"readme${ext}")).exists(_ === RenderableContent.Markdown) + ) + ) + } + + test("fromUri must recognize reStructuredText files") { + rstExtensions.foreach(ext => + assert( + RenderableContent + .fromUri(baseUri.addSegment(s"readme${ext}")) + .exists(_ === RenderableContent.ReStructuredText) + ) + ) + } +} diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala 2025-01-11 17:45:25.731881668 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala 2025-01-11 17:45:25.735881676 +0000 @@ -19,15 +19,14 @@ import java.io.IOException import java.nio.file.* -import java.util.Locale import cats.* import cats.data.* import cats.effect.* import cats.syntax.all.* import de.smederee.darcs.* +import de.smederee.html.* import de.smederee.html.LinkTools.* -import de.smederee.html.MarkdownRenderer import de.smederee.hub.RelatedTypesConverter.given import de.smederee.hub.RequestHelpers.instances.given import de.smederee.hub.config.* @@ -472,20 +471,19 @@ ) readmeData <- repo.traverse(repo => doLoadReadme(repo)) readme <- readmeData match { - case Some((lines, Some(filename))) => - if (filename.matches("(?iu).*\\.(md|markdown)$")) { - Sync[F] - .delay( - MarkdownRenderer - .renderRepositoryMarkdownFile(repo.map(_.name.toString))(lines.mkString("\n")) + case Some((lines, Some(_), Some(renderableContent))) => + Sync[F].delay( + ContentRenderer + .render(Uri.fromString(s"$repositoryName/files").toOption.map(_.path))(renderableContent)( + lines.mkString("\n") ) - .map(_.some) - } else { - Sync[F].delay(lines.mkString("\n").some) - } + .toOption + ) + case Some((lines, Some(_), None)) => + Sync[F].delay(lines.mkString("\n").some) case _ => Sync[F].delay(None) } - readmeName = readmeData.flatMap(_._2) + readmeContent = readmeData.flatMap(_._3) parentFork <- repo match { case None => Sync[F].pure(None) case Some(repo) => vcsMetadataRepo.findVcsRepositoryParentFork(repo.owner, repo.name) @@ -506,7 +504,7 @@ vcsRepositoryHistory = patches, vcsRepositoryParentFork = parentFork, vcsRepositoryReadme = readme, - vcsRepositoryReadmeFilename = readmeName, + vcsRepositoryReadmeContent = readmeContent, vcsRepositorySshUri = sshUri ) ) @@ -615,16 +613,16 @@ Uri(path = Uri.Path(actionBaseUri.path.segments.reverse.drop(1).reverse)) ) ) - fileContent <- content.isEmpty match { - case false => - if (actionBaseUri.path.toString.toLowerCase(java.util.Locale.ROOT).endsWith(".md")) - Sync[F].delay( - List(MarkdownRenderer.renderRepositoryMarkdownFile(None)(content.mkString("\n"))) - ) - else - Sync[F].delay(content.toList) - case true => - Sync[F].pure(List.empty) + renderableContent = RenderableContent.fromUri(actionBaseUri) + fileContent <- renderableContent match { + case None => Sync[F].delay(content.toList) + case Some(contentType) => + Sync[F].delay( + ContentRenderer + .render(None)(contentType)(content.mkString("\n")) + .map(html => List(html)) + .getOrElse(List.empty) + ) } branches <- repoAndId.map(_._2) match { case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList @@ -651,7 +649,7 @@ linkToTicketService, title.some, user - )(fileContent, listing, repositoryBaseUri, repo, branches) + )(fileContent, renderableContent, listing, repositoryBaseUri, repo, branches) ) } } yield resp @@ -957,9 +955,10 @@ * @param repo * A repository in which we should search. * @return - * A tuple containing a list of strings (lines) that may be empty and an option to the file name. + * A tuple containing a list of strings (lines) that may be empty, an option to the file name and an option to + * the renderable content type. */ - private def doLoadReadme(repo: VcsRepository): F[(Vector[String], Option[String])] = + private def doLoadReadme(repo: VcsRepository): F[(Vector[String], Option[String], Option[RenderableContent])] = for { path <- Sync[F].delay( Paths.get( @@ -971,7 +970,8 @@ _ <- Sync[F].delay(log.debug(s"Trying to find README file in $path.")) files <- Sync[F].delay(os.list(os.Path(path))) readme = files.find(_.last.matches("(?iu)^readme(\\..+)?$")) - _ <- Sync[F].delay(log.debug(s"Found README at $readme.")) + _ <- Sync[F].delay(log.debug(s"Found README at $readme.")) + renderableContent = readme.flatMap(path => RenderableContent.fromUri(Uri.unsafeFromString(path.toString))) size <- readme.traverse(path => Sync[F].delay(os.size(path))) stream <- readme.traverse { path => if (size.getOrElse(0L) <= MaximumFileSize) @@ -980,7 +980,7 @@ Sync[F].delay(os.Generator("")) } lines <- Sync[F].delay(stream.map(_.toVector).getOrElse(Vector.empty)) - } yield (lines, readme.map(_.last)) + } yield (lines, readme.map(_.last), renderableContent) private val cloneRepository: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> UsernamePathParameter(repositoryOwnerName) /: VcsRepositoryClonePathParameter( diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/MilestoneRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/MilestoneRoutes.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/MilestoneRoutes.scala 2025-01-11 17:45:25.731881668 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/MilestoneRoutes.scala 2025-01-11 17:45:25.735881676 +0000 @@ -104,7 +104,11 @@ projectBaseUri.addSegment("milestones").addSegment(milestone.title.toString) ) renderedDescription <- Sync[F].delay( - milestone.description.map(_.toString).map(MarkdownRenderer.render) + milestone.description + .map(_.toString) + .flatMap(content => + ContentRenderer.render(None)(RenderableContent.Markdown)(content).toOption + ) ) resp <- Ok( views.html.showMilestone(lang = language)( diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRoutes.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRoutes.scala 2025-01-11 17:45:25.731881668 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRoutes.scala 2025-01-11 17:45:25.735881676 +0000 @@ -146,7 +146,11 @@ ) ) renderedTicketContent <- Sync[F].delay( - ticket.content.map(_.toString).map(MarkdownRenderer.render) + ticket.content + .map(_.toString) + .flatMap(content => + ContentRenderer.render(None)(RenderableContent.Markdown)(content).toOption + ) ) resp <- Ok( views.html.showTicket(lang = language)( diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html 2025-01-11 17:45:25.731881668 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html 2025-01-11 17:45:25.735881676 +0000 @@ -1,6 +1,7 @@ @import java.util.Locale -@import de.smederee.html.ToDoTextCssMapping._ -@import de.smederee.hub._ +@import de.smederee.html.RenderableContent +@import de.smederee.html.ToDoTextCssMapping.* +@import de.smederee.hub.* @(baseUri: Uri, lang: LanguageCode = LanguageCode("en") @@ -11,6 +12,7 @@ title: Option[String] = None, user: Option[Account] )(fileContent: List[String], + renderableContent: Option[RenderableContent], listing: IndexedSeq[(os.RelPath, os.StatInfo)], repositoryBaseUri: Uri, vcsRepository: VcsRepository, @@ -67,40 +69,40 @@ @for(link <- goBackUri) { <a href="@link">@icon(baseUri)("chevron-up")</a> <a href="@link">..</a> } - @if(actionBaseUri.path.toString.toLowerCase(java.util.Locale.ROOT).endsWith(".md")) { - <div class="repository-markdown-file-content"> - @for(content <- fileContent) { - @Html(content) - } - </div> - } else { - <table class="pure-table"> - <thead> - </thead> - <tbody class="repository-file-content"> - @for(tuple <- fileContent.zipWithIndex) { - @defining(tuple._1) { content => - @defining(tuple._2) { lineNumber => - @defining(isToDoItem.findFirstMatchIn(content)) { matchedItem => - @if(matchedItem.nonEmpty) { - @for(todoItem <- matchedItem) { - @defining(todoCssClasses(Option(todoItem.group(4)).getOrElse(todoItem.group(2)).toLowerCase(Locale.ROOT))) { cssClass => - <tr class="code-line @cssClass"> - } + @if(renderableContent.nonEmpty) { + <div class="repository-markdown-file-content"> + @for(content <- fileContent) { + @Html(content) + } + </div> + } else { + <table class="pure-table"> + <thead> + </thead> + <tbody class="repository-file-content"> + @for(tuple <- fileContent.zipWithIndex) { + @defining(tuple._1) { content => + @defining(tuple._2) { lineNumber => + @defining(isToDoItem.findFirstMatchIn(content)) { matchedItem => + @if(matchedItem.nonEmpty) { + @for(todoItem <- matchedItem) { + @defining(todoCssClasses(Option(todoItem.group(4)).getOrElse(todoItem.group(2)).toLowerCase(Locale.ROOT))) { cssClass => + <tr class="code-line @cssClass"> } - } else { - <tr class="code-line"> } - <td class="code-line-number" id="L@lineNumber"><a href="#L@lineNumber">@lineNumber</a></td> - <td class="code-line"><code class="code-line">@content</code></td> - </tr> + } else { + <tr class="code-line"> } + <td class="code-line-number" id="L@lineNumber"><a href="#L@lineNumber">@lineNumber</a></td> + <td class="code-line"><code class="code-line">@content</code></td> + </tr> } } } - </tbody> - </table> - } + } + </tbody> + </table> + } } </div> </div> diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html 2025-01-11 17:45:25.731881668 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html 2025-01-11 17:45:25.735881676 +0000 @@ -1,4 +1,4 @@ -@import de.smederee.hub._ +@import de.smederee.hub.* @(baseUri: Uri, lang: LanguageCode = LanguageCode("en") @@ -12,7 +12,7 @@ vcsRepositoryHistory: List[VcsRepositoryPatchMetadata], vcsRepositoryParentFork: Option[VcsRepository] = None, vcsRepositoryReadme: Option[String] = None, - vcsRepositoryReadmeFilename: Option[String] = None, + vcsRepositoryReadmeContent: Option[RenderableContent] = None, vcsRepositorySshUri: Option[String] = None ) @main(baseUri, lang)()(csrf, title, user) { @@ -100,7 +100,7 @@ <div class="pure-u-1 repo-summary-readme"> <div class="l-box"> @for(content <- vcsRepositoryReadme) { - @if(vcsRepositoryReadmeFilename.exists(_.matches("(?iu).*\\.(md|markdown)$"))) { + @if(vcsRepositoryReadmeContent.nonEmpty) { <div class="repository-markdown-file-content"> @Html(content) </div>