aboutsummaryrefslogtreecommitdiff
path: root/doc-tool/src/dotty/tools/dottydoc/staticsite/Page.scala
blob: fda41a234b8e326e630523d2e1d76eb3426e6325 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
package dotty.tools
package dottydoc
package staticsite


import dotc.util.SourceFile
import com.vladsch.flexmark.html.HtmlRenderer
import com.vladsch.flexmark.parser.Parser
import com.vladsch.flexmark.ext.front.matter.AbstractYamlFrontMatterVisitor
import java.util.{ Map => JMap, List => JList }

import dotc.config.Printers.dottydoc
import model.Package

/** When the YAML front matter cannot be parsed, this exception is thrown */
case class IllegalFrontMatter(message: String) extends Exception(message)

trait Page {
  import scala.collection.JavaConverters._

  /** Full map of includes, from name to contents */
  def includes: Map[String, Include]

  /** `SourceFile` with contents of page */
  def sourceFile: SourceFile

  /** String containing full unexpanded content of page */
  final lazy val content: String = new String(sourceFile.content)

  /** Parameters to page */
  def params: Map[String, AnyRef]

  /** Path to template */
  def path: String

  /** YAML front matter from the top of the file */
  def yaml: Map[String, AnyRef] = {
    if (_yaml eq null) initFields()
    _yaml
  }

  /** HTML generated from page */
  def html: String = {
    if (_html eq null) initFields()
    _html
  }

  /** First paragraph of page extracted from rendered HTML */
  def firstParagraph: String = {
    if (_html eq null) initFields()

    val sb = new StringBuilder
    var pos = 0
    // to handle nested paragraphs in non markdown code
    var open = 0

    while (pos < _html.length - 4) {
      val str = _html.substring(pos, pos + 4)
      val lstr = str.toLowerCase
      sb append str.head

      pos += 1
      if (lstr.contains("<p>"))
        open += 1
      else if (lstr == "</p>") {
        open -= 1
        if (open == 0) {
          pos = Int.MaxValue
          sb append "/p>"
        }
      }
    }

    sb.toString
  }

  protected[this] var _yaml: Map[String, AnyRef /* String | JList[String] */] = _
  protected[this] var _html: String = _
  protected[this] def initFields() = {
    val md = Parser.builder(Site.markdownOptions).build.parse(content)
    val yamlCollector = new AbstractYamlFrontMatterVisitor()
    yamlCollector.visit(md)

    _yaml = updatedYaml {
      yamlCollector
      .getData().asScala
      .mapValues {
        case xs if xs.size == 1 =>
          val str = xs.get(0)
          if (str.length > 0 && str.head == '"' && str.last == '"')
            str.substring(1, str.length - 1)
          else str
        case xs => xs
      }
      .toMap
    }

    // YAML must start with "---" and end in either "---" or "..."
    val withoutYaml =
      if (content.startsWith("---\n")) {
        val str =
          content.lines
          .drop(1)
          .dropWhile(line => line != "---" && line != "...")
          .drop(1).mkString("\n")

        if (str.isEmpty) throw IllegalFrontMatter(content)
        else str
      }
      else content

    // make accessible via "{{ page.title }}" in templates
    val page = Map("page" ->  _yaml.asJava)
    _html = LiquidTemplate(path, withoutYaml).render(params ++ page, includes)
  }

  /** Takes "page" from `params` map in case this is a second expansion, and
    * removes "layout" from the parameters if it exists. We don't want to
    * preserve the layout from the previously expanded template
    */
  private def updatedYaml(newYaml: Map[String, AnyRef]): Map[String, AnyRef] =
    params
    .get("page")
    .flatMap {
      case page: Map[String, AnyRef] @unchecked =>
        Some(page - "layout" ++ newYaml)
      case _ => None
    }
    .getOrElse(newYaml)
}

class HtmlPage(
  val path: String,
  val sourceFile: SourceFile,
  val params: Map[String, AnyRef],
  val includes: Map[String, Include]
) extends Page

class MarkdownPage(
  val path: String,
  val sourceFile: SourceFile,
  val params: Map[String, AnyRef],
  val includes: Map[String, Include],
  docs: Map[String, Package]
) extends Page {

  override protected[this] def initFields() = {
    super.initFields()
    val md = Parser.builder(Site.markdownOptions).build.parse(_html)
    // fix markdown linking
    MarkdownLinkVisitor(md, docs, params)
    MarkdownCodeBlockVisitor(md)
    _html = HtmlRenderer
      .builder(Site.markdownOptions)
      .escapeHtml(false)
      .build()
      .render(md)
  }
}