All Downloads are FREE. Search and download functionalities are using the official Maven repository.

scalatex.site.Highlighter.scala Maven / Gradle / Ivy

The newest version!
package scalatex.site
import collection.mutable
import ammonite.ops.{RelPath, Path}

import scalatags.Text.all._
import ammonite.ops._
/**
 * Lets you instantiate a Highlighter object. This can be used to reference
 * snippets of code from files within your project via the `.ref` method, often
 * used via `hl.ref` where `hl` is a previously-instantiated Highlighter.
 */
trait Highlighter{ hl =>
  val languages = mutable.Set.empty[String]
  def webjars = resource/"META-INF"/'resources/'webjars
  def highlightJs = webjars/'highlightjs/"9.12.0"
  def highlightJsSource = webjars/"highlight.js"
  def style: String = "idea"

  case class lang(name: String){
    def apply(s: Any*) = hl.highlight(s.mkString, name)
  }
  def as = lang("actionscript")
  def scala = lang("scala")
  def asciidoc = lang("asciidoc")
  def ahk = lang("autohotkey")
  def sh = lang("bash")
  def clj = lang("clojure")
  def coffee = lang("coffeescript")
  def ex = lang("elixir")
  def erl = lang("erlang")
  def fs = lang("fsharp")
  def hs = lang("haskell")
  def hx = lang("haxe")
  def js = lang("javascript")
  def nim = lang("nimrod")
  def rb = lang("ruby")
  def ts = lang("typescript")
  def vb = lang("vbnet")
  def xml = lang("xml")
  def diff = lang("diff")
  def autoResources = {
    Seq(highlightJs/"highlight.pack.min.js") ++
    Seq(highlightJs/'styles/s"$style.css") ++
    languages.map(x => highlightJsSource/'src/'languages/s"$x.js")
  }
  /**
   * A mapping of file-path-prefixes to URLs where the source
   * can be accessed. e.g.
   *
   * Seq(
   *   "clones/scala-js" -> "https://github.com/scala-js/scala-js/blob/master",
   *   "" -> "https://github.com/lihaoyi/scalatex/blob/master"
   * )
   *
   * Will link any code reference from clones/scala-js to the scala-js
   * github repo, while all other paths will default to the scalatex
   * github repo.
   *
   * If a path is not covered by any of these rules, no link is rendered
   */
  def pathMappings: Seq[(Path, String)] = Nil

  /**
   * A mapping of file name suffixes to highlight.js classes.
   * Usually something like:
   *
   * Map(
   *   "scala" -> "scala",
   *   "js" -> "javascript"
   * )
   */
  def suffixMappings: Map[String, String] = Map(
    "scala" -> "scala",
    "sbt" -> "scala",
    "scalatex" -> "scala",
    "as" -> "actionscript",
    "ahk" -> "autohotkey",
    "coffee" -> "coffeescript",
    "clj" -> "clojure",
    "cljs" -> "clojure",
    "sh" -> "bash",
    "ex" -> "elixir",
    "erl" -> "erlang",
    "fs" -> "fsharp",
    "hs" -> "haskell",
    "hx" -> "haxe",
    "js" -> "javascript",
    "nim" -> "nimrod",
    "rkt" -> "lisp",
    "scm" -> "lisp",
    "sch" -> "lisp",
    "rb" -> "ruby",
    "ts" -> "typescript",
    "vb" -> "vbnet"
  )

  /**
   * Highlight a short code snippet with the specified language
   */
  def highlight(string: String, lang: String) = {
    languages.add(lang)
    val lines = string.split("\n", -1)
    if (lines.length == 1){
      code(
        cls:=lang + " " + Styles.highlightMe.name,
        display:="inline",
        padding:=0,
        margin:=0,
        lines(0)
      )

    }else{
      val minIndent = lines.filter(_.trim != "").map(_.takeWhile(_ == ' ').length).min
      val stripped = lines.map(_.drop(minIndent))
        .dropWhile(_ == "")
        .mkString("\n")

      pre(code(cls:=lang + " " + Styles.highlightMe.name, stripped))
    }
  }
  import Highlighter._
  /**
   * Grab a snippet of code from the given filepath, and highlight it.
   *
   * @param filePath The file containing the code in question
   * @param start Snippets used to navigate to the start of the snippet
   *              you want, from the beginning of the file
   * @param end Snippets used to navigate to the end of the snippet
   *            you want, from the start of start of the snippet
   * @param className An optional css class set on the rendered snippet
   *                  to determine what language it gets highlighted as.
   *                  If not given, it defaults to the class given in
   *                  [[suffixMappings]]
   */
  def ref[S: RefPath, V: RefPath]
         (filePath: ammonite.ops.BasePath,
          start: S = Nil,
          end: V = Nil,
          className: String = null) = {
    val absPath = filePath match{
      case p: Path => p
      case p: RelPath => pwd/p
    }

    val ext = filePath.last.split('.').last
    val lang = Option(className)
      .getOrElse(suffixMappings.getOrElse(ext, ext))


    val linkData =
      pathMappings.iterator
                  .find{case (prefix, path) => absPath startsWith prefix}
    val (startLine, endLine, blob) = referenceText(absPath, start, end)
    val link = linkData.map{ case (prefix, url) =>
      val hash =
        if (endLine == -1) ""
        else s"#L$startLine-L$endLine"

      val linkUrl = s"$url/${absPath relativeTo prefix}$hash"
      a(
        Styles.headerLink,
        i(cls:="fa fa-link "),
        position.absolute,
        right:="0.5em",
        top:="0.5em",
        display.block,
        fontSize:="24px",
        href:=linkUrl,
        target:="_blank"
      )
    }

    pre(
      Styles.hoverContainer,
      code(cls:=lang + " " + Styles.highlightMe.name, blob),
      link
    )
  }

  def referenceText[S: RefPath, V: RefPath](filepath: Path, start: S, end: V) = {
    val fileLines = read.lines! filepath
    // Start from -1 so that searching for things on the first line of the file (-1 + 1 = 0)


    def walk(query: Seq[String], start: Int) = {
      var startIndex = start
      for(str <- query){
        startIndex = fileLines.indexWhere(_.contains(str), startIndex + 1)
        if (startIndex == -1) throw new RefError(
          s"Highlighter unable to resolve reference $str in selector $query"
        )
      }
      startIndex
    }
    // But if there are no selectors, start from 0 and not -1
    val startQuery = implicitly[RefPath[S]].apply(start)
    val startIndex = if (startQuery == Nil) 0 else walk(startQuery, -1)
    val startIndent = fileLines(startIndex).takeWhile(_.isWhitespace).length
    val endQuery = implicitly[RefPath[V]].apply(end)
    val endIndex = if (endQuery == Nil) {
      val next = fileLines.drop(startIndex).takeWhile{ line =>
        line.trim == "" || line.takeWhile(_.isWhitespace).length >= startIndent
      }
      startIndex + next.length
    } else {

      walk(endQuery, startIndex)
    }
    val margin = fileLines(startIndex).takeWhile(_.isWhitespace).length
    val lines = fileLines.slice(startIndex, endIndex)
                   .map(_.drop(margin))
                   .reverse
                   .dropWhile(_.trim == "")
                   .reverse

    (startIndex, endIndex, lines.mkString("\n"))

  }
}

object Highlighter{
  class RefError(msg: String) extends Exception(msg)
  def snippet = script(raw(s"""
    ['DOMContentLoaded', 'load'].forEach(function(ev){
      addEventListener(ev, function(){
        Array.prototype.forEach.call(
          document.getElementsByClassName('${Styles.highlightMe.name}'),
          hljs.highlightBlock
        );
      })
    })
  """))
  /**
   * A context bound used to ensure you pass a `String`
   * or `Seq[String]` to the `@hl.ref` function
   */
  trait RefPath[T]{
    def apply(t: T): Seq[String]
  }
  object RefPath{
    implicit object StringRefPath extends RefPath[String]{
      def apply(t: String) = Seq(t)
    }
    implicit object SeqRefPath extends RefPath[Seq[String]]{
      def apply(t: Seq[String]) = t
    }
    implicit object NilRefPath extends RefPath[Nil.type]{
      def apply(t: Nil.type) = t
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy