Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add search functionality #189

Merged
merged 5 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
kotlin("jvm") version "1.8.20"
kotlin("plugin.serialization") version "1.8.20"
application
}

Expand Down Expand Up @@ -29,6 +30,7 @@ dependencies {
implementation("org.jsoup:jsoup:1.15.4")

implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")

implementation("org.eclipse.jetty:jetty-server:11.0.14")
implementation("org.eclipse.jetty:jetty-servlet:11.0.14")
Expand Down
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
@@ -0,0 +1,44 @@
package nl.avisi.structurizr.site.generatr.site.model

import com.structurizr.documentation.Decision
import com.structurizr.documentation.Format
import com.structurizr.documentation.Section
import com.vladsch.flexmark.ast.Heading
import com.vladsch.flexmark.ast.Paragraph
import com.vladsch.flexmark.parser.Parser

private val parser = Parser.builder().build()

fun Decision.contentText(): String {
if (format != Format.Markdown)
return ""

return extractText(content)
}

fun Section.contentText(): String {
if (format != Format.Markdown)
return ""

return extractText(content)
}

private fun extractText(content: String): String {
val document = parser.parse(content)
if (!document.hasChildren())
return ""

return document
.children
.filterIsInstance<Heading>()
.drop(1) // ignore title
.joinToString(" ") { it.text.toString() }
.plus(" ")
.plus(
document
.children
.filterIsInstance<Paragraph>()
.joinToString(" ") { it.chars.toString().trim() }
)
.trim()
}
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,48 @@
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 language: String = generatorContext.workspace.views
.configuration.properties.getOrDefault("structurizr.style.search.language", "")

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()
dirkgroot marked this conversation as resolved.
Show resolved Hide resolved
)
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()
)
}
Loading