Skip to content

Commit

Permalink
Support site search
Browse files Browse the repository at this point in the history
  • Loading branch information
jonas committed Oct 19, 2017
1 parent 1725f03 commit ed7ea1c
Show file tree
Hide file tree
Showing 11 changed files with 215 additions and 8 deletions.
2 changes: 2 additions & 0 deletions NEWS.md
Expand Up @@ -3,11 +3,13 @@
## master

- Update to [mkdocs-material-1.11.0].
- Support site search. [#1]
- Show version, wrap code and hide clipboard icon when printing. [#4]
- Document `material.language`.
- Make certain messages translatable.

[mkdocs-material-1.11.0]: https://github.com/squidfunk/mkdocs-material/releases/tag/1.11.0
[#1]: https://github.com/jonas/paradox-material-theme/issues/1
[#4]: https://github.com/jonas/paradox-material-theme/issues/4

## 0.1.1
Expand Down
21 changes: 21 additions & 0 deletions build.sbt
Expand Up @@ -3,6 +3,7 @@ organization := "io.github.jonas"
licenses += "MIT" -> url("https://github.com/jonas/paradox-material-theme/blob/master/LICENSE")
description := "Material Design theme for Paradox"

mappings in makeSite += SearchIndex.mapping(Compile).value
siteSourceDirectory := (target in (Compile, paradox)).value
makeSite := makeSite.dependsOn(paradox in Compile).value

Expand Down Expand Up @@ -36,6 +37,12 @@ paradoxProperties in Compile ++= Map(
)
//#color

//#search
paradoxProperties in Compile ++= Map(
"material.search" -> "true" // NOTE: Any value will do
)
//#search

//#repository
paradoxProperties in Compile ++= Map(
"material.repo" -> "https://github.com/jonas/paradox-material-theme",
Expand Down Expand Up @@ -115,4 +122,18 @@ val optionExamples = Def.setting(
"material.custom.javascript" -> "assets/custom.js"
)
//#custom-javascript
,
//#search-tokenizer
paradoxProperties in Compile ++= Map(
"material.search.tokenizer" -> "[\\s\\-\\.]+"
)
//#search-tokenizer
,
//#search-paradox
mappings in (Compile, paradox) += SearchIndex.mapping(Compile).value
//#search-paradox
,
//#search-sbt-site
mappings in makeSite += SearchIndex.mapping(Paradox).value
//#search-sbt-site
)
77 changes: 77 additions & 0 deletions project/SearchIndex.scala
@@ -0,0 +1,77 @@
import sbt._
import sbt.Keys._
import scala.collection.JavaConverters._
import com.lightbend.paradox.sbt.ParadoxPlugin.autoImport.paradoxMarkdownToHtml
import io.circe._
import io.circe.syntax._
import org.jsoup.Jsoup
import org.jsoup.nodes.Element

case class SearchIndex(docs: Seq[SearchIndex.Section])

object SearchIndex {
implicit val encoder: ObjectEncoder[SearchIndex] = Encoder.forProduct1("docs")(_.docs)

case class Section(location: String, title: String, text: String)
object Section {
implicit val encoder: ObjectEncoder[Section] = Encoder.forProduct3("location", "text", "title")(
page => (page.location, page.text, page.title))
}

val Headers = Set("h1", "h2", "h3", "h4", "h5", "h6")

def generate(target: File, mappings: Seq[(File, String)]): File = {
def readSections(mapping: (File, String)): Seq[Section] = {
val (file, location) = mapping
val doc = Jsoup.parse(file, "UTF-8")
val docTitle = {
val title = doc.select("head title").text()
Option(title.lastIndexOf(" · "))
.filter(_ > 0)
.map(title.substring(0, _))
.getOrElse(title)
}

def headerLocation(header: Element) =
Option(header.select("a[name]").first()) match {
case Some(anchor) => location + "#" + anchor.attr("name")
case None => location
}

def processElement(section: Section, elements: List[Element]): Seq[Section] =
elements match {
case header :: tail if Headers(header.tagName) =>
val location = headerLocation(header)
val next = Section(location, header.text, "")
Vector(section) ++ processElement(next, tail)
case element :: tail =>
val text =
if (section.text.isEmpty) element.text
else section.text + "\n" + element.text
processElement(section.copy(text = text.trim), tail)
case Nil =>
Vector(section)
}

val elements =
doc.select("body .md-content__searchable").asScala.flatMap(_.children.asScala).toList

processElement(Section(location, docTitle, ""), elements)
}

val sections = mappings.flatMap(readSections).toList
val searchIndex = SearchIndex(sections)
val json = searchIndex.asJson.noSpaces
val out = target / "search_index.json"
IO.write(out, json)
out
}

def mapping(scope: Configuration) = Def.task {
val index = generate(
(target in scope).value / "paradox-material-theme",
(paradoxMarkdownToHtml in scope).value
)
index -> "mkdocs/search_index.json"
}
}
5 changes: 5 additions & 0 deletions project/plugins.sbt
Expand Up @@ -5,3 +5,8 @@ addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.2")
addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.6")
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0")
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.0")

//#search-dependencies
libraryDependencies += "org.jsoup" % "jsoup" % "1.10.3"
libraryDependencies += "io.circe" %% "circe-core" % "0.8.0"
//#search-dependencies
6 changes: 4 additions & 2 deletions src/main/assets/page.st
Expand Up @@ -105,7 +105,9 @@ $!
$ endif $
<div class="md-content">
<article class="md-content__inner md-typeset">
$page.content$
<div class="md-content__searchable">
$page.content$
</div>
$ if (page.source_url) $
<div>
<a href="$page.source_url$" title="Edit this page" class="md-source-file md-edit">
Expand All @@ -126,7 +128,7 @@ $!
</div>
<script src="$page.base$assets/javascripts/application-268d62d82d.js"></script>
<script src="$page.base$assets/javascripts/paradox-material-theme.js"></script>
<script>app.initialize({url:{base:"$page.base$"}})</script>
<script>app.initialize({url:{base:"$page.base$."}})</script>
<script type="text/javascript" src="$page.base$lib/prettify/prettify.js"></script>
<script type="text/javascript" src="$page.base$lib/prettify/lang-scala.js"></script>
<script type="text/javascript">
Expand Down
14 changes: 10 additions & 4 deletions src/main/assets/partials/header.st
Expand Up @@ -51,10 +51,16 @@ $!
$page.title$
</span>
</div>
<div class="md-flex__cell md-flex__cell--shrink" style="display: none">
<label class="md-icon md-icon--search md-header-nav__button" for="search"></label>
$search()$
</div>
$ if (page.properties.("material.search")) $
<div class="md-flex__cell md-flex__cell--shrink">
<label class="md-icon md-icon--search md-header-nav__button" for="search"></label>
$search()$
</div>
$ else $
<form name="search" style="display: none">
<input type="text" name="query">
</form>
$ endif $
$ if (page.properties.("material.repo")) $
<div class="md-flex__cell md-flex__cell--shrink">
<div class="md-header-nav__source">
Expand Down
2 changes: 1 addition & 1 deletion src/main/assets/partials/language.st
Expand Up @@ -4,4 +4,4 @@
<meta name="lang:search.result.one" content="1 matching document">
<meta name="lang:search.result.other" content="# matching documents">
<meta name="lang:search.languages" content="">
<meta name="lang:search.tokenizer" content="[\s\-]+">
<meta name="lang:search.tokenizer" content="$page.properties.("material.search.tokenizer"); null="[\\s\\-]+"$">
2 changes: 1 addition & 1 deletion src/main/paradox/customization.md
Expand Up @@ -68,7 +68,7 @@ The directory layout of the theme is as follows:
│   ├── header.st # Page header
│   ├── nav.st # Main navigation
│   ├── poweredby.st # Powered-by footer message
│   ├── search.st # Search (currently not used)
│   ├── search.st # Search box
│   ├── social.st # Social links
│   ├── source.st # Repository information
│   └── toc.st # Table of contents
Expand Down
49 changes: 49 additions & 0 deletions src/main/paradox/getting-started.md
Expand Up @@ -204,6 +204,55 @@ This will add a `lang` attribute to the top-level `html` element:

If no language is set English (`en`) is assumed.

## Site search

Site search must be explicitly enabled by setting `material.search`:

@@ snip [build.sbt]($root$/build.sbt) { #search }

In addition, you need to generate a `search_index.json` that contains all your
site's content and add it to your site.
See the @ref:[search index generation instructions](search.md) on how to do this.

<!--
### Search language
Site search is implemented using [lunr.js][22], which includes stemmers for the
English language by default, while stemmers for other languages are included
with [lunr-languages][23], both of which are integrated with this theme. Support
for other languages and even multilingual search can be activated by setting
`material.search` to a comma-separated list of supported 2-letter
language codes, e.g.:
@@ snip [build.sbt]($root$/build.sbt) { #search-multi }
This will automatically load the stemmers for the specified languages and
set them up with site search, nothing else to be done.
At the time of writing, the following languages are supported: English `en`,
French `fr`, Spanish `es`, Italian `it`, Japanese `jp`, Dutch `du`, Danish `da`,
Portguese `pt`, Finnish `fi`, Romanian `ro`, Hungarian `hu`, Russian `ru`,
Norwegian `no`, Swedish `sv` and Turkish `tr`.
@@@ warning { title="Only specify the languages you really need" }
Be aware that including support for other languages increases the general
JavaScript payload by around 20kb (without gzip) and by another 15-30kb per
language.
@@@
-->

### Search tokenization

The separator for tokenization can also be customized, which makes it possible
to index parts of words that are separated by `-` or `.` for example:

@@ snip [build.sbt]($root$/build.sbt) { #search-tokenizer }

[22]: https://lunrjs.com
[23]: https://github.com/MihaiValentin/lunr-languages

## Google Analytics integration

The theme makes it easy to integrate site tracking with Google Analytics.
Expand Down
6 changes: 6 additions & 0 deletions src/main/paradox/index.md
Expand Up @@ -2,6 +2,7 @@

@@@ index
- [getting-started](getting-started.md)
- [search](search.md)
- [customization](customization.md)
- [specimen](specimen/index.md)
- [release notes](release-notes.md)
Expand Down Expand Up @@ -41,11 +42,16 @@ For detailed instructions see the @ref:[getting started guide](getting-started.m
* Easily customizable primary and accent color, fonts, favicon and logo;
integrated with Google Analytics and GitHub.

* Well-designed [search interface] accessible through hotkeys (<kbd>F</kbd> or
<kbd>S</kbd>), intelligent grouping of search results, search term
highlighting and lazy loading.

* Support most Paradox features except for [groups].

The @ref:[specimen pages] show examples of the theme in action, such as
[callouts] and [tabbed snippets].

[search interface]: search.md
[specimen pages]: specimen/index.md
[callouts]: specimen/callouts.md
[tabbed snippets]: specimen/tabbed-snippets.md
Expand Down
39 changes: 39 additions & 0 deletions src/main/paradox/search.md
@@ -0,0 +1,39 @@
# Search index generation

To enable @ref:[site search](getting-started.md#site-search) you must also
generate a `search_index.json` file which contains the content of your site.
The following sections describe how to configure sbt to extract text from the
HTML files generated by Paradox.

## Project dependencies

Add the following lines to either `project/plugins.sbt` or `project/build.sbt`
so the index generator can use [jsoup] to process the HTML content and [circe]
to serialize the index to JSON.

@@ snip [project/plugins.sbt]($root$/project/plugins.sbt) { #search-dependencies }

[jsoup]: https://jsoup.org/
[circe]: http://circe.io/

## Add generator output to the site

Depending on whether you are using Paradox in a stand-alone fashion or sbt-site's
[Paradox generator], you need to add a line to your `build.sbt` to make the search
index part of your site.

Stand-alone Paradox
: @@ snip [build.sbt]($root$/build.sbt) { #search-paradox }

Paradox using sbt-site
: @@ snip [build.sbt]($root$/build.sbt) { #search-sbt-site }

[Paradox generator]: http://www.scala-sbt.org/sbt-site/generators/paradox.html

## Generate the search index

The following code does the bulk work of reading in each HTML file to extract
the text for each header. You should place it in a file named
`project/SearchIndex.scala`.

@@ snip [project/SearchIndex.scala]($root$/project/SearchIndex.scala)

0 comments on commit ed7ea1c

Please sign in to comment.