aboutsummaryrefslogtreecommitdiff
path: root/doc-tool/src/dotty/tools/dottydoc/staticsite/Page.scala
blob: 4cbb57705442f166176d09bbc272877a5ed23a93 (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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
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 java.io.{ OutputStreamWriter, BufferedWriter }

import io.VirtualFile
import dotc.core.Contexts.Context
import model.Package
import scala.io.Codec

/** 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(implicit ctx: Context): Map[String, AnyRef] = {
    if (_yaml eq null) initFields
    _yaml
  }

  /** HTML generated from page */
  def html(implicit ctx: Context): Option[String] = {
    if (_html eq null) initFields
    _html
  }

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

    _html.map { _html =>
      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
    }
    .getOrElse("")
  }

  protected def virtualFile(subSource: String): SourceFile = {
    val virtualFile = new VirtualFile(path, path)
    val writer = new BufferedWriter(new OutputStreamWriter(virtualFile.output, "UTF-8"))
    writer.write(subSource)
    writer.close()

    new SourceFile(virtualFile, Codec.UTF8)
  }


  protected[this] var _yaml: Map[String, AnyRef /* String | JList[String] */] = _
  protected[this] var _html: Option[String] = _
  protected[this] def initFields(implicit ctx: Context) = {
    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 = virtualFile(
      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(implicit ctx: Context) = {
    super.initFields
    _html = _html.map { _html =>
      val md = Parser.builder(Site.markdownOptions).build.parse(_html)
      // fix markdown linking
      MarkdownLinkVisitor(md, docs, params)
      MarkdownCodeBlockVisitor(md)
      HtmlRenderer
        .builder(Site.markdownOptions)
        .escapeHtml(false)
        .build()
        .render(md)
    }
  }
}