From 8ff8764419002772be2623c03a857523ec2a6ec7 Mon Sep 17 00:00:00 2001 From: Arunanshu Dey Date: Mon, 30 Mar 2026 20:57:38 +0530 Subject: [PATCH] feats: fillering using Index Honcon config data ,issue(#589) --- build.scala | 81 ++++++++- src/main.js | 302 ++++++++++++++++++++++++-------- src/templates/nav.template.html | 147 ++++++++++------ 3 files changed, 396 insertions(+), 134 deletions(-) diff --git a/build.scala b/build.scala index bd3e567d..d07f5e11 100644 --- a/build.scala +++ b/build.scala @@ -48,7 +48,8 @@ object Main extends CommandIOApp("build", "builds the site") { def main = opts.map { case Subcommand.Build(destination) => - Files[IO].deleteRecursively(destination).voidError *> + IO.println(s"Building to: $destination") *> + Files[IO].deleteRecursively(destination).voidError *> LaikaBuild.build(FilePath.fromFS2Path(destination)).as(ExitCode.Success) case Subcommand.Serve(port) => @@ -85,12 +86,23 @@ object LaikaBuild { "https://raw.githubusercontent.com/typelevel/.github/refs/heads/main/SECURITY.md" ).toURL() + val metadataContent = scala.util + .Try( + java.nio.file.Files.readString( + java.nio.file.Paths.get("target/search/metadata.json") + ) + ) + .getOrElse("{}") InputTree[IO] .addDirectory("src") // Laika skips .dotfiles by default .addDirectory("src/.well-known", Path.Root / ".well-known") + .addString(metadataContent, Path.Root / "search" / "metadata.json") .addInputStream( - IO.blocking(securityPolicy.openStream()), + IO.blocking(securityPolicy.openStream()).handleErrorWith { _ => + IO.println("fetching security.md failed, skipping.") *> + IO.pure(new java.io.ByteArrayInputStream(Array.emptyByteArray)) + }, Path.Root / "security.md" ) .addClassResource[this.type]( @@ -163,10 +175,72 @@ object LaikaBuild { index .from(tree) .toFile(destination / "search" / "searchIndex.idx") - .render + .render *> + writeMetadata(tree.root, destination) } } } + + private def writeMetadata( + tree: DocumentTreeRoot, + destination: FilePath + ): IO[Unit] = { + import java.time.OffsetDateTime + + case class PostMetadata( + path: String, + authors: Seq[String], + tags: Seq[String], + date: String + ) + + // walk all documents, keep only ones with a date field (blog posts + events) + val posts: Seq[PostMetadata] = tree.allDocuments.flatMap { doc => + doc.config.get[OffsetDateTime]("date").toOption.map { date => + val path = "/" + doc.path.toString + .stripPrefix("/") + .stripSuffix(".md") + ".html" + val authors = doc.config.get[Seq[String]]("author.github") match { + case Right(list) => list + case _ => + doc.config.get[String]("author.github") match { + case Right(single) => Seq(single) + case _ => Nil + } + } + val tags = doc.config + .get[Seq[String]]("tags") + .getOrElse(Nil) + PostMetadata(path, authors, tags, date.toLocalDate.toString) + } + } + + // build JSON manually — no extra dependency needed + def jsonStr(s: String) = s""""$s"""" + def jsonArr(items: Seq[String]) = + items.map(jsonStr).mkString("[", ", ", "]") + + val entries = posts.map { p => + s""" ${jsonStr(p.path)}: {""" + + s""""authors": ${jsonArr(p.authors)}, """ + + s""""tags": ${jsonArr(p.tags)}, """ + + s""""date": ${jsonStr(p.date)}}""" + } + + val json = entries.mkString("{\n", ",\n", "\n}") + + // write to target/search/metadata.json + val outPath = java.nio.file.Paths.get( + destination.toString, + "search", + "metadata.json" + ) + + IO.blocking { + java.nio.file.Files.createDirectories(outPath.getParent) + java.nio.file.Files.writeString(outPath, json) + }.void + } } object LaikaCustomizations { @@ -180,6 +254,7 @@ object LaikaCustomizations { import laika.ast.* import laika.format.* import laika.theme.* + // import ciris.{Config, ConfigValue, ObjectValue, ArrayValue, Field} def addAnchorLinks(fmt: TagFormatter, h: Header) = { val link = h.options.id.map { id => diff --git a/src/main.js b/src/main.js index 2affcbf9..51f7ca33 100644 --- a/src/main.js +++ b/src/main.js @@ -1,73 +1,229 @@ -function handleBurgerClick(el) { - const target = el.dataset.target; - const $target = document.getElementById(target); - el.classList.toggle('bulma-is-active'); - $target.classList.toggle('bulma-is-active'); -} - -// search - -function showSearchModal() { - const modal = document.getElementById("search-modal"); - if (!modal.classList.contains("bulma-is-active")) { - modal.classList.add("bulma-is-active"); - const input = document.getElementById("search-input"); - input.value = ""; - input.focus(); - const results = document.getElementById("search-results"); - results.innerHTML = `

Type to search...

`; - } -} - -function hideSearchModal() { - const modal = document.getElementById("search-modal"); - if (modal.classList.contains("bulma-is-active")) - modal.classList.remove("bulma-is-active"); -} - -function renderHit(hit) { - const link = `${hit.fields.path}.html` - const title = hit.highlights["title"] || hit.fields["title"] - const preview = hit.highlights["body"] - const tags = [] - if (link.startsWith("/blog")) tags.push("blog") - const renderedTags = tags.map(tag => `${tag}`).join("") - - return ` -
-
-
- ${title} -  ${renderedTags} -

${preview}

-
-
-
- ` -} - -const searchWorker = new Worker("/search/worker.js"); - -searchWorker.onmessage = function (e) { - const fallback = `

No results found

` - const markup = e.data.map(renderHit).join("") || fallback - const results = document.getElementById("search-results"); - results.innerHTML = markup -} - -function onSearchInput(event) { - searchWorker.postMessage({"query": event.target.value || ""}); -} - -// Keyboard shortcuts: `/` to open, `Escape` to close -window.addEventListener("keydown", (event) => { - if (event.defaultPrevented) return; - if (event.code == "Slash") { - event.preventDefault(); - showSearchModal(); - } - if (event.code == "Escape") { - event.preventDefault(); - hideSearchModal(); - } -}); +function handleBurgerClick(el) { + const target = el.dataset.target; + const $target = document.getElementById(target); + el.classList.toggle('bulma-is-active'); + $target.classList.toggle('bulma-is-active'); +} + +// search + +function showSearchModal() { + const modal = document.getElementById("search-modal"); + if (!modal.classList.contains("bulma-is-active")) { + modal.classList.add("bulma-is-active"); + const input = document.getElementById("search-input"); + input.value = ""; + input.focus(); + const results = document.getElementById("search-results"); + results.innerHTML = `

Type to search...

`; + } +} + +function hideSearchModal() { + const modal = document.getElementById("search-modal"); + if (modal.classList.contains("bulma-is-active")) + modal.classList.remove("bulma-is-active"); +} +// + +let postMetadata = {}; +let isFilter = false; + +fetch("/search/metadata.json") + .then(r => r.json()) + .then(data => { postMetadata = data; renderResults(); }) + .catch(() => { postMetadata = {}; }); + + +let filters = { + tags: [], + authors: [] +}; + +function switchSearchMode(mode) { + const textTab = document.getElementById("tab-text"); + const metaTab = document.getElementById("tab-meta"); + + const textBox = document.getElementById("text-search-container"); + const metaBox = document.getElementById("meta-search-container"); + + if (mode === "text") { + textTab.classList.add("bulma-is-active"); + metaTab.classList.remove("bulma-is-active"); + + textBox.style.display = "block"; + metaBox.style.display = "none"; + + document.getElementById("search-input").focus(); + } else { + metaTab.classList.add("bulma-is-active"); + textTab.classList.remove("bulma-is-active"); + + textBox.style.display = "none"; + metaBox.style.display = "block"; + + document.getElementById("meta-input").focus(); + } +} +function handleKey(e) { + if (e.key === "Enter") { + const value = e.target.value.trim(); + if (!value) return; + + if (value.startsWith("tag:")) { + addFilter("tags", value.replace("tag:", "")); + } else if (value.startsWith("author:")) { + addFilter("authors", value.replace("author:", "")); + } + e.target.value = ""; + } +} + + +function addFilter(type, value) { + value = value.toLowerCase(); + + if (!filters[type].includes(value)) { + filters[type].push(value); + } + + if(filters[type].length > 0) isFilter = true; + renderChips(); + renderResults(); +} + +function removeFilter(type, value) { + filters[type] = filters[type].filter(v => v !== value); + if(filters["tags"].length == 0 && filters["authors"].length == 0) isFilter = false; + renderChips(); + renderResults(); +} + +function renderChips() { + const container = document.getElementById("filter-chips"); + + const chips = [ + ...filters.tags.map(t => ({ type: "tags", value: t })), + ...filters.authors.map(a => ({ type: "authors", value: a })) + ]; + + container.innerHTML = chips.map(c => ` + + ${c.type.slice(0,-1)}: ${c.value} + × + + `).join(""); + } + + function renderResults() { + + const results = Object.entries(postMetadata).filter(([path, meta]) => { + + const tagMatch = + filters.tags.length === 0 || + filters.tags.every(f => + (meta.tags || []).some(t => t.toLowerCase().includes(f)) + ); + + const authorMatch = + filters.authors.length === 0 || + filters.authors.every(f => + (meta.authors || []).some(a => a.toLowerCase().includes(f)) + ); + + return tagMatch && authorMatch; + }); + + // console.log(results) + const html = results.map(([path, meta]) => ` +
+
+ + + ${path.split("/").pop().replace(".html","")} + + +
+ ${(meta.tags || []).map(t => ` + + ${t} + + `).join("")} + + ${(meta.authors || []).map(a => ` + + ${a} + + `).join("")} + + + ${meta.date || ""} + +
+ +
+
+`).join(""); + + + if(isFilter) document.getElementById("metadata-results").innerHTML = + html || `

No results found

`; + else document.getElementById("metadata-results").innerHTML = ""; +} +// +function renderHit(hit) { + const link = `${hit.fields.path}.html` + const title = hit.highlights["title"] || hit.fields["title"] + const preview = hit.highlights["body"] + const tags = [] + if (link.startsWith("/blog")) tags.push("blog") + const renderedTags = tags.map(tag => `${tag}`).join("") + + return ` +
+
+
+ ${title} +  ${renderedTags} +

${preview}

+
+
+
+ ` +} + +const searchWorker = new Worker("/search/worker.js"); + +searchWorker.onmessage = function (e) { + const fallback = `

No results found

` + const markup = e.data.map(renderHit).join("") || fallback + const results = document.getElementById("search-results"); + results.innerHTML = markup +} + +function onSearchInput(event) { + searchWorker.postMessage({"query": event.target.value || ""}); +} + +// Keyboard shortcuts: `/` to open, `Escape` to close +window.addEventListener("keydown", (event) => { + if (event.defaultPrevented) return; + if (event.code == "Slash") { + event.preventDefault(); + showSearchModal(); + } + if (event.code == "Escape") { + event.preventDefault(); + hideSearchModal(); + } +}); diff --git a/src/templates/nav.template.html b/src/templates/nav.template.html index 2b97b99c..7633e808 100644 --- a/src/templates/nav.template.html +++ b/src/templates/nav.template.html @@ -1,58 +1,89 @@ - - - -
-
-
-
-

- - - @:svg(fa-magnifying-glass) - -

-
-
-
-
- - / to open search   - Esc to close - -
-
-
+ + + +
+
+
+
+

+ + + @:svg(fa-magnifying-glass) + +

+
+ +
+
+ +
+
+
+
+ +
+ + +
+
\ No newline at end of file