diff --git a/Makefile b/Makefile
index 0f2ae09..06d9f2e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,15 +1,28 @@
.PHONY: all clean norm help
-# Default target: full build with search index
-all:
- @echo "Building project (full rebuild with search index)..."
- emacs -Q --script build-site.el
- python search-index-json.py
+VENV := .venv
+PY := $(VENV)/bin/python
+PIP := $(VENV)/bin/pip
+
+all: build search
+
+build:
+ @echo "Building project..."
+ emacs -Q --script build-site.el
+
+search: $(VENV)
+ @echo "Generating search index"
+ $(PY) search-index-json.py
+
+$(VENV):
+ @echo "Generating virtual environment"
+ python3 -m venv $(VENV)
+ $(PIP) install -r requirements.txt
-# Clean output directory
clean:
@echo "Cleaning output directory..."
rm -rf output/
+ rm -rf $(VENV)
norm:
@echo "sorting out the backups..."
@@ -19,6 +32,8 @@ norm:
help:
@echo "Available targets:"
@echo " make - Full rebuild"
+ @echo " make build - Rebuild the output directory"
@echo " make clean - Remove output directory"
@echo " make norm - Move all files that end with ~ to the backup folder"
+ @echo " make search - creates the search index"
@echo " make help - Show this help message"
diff --git a/assets/scripts/competency-status-board.js b/assets/scripts/competency-status-board.js
index 0d54656..25cb779 100644
--- a/assets/scripts/competency-status-board.js
+++ b/assets/scripts/competency-status-board.js
@@ -1,9 +1,9 @@
const STATES = [
- { key: "completed", label: "Completed" },
- { key: "manager_review", label: "Manager Review" },
- { key: "in_progress", label: "In Progress" },
- { key: "not_started", label: "Not Started" },
- { key: "comments", label: "Comments" }
+ { key: "completed", label: "Completed" },
+ { key: "manager_review", label: "Manager Review" },
+ { key: "in_progress", label: "In Progress" },
+ { key: "not_started", label: "Not Started" },
+ { key: "comments", label: "Comments" },
];
function isMobile() {
@@ -20,9 +20,9 @@ function populateMobileControls(items) {
itemSelect.innerHTML = "";
fromSelect.innerHTML = "";
- toSelect.innerHTML = "";
+ toSelect.innerHTML = "";
- items.forEach(item => {
+ items.forEach((item) => {
const opt = document.createElement("option");
opt.value = item.id;
opt.textContent = item.title;
@@ -30,7 +30,7 @@ function populateMobileControls(items) {
itemSelect.appendChild(opt);
});
- STATES.forEach(s => {
+ STATES.forEach((s) => {
fromSelect.appendChild(new Option(s.label, s.key));
toSelect.appendChild(new Option(s.label, s.key));
});
@@ -46,8 +46,8 @@ function populateMobileControls(items) {
document.getElementById("move-confirm").addEventListener("click", async () => {
const itemId = document.getElementById("move-item").value;
- const from = document.getElementById("move-from").value;
- const to = document.getElementById("move-to").value;
+ const from = document.getElementById("move-from").value;
+ const to = document.getElementById("move-to").value;
if (!itemId || !to) {
alert("Select an item and target column");
@@ -62,7 +62,7 @@ document.getElementById("move-confirm").addEventListener("click", async () => {
const res = await fetch(`/api/competencies/items/${itemId}/state`, {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ state: to })
+ body: JSON.stringify({ state: to }),
});
if (res.ok) {
@@ -72,8 +72,6 @@ document.getElementById("move-confirm").addEventListener("click", async () => {
}
});
-
-
let draggedItemId = null;
function countByState(items) {
@@ -113,11 +111,11 @@ function createColumn(state, items, counts) {
${state.label}
${counts[state.key] || 0}
`;
-
+
const list = document.createElement("div");
list.className = "kanban-list";
- list.addEventListener("dragover", e => e.preventDefault());
+ list.addEventListener("dragover", (e) => e.preventDefault());
list.addEventListener("drop", async () => {
if (!draggedItemId) return;
@@ -125,7 +123,7 @@ function createColumn(state, items, counts) {
const res = await fetch(`/api/competencies/items/${draggedItemId}/state`, {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ state: state.key })
+ body: JSON.stringify({ state: state.key }),
});
if (res.ok) {
@@ -136,8 +134,8 @@ function createColumn(state, items, counts) {
});
items
- .filter(i => i.state === state.key)
- .forEach(i => list.appendChild(createCard(i)));
+ .filter((i) => i.state === state.key)
+ .forEach((i) => list.appendChild(createCard(i)));
col.appendChild(header);
col.appendChild(list);
@@ -152,12 +150,11 @@ function renderBoard(items) {
const counts = countByState(items);
- STATES.forEach(state => {
+ STATES.forEach((state) => {
board.appendChild(createColumn(state, items, counts));
});
}
-
async function loadBoard() {
const res = await fetch("/api/competencies/items");
const items = await res.json();
@@ -165,5 +162,4 @@ async function loadBoard() {
populateMobileControls(items);
}
-
loadBoard();
diff --git a/assets/scripts/search.js b/assets/scripts/search.js
index 5eeb96a..cad145d 100644
--- a/assets/scripts/search.js
+++ b/assets/scripts/search.js
@@ -1,138 +1,16 @@
let lunrIndex;
let documents = [];
-
-/* ------------------------------
- JSON → documents
--------------------------------- */
-function extractDocuments(node, currentPath = "") {
- if (node.type === "folder" && node.children) {
- const nextPath = currentPath
- ? `${currentPath}/${node.name}`
- : node.name;
-
- node.children.forEach(child => {
- extractDocuments(child, nextPath);
- });
- }
-
- if (node.type === "file") {
- const filePath = currentPath
- ? `${currentPath}/${node.name}`
- : node.name;
-
- documents.push({
- id: documents.length.toString(),
- title: node.name,
- content: node.content || "",
- path: "/" + filePath.replace(/^output\//, "")
- });
- }
-}
-
-/* ------------------------------
- Lunr index
--------------------------------- */
-function buildIndex() {
- lunrIndex = lunr(function () {
- this.ref("id");
- this.field("title", { boost: 10 });
- this.field("content");
-
- documents.forEach(doc => this.add(doc));
- });
-
- console.log("Lunr index built with", documents.length, "documents");
-}
-
-async function initSearch() {
- const response = await fetch("test.json");
- const json = await response.json();
-
- extractDocuments(json);
- buildIndex();
-}
-
-initSearch();
-
-/* ------------------------------
- Fuzzy search
--------------------------------- */
-function buildFuzzyQuery(query) {
- return query
- .split(/\s+/)
- .map(term => `${term}~1`)
- .join(" ");
-}
-
-function search(query) {
- if (!lunrIndex) return [];
-
- const results = lunrIndex.search(buildFuzzyQuery(query));
-
- return results.map(r => {
- const doc = documents.find(d => d.id === r.ref);
- return {
- title: doc.title,
- path: doc.path,
- score: r.score,
- preview: doc.content.slice(0, 200) + "…"
- };
- });
-}
-
-/* ------------------------------
- DOM elements
--------------------------------- */
+let activeIndex = -1;
const searchInput = document.getElementById("search-input");
-const searchBtn = document.getElementById("search-btn");
-
-/* ------------------------------
- Live dropdown results
--------------------------------- */
+const searchBtn = document.getElementById("search-btn");
const resultsBox = document.createElement("div");
+const searchForm = searchInput.closest("form");
+const modal = document.createElement("div");
+const modalResults = modal.querySelector("#search-modal-results");
+
resultsBox.id = "search-results";
document.body.appendChild(resultsBox);
-function showResults(results) {
- resultsBox.innerHTML = "";
-
- if (results.length === 0) {
- resultsBox.style.display = "none";
- return;
- }
-
- const rect = searchInput.getBoundingClientRect();
- resultsBox.style.top = `${rect.bottom + window.scrollY}px`;
- resultsBox.style.left = `${rect.left + window.scrollX}px`;
- resultsBox.style.width = `${rect.width}px`;
-
- results.slice(0, 5).forEach(result => {
- const item = document.createElement("div");
- item.className = "search-result";
- item.innerHTML = `
-
${result.title}
- ${result.preview}
- `;
- item.onclick = () => window.location.href = result.path;
- resultsBox.appendChild(item);
- });
-
- resultsBox.style.display = "block";
-}
-
-searchInput.addEventListener("input", () => {
- const q = searchInput.value.trim();
- if (!q) {
- resultsBox.style.display = "none";
- return;
- }
- showResults(search(q));
-});
-
-/* ------------------------------
- Modal popup
--------------------------------- */
-const modal = document.createElement("div");
modal.id = "search-modal";
modal.innerHTML = `
@@ -141,70 +19,311 @@ modal.innerHTML = `
`;
+
document.body.appendChild(modal);
-const modalResults = modal.querySelector("#search-modal-results");
+function extractDocuments(node, currentPath = "") {
+ if (node.type === "folder" && node.children) {
+ const nextPath = currentPath ? `${currentPath}/${node.name}` : node.name;
+
+ node.children.forEach((child) => {
+ extractDocuments(child, nextPath);
+ });
+ }
+
+ if (node.type === "file") {
+ documents.push({
+ id: documents.length.toString(),
+ title: node.name,
+ content: node.content || "",
+ path: node.url,
+ });
+ }
+}
+
+function buildIndex() {
+ lunrIndex = lunr(function () {
+ this.ref("id");
+ this.field("title", { boost: 10 });
+ this.field("url");
+ this.field("content");
+
+ documents.forEach((doc) => this.add(doc));
+ });
+}
+
+async function initSearch() {
+ const response = await fetch("/test.json");
+ const json = await response.json();
+
+ extractDocuments(json);
+ buildIndex();
+}
+
+initSearch();
+
+function buildExactQuery(query) {
+ return query
+ .toLowerCase()
+ .split(/\s+/)
+ .filter((term) => term.length > 1)
+ .map((term) => `${term}*`)
+ .join(" ");
+}
+
+function buildSnippet(content, terms, radius = 80) {
+ if (!content || terms.length === 0) return "";
+
+ const lower = content.toLowerCase();
+
+ let index = -1;
+ let matchedTerm = "";
+
+ for (const term of terms) {
+ const i = lower.indexOf(term);
+ if (i !== -1 && (index === -1 || i < index)) {
+ index = i;
+ matchedTerm = term;
+ }
+ }
+
+ if (index === -1) {
+ return content.slice(0, radius * 2) + "…";
+ }
+
+ const start = Math.max(0, index - radius);
+ const end = Math.min(content.length, index + matchedTerm.length + radius);
+
+ const prefix = start > 0 ? "…" : "";
+ const suffix = end < content.length ? "…" : "";
+
+ return prefix + content.slice(start, end) + suffix;
+}
+
+function getSearchTerms(query) {
+ return query
+ .toLowerCase()
+ .split(/\s+/)
+ .filter((t) => t.length > 2);
+}
+
+function highlight(text, terms) {
+ if (!text || terms.length === 0) return text;
+
+ const escaped = terms.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
+
+ const regex = new RegExp(`(${escaped.join("|")})`, "gi");
+
+ return text.replace(regex, "$1");
+}
+
+function search(query) {
+ if (!lunrIndex) return [];
+
+ const q = buildExactQuery(query);
+ if (!q) return [];
+
+ const terms = getSearchTerms(query);
+ const results = lunrIndex.search(q);
+
+ return results
+ .map((r) => {
+ const doc = documents.find((d) => d.id === r.ref);
+ if (!doc) return null;
+
+ const snippet = buildSnippet(doc.content, terms);
+
+ return {
+ title: highlight(doc.title, terms),
+ path: doc.path,
+ score: r.score,
+ preview: highlight(snippet, terms),
+ };
+ })
+ .filter(Boolean);
+}
+
+function showResults(results) {
+ resultsBox.innerHTML = "";
+ activeIndex = -1;
+
+ if (results.length === 0) {
+ resultsBox.style.display = "none";
+ return;
+ }
+
+ const rect = searchInput.getBoundingClientRect();
+ resultsBox.style.top = `${rect.bottom + window.scrollY}px`;
+ resultsBox.style.left = `${rect.left + window.scrollX}px`;
+ resultsBox.style.width = `${rect.width}px`;
+
+ results.forEach((result, i) => {
+ const item = document.createElement("div");
+ item.className = "search-result";
+ item.dataset.index = i;
+
+ item.innerHTML = `
+ ${result.title}
+ ${result.preview}
+ `;
+
+ item.onclick = () => {
+ window.location.href = result.path;
+ };
+
+ resultsBox.appendChild(item);
+ });
+
+ resultsBox.style.display = "block";
+}
+
+function updateActiveResult() {
+ const items = resultsBox.querySelectorAll(".search-result");
+
+ items.forEach((item) => item.classList.remove("active"));
+
+ if (activeIndex >= 0 && activeIndex < items.length) {
+ const activeItem = items[activeIndex];
+
+ activeItem.classList.add("active");
+
+ activeItem.scrollIntoView({
+ block: "nearest",
+ behavior: "smooth",
+ });
+ }
+}
function openSearchModal(results) {
- modalResults.innerHTML = "";
+ const modal = document.getElementById("search-modal");
+ const modalResults = modal?.querySelector("#search-modal-results");
+ modal.tabIndex = -1;
- if (results.length === 0) {
- modalResults.innerHTML = "No results found.
";
- }
+ if (!modal || !modalResults) {
+ console.warn("Search modal not available");
+ return;
+ }
- results.forEach(result => {
- const item = document.createElement("div");
- item.className = "search-modal-result";
- item.innerHTML = `
- ${result.title}
- ${result.preview}
- `;
- item.onclick = () => window.location.href = result.path;
- modalResults.appendChild(item);
- });
+ modalResults.innerHTML = "";
- modal.style.display = "block";
+ if (results.length === 0) {
+ modalResults.innerHTML = "No results found.
";
+ }
+
+ results.forEach((result) => {
+ const item = document.createElement("div");
+ item.className = "search-modal-result";
+ item.innerHTML = `
+ ${result.title}
+ ${result.preview}
+ `;
+ item.onclick = () => (window.location.href = result.path);
+ modalResults.appendChild(item);
+ });
+
+ modal.style.display = "block";
+ modal.focus();
}
-/* ------------------------------
- Submit search (Enter / 🔍)
--------------------------------- */
function submitSearch() {
- const q = searchInput.value.trim();
- if (!q) return;
- openSearchModal(search(q));
+ const q = searchInput.value.trim();
+ if (!q) return;
+ openSearchModal(search(q));
}
-searchInput.addEventListener("keydown", e => {
- if (e.key === "Enter") {
- e.preventDefault();
+/* EVENTS */
+searchInput.addEventListener("keydown", (e) => {
+ const items = resultsBox.querySelectorAll(".search-result");
+ if (!items.length) return;
+
+ switch (e.key) {
+ case "ArrowDown":
+ e.preventDefault();
+ if (activeIndex < items.length - 1) {
+ activeIndex++;
+ updateActiveResult();
+ }
+ break;
+
+ case "ArrowUp":
+ e.preventDefault();
+ if (activeIndex > 0) {
+ activeIndex--;
+ updateActiveResult();
+ }
+ break;
+
+ case "Enter":
+ e.preventDefault();
+ if (activeIndex >= 0 && activeIndex < items.length) {
+ items[activeIndex].click();
+ } else {
submitSearch();
- }
+ }
+ break;
+
+ case "Escape":
+ resultsBox.style.display = "none";
+ activeIndex = -1;
+ break;
+ }
});
+document.addEventListener("click", (e) => {
+ if (
+ !resultsBox.contains(e.target) &&
+ e.target !== searchInput &&
+ e.target !== searchBtn
+ ) {
+ resultsBox.style.display = "none";
+ }
+});
+
+modal.addEventListener("click", (e) => {
+ if (e.target.classList.contains("search-modal-backdrop")) {
+ modal.style.display = "none";
+ }
+});
+modal.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") {
+ modal.style.display = "none";
+ searchInput.focus();
+ }
+});
+
+window.addEventListener("pageshow", () => {
+ searchInput.value = "";
+ resultsBox.style.display = "none";
+});
+window.addEventListener("beforeunload", () => {
+ searchInput.value = "";
+});
+
+if (searchForm) {
+ searchForm.addEventListener("submit", (e) => {
+ e.preventDefault();
+ submitSearch();
+ });
+}
+
searchBtn.addEventListener("click", submitSearch);
-/* ------------------------------
- Close behaviours
--------------------------------- */
-document.addEventListener("click", e => {
- if (
- !resultsBox.contains(e.target) &&
- e.target !== searchInput &&
- e.target !== searchBtn
- ) {
- resultsBox.style.display = "none";
- }
+searchInput.addEventListener("input", () => {
+ const q = searchInput.value.trim();
+
+ if (q.length < 3) {
+ resultsBox.style.display = "none";
+ return;
+ }
+ showResults(search(q));
});
-modal.addEventListener("click", e => {
- if (e.target.classList.contains("search-modal-backdrop")) {
- modal.style.display = "none";
- }
-});
-
-document.addEventListener("keydown", e => {
- if (e.key === "Escape") {
- modal.style.display = "none";
- }
-});
+/* OLD FUNCTIONS */
+function buildFuzzyQuery(query) {
+ return query
+ .toLowerCase()
+ .split(/\s+/)
+ .filter((term) => term.length > 2)
+ .map((term) => `${term}~1`)
+ .join(" ");
+}
diff --git a/assets/styles/misc.css b/assets/styles/misc.css
index 89356fe..24af4cc 100644
--- a/assets/styles/misc.css
+++ b/assets/styles/misc.css
@@ -345,3 +345,21 @@ nav {
font-size: 0.85rem;
color: #666;
}
+mark {
+ background: rgba(255, 230, 150, 0.8);
+ color: inherit;
+ padding: 0 0.15em;
+ border-radius: 3px;
+}
+.search-result {
+ cursor: pointer;
+ padding: 0.5em 0.75em;
+}
+
+.search-result.active {
+ background: rgba(0, 0, 0, 0.08);
+}
+
+.search-result.active mark {
+ background: rgba(255, 230, 150, 0.9);
+}
diff --git a/blogs/blogs-list.org b/blogs/blogs-list.org
index 52b3206..31573ee 100644
--- a/blogs/blogs-list.org
+++ b/blogs/blogs-list.org
@@ -5,8 +5,8 @@ See the categories: @@html:Categories@@
* Blogs:
- [[file:2026/01-january/04-01-week-review.org][[04-01-2026] - Weekly Review]] @@html:04-01-2026 12:12@@ @@html: review @@
-- [[file:2026/2026-list.org][2026 List]] @@html:30-12-2025 22:33@@
-- [[file:2025/2025-list.org][2025 List]] @@html:30-12-2025 22:33@@
+- [[file:2026/2026-list.org][2026 List]] @@html:31-12-2025 17:49@@
+- [[file:2025/2025-list.org][2025 List]] @@html:31-12-2025 17:49@@
- [[file:2025/12-december/28-12-week-review.org][[28-12-2025] - Weekly Review]] @@html:28-12-2025 12:12@@ @@html: review @@
- [[file:2025/12-december/21-12-week-review.org][[21-12-2025] - Weekly Review]] @@html:21-12-2025 12:12@@ @@html: review @@
- [[file:2025/12-december/14-12-week-review.org][[14-12-2025] - Weekly Review]] @@html:09-12-2025 12:12@@ @@html: review @@
diff --git a/posts/posts-list.org b/posts/posts-list.org
index e4b924a..94c747e 100644
--- a/posts/posts-list.org
+++ b/posts/posts-list.org
@@ -4,7 +4,7 @@
See the categories: @@html:Categories@@
* Posts:
-- [[file:career/career-list.org][Career List]] @@html:30-12-2025 22:33@@
+- [[file:career/career-list.org][Career List]] @@html:31-12-2025 17:49@@
- [[file:career/probation-objectives.org][Probation Objectives:]] @@html:08-12-2025 17:55@@ @@html: review @@ @@html: notes @@
- [[file:career/airflow.org][Datamarts, Airflow and DAG's]] @@html:15-11-2025 18:37@@ @@html: learning @@ @@html: notes @@
- [[file:career/normalisation.org][Benefits of Normalisation]] @@html:10-11-2025 18:04@@ @@html: learning @@ @@html: notes @@
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..6f83e94
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+beautifulsoup4
+lxml
diff --git a/search-index-json.py b/search-index-json.py
index 84c5276..8359e1b 100644
--- a/search-index-json.py
+++ b/search-index-json.py
@@ -4,15 +4,11 @@ from bs4 import BeautifulSoup
def strip_html_tags(html):
soup = BeautifulSoup(html, "html.parser")
-
- # Remove script and style elements completely
for tag in soup(["script", "style"]):
tag.decompose()
- # Extract plain text
text = soup.get_text(separator="\n")
- # Clean up whitespace
lines = [line.strip() for line in text.splitlines() if line.strip()]
return "\n".join(lines)
diff --git a/sitemap.org b/sitemap.org
index a250e45..6449560 100644
--- a/sitemap.org
+++ b/sitemap.org
@@ -71,11 +71,11 @@
- tags
- [[file:tags/learning.org][Tag: learning]]
- [[file:tags/introduction.org][Tag: introduction]]
+ - [[file:tags/education.org][Tag: education]]
+ - [[file:tags/website.org][Tag: website]]
- [[file:tags/notes.org][Tag: notes]]
- [[file:tags/review.org][Tag: review]]
- [[file:tags/emacs.org][Tag: emacs]]
- - [[file:tags/education.org][Tag: education]]
- - [[file:tags/website.org][Tag: website]]
- [[file:tags/reading.org][Tag: reading]]
- - [[file:tags/insights.org][Tag: insights]]
- - [[file:tags/maths.org][Tag: maths]]
\ No newline at end of file
+ - [[file:tags/maths.org][Tag: maths]]
+ - [[file:tags/insights.org][Tag: insights]]
\ No newline at end of file