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

laika.helium.config.ThemeLink.scala Maven / Gradle / Ivy

/*
 * Copyright 2012-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package laika.helium.config

import cats.data.NonEmptyList
import laika.ast.RelativePath.CurrentDocument
import laika.ast._
import laika.config.LaikaKeys
import laika.parse.{GeneratedSource, SourceFragment}
import laika.rewrite.Versions

/** A Helium link type available for navigation bars and the landing page.
  */
sealed trait ThemeLink extends Unresolved {
  
  val source: SourceFragment = GeneratedSource

  type Self <: ThemeLink
  
  def unresolvedMessage: String = s"Unresolved theme link: $this"

  def runsIn (phase: RewritePhase): Boolean = phase.isInstanceOf[RewritePhase.Render]

}

sealed trait ThemeLinkSpan extends ThemeLink with SpanResolver {
  type Self <: ThemeLinkSpan
}
sealed trait ThemeLinkBlock extends ThemeLink with BlockResolver {
  type Self <: ThemeLinkBlock
}

sealed trait SingleTargetLink extends ThemeLinkSpan {

  /** The target of the link, either internal or external. */
  def target: Target

  def resolve (cursor: DocumentCursor): Span = target match {
    case et: ExternalTarget => createLink(et)
    case it: InternalTarget => cursor.validateAndRecover(createLink(it), source)
  }

  protected def createLink (target: Target): Link
  
}

sealed trait MultiTargetLink extends ThemeLink {
  
  def links: Seq[SingleTargetLink]
  
}


/** A link consisting of an icon and optional text.
  */
sealed abstract case class IconLink (target: Target, icon: Icon, text: Option[String] = None, options: Options = NoOpt) extends SingleTargetLink {
  type Self = IconLink
  protected def createLink (target: Target): Link = {
    val iconTypeStyle = icon match {
      case _: IconGlyph => "glyph-link"
      case _: IconStyle => "style-link"
      case _: InlineSVGIcon => "svg-link"
      case _: SVGSymbolIcon => "svg-link"
    }
    val allOptions = HeliumStyles.iconLink + Styles(iconTypeStyle) + options
    SpanLink(icon +: text.map(Text(_)).toSeq, target, options = allOptions)
  }
  def withOptions(newOptions: Options): IconLink = new IconLink(target, icon, text, newOptions) {}
}

object IconLink {
  /** Creates an icon link to an external target, consisting of an icon and optional text. */
  def external (url: String, icon: Icon, text: Option[String] = None, options: Options = NoOpt): IconLink =
    new IconLink(ExternalTarget(url), icon, text, options) {}
  /** Creates an icon link to an internal target, consisting of an icon and optional text. */
  def internal (path: Path, icon: Icon, text: Option[String] = None, options: Options = NoOpt): IconLink =
    new IconLink(InternalTarget(path), icon, text, options) {}
}

/** A link consisting of text and an optional icon, by default rendered in a rounded rectangle.
  */
sealed abstract case class ButtonLink (target: Target, text: String, icon: Option[Icon] = None, options: Options = NoOpt) extends SingleTargetLink {
  type Self = ButtonLink
  protected def createLink (target: Target): Link = SpanLink(icon.toSeq :+ Text(text), target, options = HeliumStyles.buttonLink + options)
  def withOptions(newOptions: Options): ButtonLink = new ButtonLink(target, text, icon, newOptions) {}
}

object ButtonLink {
  /** Creates a button link to an external target, consisting of text and an optional icon rendered in a rounded rectangle. */
  def external (url: String, text: String, icon: Option[Icon] = None, options: Options = NoOpt): ButtonLink =
    new ButtonLink(ExternalTarget(url), text, icon, options) {}
  /** Creates a button link to an internal target, consisting of text and an optional icon rendered in a rounded rectangle. */
  def internal (path: Path, text: String, icon: Option[Icon] = None, options: Options = NoOpt): ButtonLink =
    new ButtonLink(InternalTarget(path), text, icon, options) {}
}

/** A simple text link.
  */
sealed abstract case class TextLink (target: Target, text: String, options: Options = NoOpt) extends SingleTargetLink {
  type Self = TextLink
  protected def createLink (target: Target): Link = SpanLink(Seq(Text(text)), target, options = HeliumStyles.textLink + options)
  def withOptions(newOptions: Options): TextLink = new TextLink(target, text, newOptions) {}
}

object TextLink {
  /** Creates a simple text link to an external target. */
  def external (url: String, text: String, options: Options = NoOpt): TextLink =
    new TextLink(ExternalTarget(url), text, options) {}
  /** Creates a simple text link to an internal target. */
  def internal (path: Path, text: String, options: Options = NoOpt): TextLink =
    new TextLink(InternalTarget(path), text, options) {}
}

/** A simple image link.
  */
sealed abstract case class ImageLink (target: Target, image: Image, options: Options = NoOpt) extends SingleTargetLink {
  type Self = ImageLink
  protected def createLink (target: Target): Link = SpanLink(Seq(image), target, options = HeliumStyles.imageLink + options)
  def withOptions(newOptions: Options): ImageLink =
    new ImageLink(target, image, newOptions) {}
}

object ImageLink {
  /** Creates a simple image link to an external target. */
  def external (url: String, image: Image, options: Options = NoOpt): ImageLink = 
    new ImageLink(ExternalTarget(url), image, options) {}
  /** Creates a simple image link to an internal target. */
  def internal (path: Path, image: Image, options: Options = NoOpt): ImageLink =
    new ImageLink(InternalTarget(path), image, options) {}
}

/** A home link that inserts a link to the title page of the root document tree (if available)
  * or otherwise an invalid element.
  */
final case class DynamicHomeLink (options: Options = NoOpt) extends ThemeLinkSpan {
  type Self = DynamicHomeLink

  def resolve (cursor: DocumentCursor): Span = {
    cursor.root.tree.titleDocument match {
      case Some(homePage) => 
        IconLink.internal(homePage.path, HeliumIcon.home).resolve(cursor)
      case None => 
        val message = "No target for home link found - for options see 'Theme Settings / Top Navigation Bar' in the manual" 
        InvalidSpan(message, GeneratedSource)
    }
  }
  
  def withOptions(newOptions: Options): DynamicHomeLink = copy(options = newOptions)
}

object DynamicHomeLink {
  val default: DynamicHomeLink = DynamicHomeLink()
}

/** A generic group of theme links.
  *
  * Can be used to create structures like a row of icon links in a vertical column of text links. 
  */
sealed abstract case class LinkGroup (links: Seq[SingleTargetLink], options: Options = NoOpt) extends ThemeLinkSpan with MultiTargetLink {
  type Self = LinkGroup
  def resolve (cursor: DocumentCursor): Span = SpanSequence(links.map(_.resolve(cursor)), HeliumStyles.linkRow + options)
  def withOptions(newOptions: Options): LinkGroup = new LinkGroup(links, newOptions) {}
}

object LinkGroup {
  def create (link: SingleTargetLink, links: SingleTargetLink*): LinkGroup = new LinkGroup(link +: links) {}
}

/** A menu for the top navigation bar or the landing page.
  */
sealed abstract case class Menu (label: Seq[Span], links: Seq[SingleTargetLink], options: Options = NoOpt) extends ThemeLinkBlock with MultiTargetLink {
  type Self = Menu
  
  def resolve (cursor: DocumentCursor): Block = {
    
    val toggle = SpanLink(label, InternalTarget(CurrentDocument()))
      .withOptions(HeliumStyles.textLink + HeliumStyles.menuToggle)
    
    val navLinks = links.map(_.resolve(cursor)).collect {
      case sl: SpanLink => 
        NavigationItem(SpanSequence(sl.content), Nil, Some(NavigationLink(sl.target)))
          .withOptions(Style.level(1))
    }
    
    val content = BlockSequence(NavigationList(navLinks)).withOptions(HeliumStyles.menuContent)
      
    BlockSequence(SpanSequence(toggle), content).withOptions(HeliumStyles.menuContainer + options)
  }
  def withOptions(newOptions: Options): Menu = new Menu(label, links, newOptions) {}
}

object Menu {
  def create (label: String, link: SingleTargetLink, links: SingleTargetLink*): Menu = 
    new Menu(Seq(Text(label)), link +: links) {}
  def create (label: Seq[Span], link: SingleTargetLink, links: SingleTargetLink*): Menu = 
    new Menu(label, link +: links) {}
}

sealed abstract case class VersionMenu (versionedLabelPrefix: String, 
                                        unversionedLabel: String,
                                        links: Seq[SingleTargetLink], 
                                        options: Options = NoOpt) extends ThemeLinkBlock with MultiTargetLink {
  type Self = VersionMenu

  def resolve (cursor: DocumentCursor): Block = {
    cursor.config.get[Versions].toOption.fold[Block](BlockSequence.empty) { versions =>
      val isVersioned = cursor.config.get[Boolean](LaikaKeys.versioned).getOrElse(false)
      val labelText = 
        if (isVersioned) s"$versionedLabelPrefix ${versions.currentVersion.displayValue}"
        else unversionedLabel
      val menu = new Menu(Seq(Text(labelText)), links, HeliumStyles.versionMenu) {}
      menu.resolve(cursor)
    }
  }
  
  def withOptions(newOptions: Options): VersionMenu = 
    new VersionMenu(versionedLabelPrefix, unversionedLabel, links, newOptions) {}
}

object VersionMenu {
  
  def create (versionedLabelPrefix: String = default.versionedLabelPrefix, 
              unversionedLabel: String = default.unversionedLabel, 
              additionalLinks: Seq[SingleTargetLink] = Nil): VersionMenu = 
    new VersionMenu(versionedLabelPrefix, unversionedLabel, additionalLinks) {}
    
  val default: VersionMenu = create("Version", "Choose Version")
  
}

final case class ThemeNavigationSection (title: String, links: NonEmptyList[TextLink])

object ThemeNavigationSection {
  def apply (title: String, link: TextLink, links: TextLink*): ThemeNavigationSection =
    ThemeNavigationSection(title, NonEmptyList.of(link, links:_*))
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy