Skip to content

Commit

Permalink
Add search input and results page
Browse files Browse the repository at this point in the history
This is based on Lunrjs.
We assemble documents when generating the site, then Lunr builds
an index on the client on page load and we search through that.
  • Loading branch information
jp7677 committed Apr 10, 2023
1 parent 581a30e commit 16ba3da
Show file tree
Hide file tree
Showing 20 changed files with 810 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import java.security.MessageDigest

fun copySiteWideAssets(exportDir: File) {
copySiteWideAsset(exportDir, "/css/style.css")
copySiteWideAsset(exportDir, "/js/header.js")
copySiteWideAsset(exportDir, "/js/search.js")
copySiteWideAsset(exportDir, "/js/auto-reload.js")
}

Expand Down Expand Up @@ -89,6 +91,14 @@ private fun generateStyle(context: GeneratorContext, exportDir: File) {
color: $secondary!important;
background-color: $primary!important;
}
.input.has-site-branding {
color: dimgrey!important;
background-color: white!important;
}
.input.has-site-branding:focus {
border-color: $secondary!important;
box-shadow: 0 0 0 0.125em $secondary;
}
""".trimIndent()

file.writeText(content)
Expand All @@ -100,6 +110,7 @@ private fun generateHtmlFiles(context: GeneratorContext, exportDir: File) {
add { writeHtmlFile(branchDir, HomePageViewModel(context)) }
add { writeHtmlFile(branchDir, WorkspaceDecisionsPageViewModel(context)) }
add { writeHtmlFile(branchDir, SoftwareSystemsPageViewModel(context)) }
add { writeHtmlFile(branchDir, SearchViewModel(context)) }

context.workspace.documentation.sections
.filter { it.order != 1 }
Expand Down Expand Up @@ -140,6 +151,7 @@ private fun writeHtmlFile(exportDir: File, viewModel: PageViewModel) {
appendHTML().html {
when (viewModel) {
is HomePageViewModel -> homePage(viewModel)
is SearchViewModel -> searchPage(viewModel)
is SoftwareSystemsPageViewModel -> softwareSystemsPage(viewModel)
is SoftwareSystemHomePageViewModel -> softwareSystemHomePage(viewModel)
is SoftwareSystemContextPageViewModel -> softwareSystemContextPage(viewModel)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package nl.avisi.structurizr.site.generatr.site.model
import nl.avisi.structurizr.site.generatr.site.GeneratorContext

class HeaderBarViewModel(pageViewModel: PageViewModel, generatorContext: GeneratorContext) {
val url = pageViewModel.url
val logo = logoPath(generatorContext)?.let { ImageViewModel(pageViewModel, "/$it") }
val hasLogo = logo != null
val titleLink = LinkViewModel(pageViewModel, generatorContext.workspace.name, HomePageViewModel.url())
val searchLink = LinkViewModel(pageViewModel, generatorContext.workspace.name, SearchViewModel.url())
val branches = generatorContext.branches
.map { BranchHomeLinkViewModel(pageViewModel, it) }
val currentBranch = generatorContext.currentBranch
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package nl.avisi.structurizr.site.generatr.site.model

import nl.avisi.structurizr.site.generatr.includedSoftwareSystem
import nl.avisi.structurizr.site.generatr.site.GeneratorContext
import nl.avisi.structurizr.site.generatr.site.model.indexing.home
import nl.avisi.structurizr.site.generatr.site.model.indexing.softwareSystemComponents
import nl.avisi.structurizr.site.generatr.site.model.indexing.softwareSystemContainers
import nl.avisi.structurizr.site.generatr.site.model.indexing.softwareSystemContext
import nl.avisi.structurizr.site.generatr.site.model.indexing.softwareSystemDecisions
import nl.avisi.structurizr.site.generatr.site.model.indexing.softwareSystemHome
import nl.avisi.structurizr.site.generatr.site.model.indexing.softwareSystemRelationships
import nl.avisi.structurizr.site.generatr.site.model.indexing.softwareSystemSections
import nl.avisi.structurizr.site.generatr.site.model.indexing.workspaceDecisions
import nl.avisi.structurizr.site.generatr.site.model.indexing.workspaceSections

class SearchViewModel(generatorContext: GeneratorContext) : PageViewModel(generatorContext) {
override val pageSubTitle = "Search results"
override val url = url()

val documents = buildList {
add(home(generatorContext.workspace.documentation, this@SearchViewModel))
addAll(workspaceDecisions(generatorContext.workspace.documentation, this@SearchViewModel))
addAll(workspaceSections(generatorContext.workspace.documentation, this@SearchViewModel))
addAll(
generatorContext.workspace.model.softwareSystems
.filter { it.includedSoftwareSystem }
.flatMap {
buildList {
add(softwareSystemHome(it, this@SearchViewModel))
add(softwareSystemContext(it, this@SearchViewModel))
add(softwareSystemContainers(it, this@SearchViewModel))
add(softwareSystemComponents(it, this@SearchViewModel))
add(softwareSystemRelationships(it, this@SearchViewModel))
addAll(softwareSystemDecisions(it, this@SearchViewModel))
addAll(softwareSystemSections(it, this@SearchViewModel))
}
}
.mapNotNull { it },
)
}.mapNotNull { it }

companion object {
fun url() = "/search"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package nl.avisi.structurizr.site.generatr.site.model.indexing

import kotlinx.serialization.Serializable

@Serializable
data class Document(val href: String, val type: String, val title: String, val text: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package nl.avisi.structurizr.site.generatr.site.model.indexing

import com.structurizr.documentation.Documentation
import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory
import nl.avisi.structurizr.site.generatr.site.model.HomePageViewModel
import nl.avisi.structurizr.site.generatr.site.model.PageViewModel
import nl.avisi.structurizr.site.generatr.site.model.contentText
import nl.avisi.structurizr.site.generatr.site.model.contentTitle

fun home(documentation: Documentation, viewModel: PageViewModel) = documentation.sections.firstOrNull()
?.let { section ->
Document(
HomePageViewModel.url().asUrlToDirectory(viewModel.url),
"Home",
section.contentTitle(),
"${section.contentTitle()} ${section.contentText()}".trim()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package nl.avisi.structurizr.site.generatr.site.model.indexing

import com.structurizr.model.SoftwareSystem
import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory
import nl.avisi.structurizr.site.generatr.site.model.PageViewModel
import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel

fun softwareSystemComponents(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = softwareSystem.containers
.sortedBy { it.name }
.flatMap { container ->
container.components
.sortedBy { it.name }
.flatMap { component ->
listOf(component.name, component.description, component.technology)
}
}
.filter { it != null && it.isNotBlank() }
.joinToString(" ")
.ifBlank { null }
?.let {
Document(
SoftwareSystemPageViewModel.url(softwareSystem, SoftwareSystemPageViewModel.Tab.COMPONENT)
.asUrlToDirectory(viewModel.url),
"Component views",
"${softwareSystem.name} | Component views",
it
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package nl.avisi.structurizr.site.generatr.site.model.indexing

import com.structurizr.model.SoftwareSystem
import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory
import nl.avisi.structurizr.site.generatr.site.model.PageViewModel
import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel

fun softwareSystemContainers(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = softwareSystem.containers
.sortedBy { it.name }
.flatMap { container ->
listOf(container.name, container.description, container.technology)
}
.filter { it != null && it.isNotBlank() }
.joinToString(" ")
.ifBlank { null }
?.let {
Document(
SoftwareSystemPageViewModel.url(softwareSystem, SoftwareSystemPageViewModel.Tab.CONTAINER)
.asUrlToDirectory(viewModel.url),
"Container views",
"${softwareSystem.name} | Container views",
it
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package nl.avisi.structurizr.site.generatr.site.model.indexing

import com.structurizr.model.SoftwareSystem
import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory
import nl.avisi.structurizr.site.generatr.site.model.PageViewModel
import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel

fun softwareSystemContext(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = Document(
SoftwareSystemPageViewModel.url(softwareSystem, SoftwareSystemPageViewModel.Tab.SYSTEM_CONTEXT)
.asUrlToDirectory(viewModel.url),
"Context views",
"${softwareSystem.name} | Context views",
"${softwareSystem.name} ${softwareSystem.description}".trim()
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package nl.avisi.structurizr.site.generatr.site.model.indexing

import com.structurizr.model.SoftwareSystem
import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory
import nl.avisi.structurizr.site.generatr.site.model.PageViewModel
import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel
import nl.avisi.structurizr.site.generatr.site.model.contentText

fun softwareSystemDecisions(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = softwareSystem.documentation
.decisions
.map { decision ->
Document(
"${
SoftwareSystemPageViewModel.url(
softwareSystem,
SoftwareSystemPageViewModel.Tab.HOME
)
}/decisions/${decision.id}".asUrlToDirectory(viewModel.url),
"Decision",
"${softwareSystem.name} | ${decision.title}",
"${decision.title} ${decision.contentText()}".trim()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package nl.avisi.structurizr.site.generatr.site.model.indexing

import com.structurizr.model.SoftwareSystem
import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory
import nl.avisi.structurizr.site.generatr.site.model.PageViewModel
import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel
import nl.avisi.structurizr.site.generatr.site.model.contentText
import nl.avisi.structurizr.site.generatr.site.model.contentTitle

fun softwareSystemHome(softwareSystem: SoftwareSystem, viewModel: PageViewModel): Document? {
val (contentTitle, contentText) = softwareSystem.section()
val propertyValues = softwareSystem.propertyValues()

if (contentTitle.isBlank() && contentText.isBlank() && propertyValues.isBlank())
return null

return Document(
SoftwareSystemPageViewModel.url(softwareSystem, SoftwareSystemPageViewModel.Tab.HOME)
.asUrlToDirectory(viewModel.url),
"Software System Info",
"${softwareSystem.name} | ${contentTitle.ifEmpty { "Info" }}",
"$contentTitle $contentText $propertyValues".trim(),
)
}

private fun SoftwareSystem.section() = documentation.sections.firstOrNull()?.let {
it.contentTitle() to it.contentText()
} ?: ("" to "")

private fun SoftwareSystem.propertyValues() = properties.values.joinToString(" ")
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package nl.avisi.structurizr.site.generatr.site.model.indexing

import com.structurizr.model.SoftwareSystem
import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory
import nl.avisi.structurizr.site.generatr.site.model.PageViewModel
import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel

fun softwareSystemRelationships(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = softwareSystem.relationships
.filter { r -> r.source == softwareSystem }
.sortedBy { it.destination.name }
.flatMap { relationship ->
listOf(relationship.destination.name, relationship.description, relationship.technology)
}
.plus(
softwareSystem.model.softwareSystems
.sortedBy { it.name }
.filterNot { system -> system == softwareSystem }
.flatMap { other ->
other.relationships
.filter { r -> r.destination == softwareSystem }
.sortedBy { it.source.name }
.flatMap { relationship ->
listOf(relationship.source.name, relationship.description, relationship.technology)
}
}
)
.filter { it != null && it.isNotBlank() }
.joinToString(" ")
.ifBlank { null }
?.let {
Document(
SoftwareSystemPageViewModel.url(softwareSystem, SoftwareSystemPageViewModel.Tab.DEPENDENCIES)
.asUrlToDirectory(viewModel.url),
"Dependencies",
"${softwareSystem.name} | Dependencies",
it
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package nl.avisi.structurizr.site.generatr.site.model.indexing

import com.structurizr.model.SoftwareSystem
import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory
import nl.avisi.structurizr.site.generatr.site.model.PageViewModel
import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel
import nl.avisi.structurizr.site.generatr.site.model.contentText
import nl.avisi.structurizr.site.generatr.site.model.contentTitle

fun softwareSystemSections(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = softwareSystem.documentation
.sections
.drop(1) // Drop software system home
.map { section ->
Document(
"${
SoftwareSystemPageViewModel.url(
softwareSystem,
SoftwareSystemPageViewModel.Tab.HOME
)
}/sections/${section.order}".asUrlToDirectory(viewModel.url),
"Documentation",
"${softwareSystem.name} | ${section.contentTitle()}",
"${section.contentTitle()} ${section.contentText()}".trim()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package nl.avisi.structurizr.site.generatr.site.model.indexing

import com.structurizr.documentation.Documentation
import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory
import nl.avisi.structurizr.site.generatr.site.model.PageViewModel
import nl.avisi.structurizr.site.generatr.site.model.contentText

fun workspaceDecisions(documentation: Documentation, viewModel: PageViewModel) = documentation.decisions
.map { decision ->
Document(
"/decisions/${decision.id}".asUrlToDirectory(viewModel.url),
"Workspace Decision",
decision.title,
"${decision.title} ${decision.contentText()}".trim()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package nl.avisi.structurizr.site.generatr.site.model.indexing

import com.structurizr.documentation.Documentation
import nl.avisi.structurizr.site.generatr.normalize
import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory
import nl.avisi.structurizr.site.generatr.site.model.PageViewModel
import nl.avisi.structurizr.site.generatr.site.model.contentText
import nl.avisi.structurizr.site.generatr.site.model.contentTitle

fun workspaceSections(documentation: Documentation, viewModel: PageViewModel) = documentation.sections
.drop(1) // Drop home
.map { section ->
Document(
"/${section.contentTitle().normalize()}".asUrlToDirectory(viewModel.url),
"Workspace Documentation",
section.contentTitle(),
"${section.contentTitle()} ${section.contentText()}".trim()
)
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package nl.avisi.structurizr.site.generatr.site.views

import kotlinx.html.*
import nl.avisi.structurizr.site.generatr.site.asUrlToFile
import nl.avisi.structurizr.site.generatr.site.model.HeaderBarViewModel
import nl.avisi.structurizr.site.generatr.site.model.ImageViewModel
import nl.avisi.structurizr.site.generatr.site.model.LinkViewModel

fun BODY.pageHeader(viewModel: HeaderBarViewModel) {
script(
type = ScriptType.textJavaScript,
src = "../" + "/header.js".asUrlToFile(viewModel.url)
) { }
nav(classes = "navbar") {
role = "navigation"
attributes["aria-label"] = "main navigation"
Expand All @@ -18,6 +23,14 @@ fun BODY.pageHeader(viewModel: HeaderBarViewModel) {
}
div(classes = "navbar-menu has-site-branding") {
div(classes = "navbar-end") {
div(classes = "navbar-item") {
input(classes = "input is-small is-rounded has-site-branding") {
id = "search"
type = InputType.search
placeholder = "Search (3 characters required)..."
onKeyUp = "redirect(event, value, '${viewModel.searchLink.relativeHref}')"
}
}
div(classes = "navbar-item has-dropdown is-hoverable") {
a(classes = "navbar-link has-site-branding") {
+viewModel.currentBranch
Expand Down
Loading

0 comments on commit 16ba3da

Please sign in to comment.