~jan0sch/smederee

Showing details for patch 2a56d7d06bff5d7004ec3b4c6854baed1de07fa8.
2023-03-06 (Mon), 10:28 AM - Jens Grassel - 2a56d7d06bff5d7004ec3b4c6854baed1de07fa8

HTML/CSS: Add highlighting for todo items in file renderer.

- match on a [very limited] list of words that are considered
  "todo indicators"
- wrap matched words into a span tag with certain css class
- add css for highlighting
- works currently only for markdown files
Summary of changes
2 files modified with 95 lines added and 2 lines removed
  • modules/hub/src/main/resources/assets/css/main.css with 32 added and 0 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/MarkdownRenderer.scala with 63 added and 2 removed lines
diff -rN -u old-smederee/modules/hub/src/main/resources/assets/css/main.css new-smederee/modules/hub/src/main/resources/assets/css/main.css
--- old-smederee/modules/hub/src/main/resources/assets/css/main.css	2025-01-31 13:57:06.982324843 +0000
+++ new-smederee/modules/hub/src/main/resources/assets/css/main.css	2025-01-31 13:57:06.982324843 +0000
@@ -345,6 +345,38 @@
   border-bottom: 1px solid var(--nord4);
 }
 
+.todo-default {
+  background-color: var(--nord15);
+}
+
+.todo-default:hover {
+  background-color: transparent;
+}
+
+.todo-error {
+  background-color: var(--nord11);
+}
+
+.todo-error:hover {
+  background-color: transparent;
+}
+
+.todo-info {
+  background-color: var(--nord14);
+}
+
+.todo-info:hover {
+  background-color: transparent;
+}
+
+.todo-warning {
+  background-color: var(--nord13);
+}
+
+.todo-warning:hover {
+  background-color: transparent;
+}
+
 pre.latest-changes {
   overflow-x: auto;
   overflow-y: hidden;
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/MarkdownRenderer.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/MarkdownRenderer.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/MarkdownRenderer.scala	2025-01-31 13:57:06.982324843 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/MarkdownRenderer.scala	2025-01-31 13:57:06.982324843 +0000
@@ -17,17 +17,21 @@
 
 package de.smederee.hub
 
+import java.util.Locale
+
 import cats.syntax.all._
 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.LoggerFactory
+import org.slf4j.{ Logger, LoggerFactory }
 
 import scala.jdk.CollectionConverters._
+import scala.util.matching.Regex
 
 object MarkdownRenderer {
   private val log = LoggerFactory.getLogger(getClass())
@@ -56,6 +60,9 @@
       })
       .escapeHtml(true)
       .extensions(MarkdownExtensions.asJava)
+      .nodeRendererFactory(new HtmlNodeRendererFactory {
+        override def create(context: HtmlNodeRendererContext): NodeRenderer = new ToDoTextRenderer(context)
+      })
       .sanitizeUrls(true)
       .build()
     renderer.render(markdown)
@@ -69,7 +76,7 @@
     */
   @SuppressWarnings(
     Array("scalafix:DisableSyntax.isInstanceOf")
-  ) // TODO Find a way to get rid of the isInstanceOf below.
+  ) // TODO: Find a way to get rid of the isInstanceOf below.
   class LinkHrefCorrector(private val repo: Option[VcsRepository]) extends AttributeProvider {
     override def setAttributes(node: Node, tagName: String, attributes: java.util.Map[String, String]): Unit =
       if (node.isInstanceOf[Link]) {
@@ -88,4 +95,58 @@
         }
       }
   }
+
+  /** 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 {
+    private final val htmlWriter: HtmlWriter = context.getWriter()
+    /* 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.
+     */
+    private final val isToDoItem: Regex = "(?i)(.*)(DEBUG|FIXME|HACK|TODO):(.*)|^(DEBUG|FIXME|HACK|TODO)$".r
+    private final val log: Logger       = LoggerFactory.getLogger(getClass())
+
+    private final val todoCssClasses: Map[String, String] = Map(
+      "todo"  -> "todo-default",
+      "fixme" -> "todo-error",
+      "debug" -> "todo-info",
+      "hack"  -> "todo-warning"
+    ).withDefaultValue("todo-default")
+
+    override def getNodeTypes(): java.util.Set[Class[? <: Node]] = Set(classOf[Text]).asJava
+
+    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.info(s"MATCH: ${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())
+      }
+    }
+
+  }
 }