Fixed and finished the search function
This commit is contained in:
27
Makefile
27
Makefile
@@ -1,15 +1,28 @@
|
|||||||
.PHONY: all clean norm help
|
.PHONY: all clean norm help
|
||||||
|
|
||||||
# Default target: full build with search index
|
VENV := .venv
|
||||||
all:
|
PY := $(VENV)/bin/python
|
||||||
@echo "Building project (full rebuild with search index)..."
|
PIP := $(VENV)/bin/pip
|
||||||
emacs -Q --script build-site.el
|
|
||||||
python search-index-json.py
|
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:
|
clean:
|
||||||
@echo "Cleaning output directory..."
|
@echo "Cleaning output directory..."
|
||||||
rm -rf output/
|
rm -rf output/
|
||||||
|
rm -rf $(VENV)
|
||||||
|
|
||||||
norm:
|
norm:
|
||||||
@echo "sorting out the backups..."
|
@echo "sorting out the backups..."
|
||||||
@@ -19,6 +32,8 @@ norm:
|
|||||||
help:
|
help:
|
||||||
@echo "Available targets:"
|
@echo "Available targets:"
|
||||||
@echo " make - Full rebuild"
|
@echo " make - Full rebuild"
|
||||||
|
@echo " make build - Rebuild the output directory"
|
||||||
@echo " make clean - Remove output directory"
|
@echo " make clean - Remove output directory"
|
||||||
@echo " make norm - Move all files that end with ~ to the backup folder"
|
@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"
|
@echo " make help - Show this help message"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const STATES = [
|
|||||||
{ key: "manager_review", label: "Manager Review" },
|
{ key: "manager_review", label: "Manager Review" },
|
||||||
{ key: "in_progress", label: "In Progress" },
|
{ key: "in_progress", label: "In Progress" },
|
||||||
{ key: "not_started", label: "Not Started" },
|
{ key: "not_started", label: "Not Started" },
|
||||||
{ key: "comments", label: "Comments" }
|
{ key: "comments", label: "Comments" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function isMobile() {
|
function isMobile() {
|
||||||
@@ -22,7 +22,7 @@ function populateMobileControls(items) {
|
|||||||
fromSelect.innerHTML = "<option value=''>From</option>";
|
fromSelect.innerHTML = "<option value=''>From</option>";
|
||||||
toSelect.innerHTML = "<option value=''>To</option>";
|
toSelect.innerHTML = "<option value=''>To</option>";
|
||||||
|
|
||||||
items.forEach(item => {
|
items.forEach((item) => {
|
||||||
const opt = document.createElement("option");
|
const opt = document.createElement("option");
|
||||||
opt.value = item.id;
|
opt.value = item.id;
|
||||||
opt.textContent = item.title;
|
opt.textContent = item.title;
|
||||||
@@ -30,7 +30,7 @@ function populateMobileControls(items) {
|
|||||||
itemSelect.appendChild(opt);
|
itemSelect.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
|
||||||
STATES.forEach(s => {
|
STATES.forEach((s) => {
|
||||||
fromSelect.appendChild(new Option(s.label, s.key));
|
fromSelect.appendChild(new Option(s.label, s.key));
|
||||||
toSelect.appendChild(new Option(s.label, s.key));
|
toSelect.appendChild(new Option(s.label, s.key));
|
||||||
});
|
});
|
||||||
@@ -62,7 +62,7 @@ document.getElementById("move-confirm").addEventListener("click", async () => {
|
|||||||
const res = await fetch(`/api/competencies/items/${itemId}/state`, {
|
const res = await fetch(`/api/competencies/items/${itemId}/state`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ state: to })
|
body: JSON.stringify({ state: to }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -72,8 +72,6 @@ document.getElementById("move-confirm").addEventListener("click", async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let draggedItemId = null;
|
let draggedItemId = null;
|
||||||
|
|
||||||
function countByState(items) {
|
function countByState(items) {
|
||||||
@@ -117,7 +115,7 @@ function createColumn(state, items, counts) {
|
|||||||
const list = document.createElement("div");
|
const list = document.createElement("div");
|
||||||
list.className = "kanban-list";
|
list.className = "kanban-list";
|
||||||
|
|
||||||
list.addEventListener("dragover", e => e.preventDefault());
|
list.addEventListener("dragover", (e) => e.preventDefault());
|
||||||
|
|
||||||
list.addEventListener("drop", async () => {
|
list.addEventListener("drop", async () => {
|
||||||
if (!draggedItemId) return;
|
if (!draggedItemId) return;
|
||||||
@@ -125,7 +123,7 @@ function createColumn(state, items, counts) {
|
|||||||
const res = await fetch(`/api/competencies/items/${draggedItemId}/state`, {
|
const res = await fetch(`/api/competencies/items/${draggedItemId}/state`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ state: state.key })
|
body: JSON.stringify({ state: state.key }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -136,8 +134,8 @@ function createColumn(state, items, counts) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
items
|
items
|
||||||
.filter(i => i.state === state.key)
|
.filter((i) => i.state === state.key)
|
||||||
.forEach(i => list.appendChild(createCard(i)));
|
.forEach((i) => list.appendChild(createCard(i)));
|
||||||
|
|
||||||
col.appendChild(header);
|
col.appendChild(header);
|
||||||
col.appendChild(list);
|
col.appendChild(list);
|
||||||
@@ -152,12 +150,11 @@ function renderBoard(items) {
|
|||||||
|
|
||||||
const counts = countByState(items);
|
const counts = countByState(items);
|
||||||
|
|
||||||
STATES.forEach(state => {
|
STATES.forEach((state) => {
|
||||||
board.appendChild(createColumn(state, items, counts));
|
board.appendChild(createColumn(state, items, counts));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function loadBoard() {
|
async function loadBoard() {
|
||||||
const res = await fetch("/api/competencies/items");
|
const res = await fetch("/api/competencies/items");
|
||||||
const items = await res.json();
|
const items = await res.json();
|
||||||
@@ -165,5 +162,4 @@ async function loadBoard() {
|
|||||||
populateMobileControls(items);
|
populateMobileControls(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
loadBoard();
|
loadBoard();
|
||||||
|
|||||||
@@ -1,51 +1,59 @@
|
|||||||
let lunrIndex;
|
let lunrIndex;
|
||||||
let documents = [];
|
let documents = [];
|
||||||
|
let activeIndex = -1;
|
||||||
|
const searchInput = document.getElementById("search-input");
|
||||||
|
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);
|
||||||
|
|
||||||
|
modal.id = "search-modal";
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="search-modal-backdrop"></div>
|
||||||
|
<div class="search-modal-content">
|
||||||
|
<h2>Search results</h2>
|
||||||
|
<div id="search-modal-results"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
/* ------------------------------
|
|
||||||
JSON → documents
|
|
||||||
-------------------------------- */
|
|
||||||
function extractDocuments(node, currentPath = "") {
|
function extractDocuments(node, currentPath = "") {
|
||||||
if (node.type === "folder" && node.children) {
|
if (node.type === "folder" && node.children) {
|
||||||
const nextPath = currentPath
|
const nextPath = currentPath ? `${currentPath}/${node.name}` : node.name;
|
||||||
? `${currentPath}/${node.name}`
|
|
||||||
: node.name;
|
|
||||||
|
|
||||||
node.children.forEach(child => {
|
node.children.forEach((child) => {
|
||||||
extractDocuments(child, nextPath);
|
extractDocuments(child, nextPath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.type === "file") {
|
if (node.type === "file") {
|
||||||
const filePath = currentPath
|
|
||||||
? `${currentPath}/${node.name}`
|
|
||||||
: node.name;
|
|
||||||
|
|
||||||
documents.push({
|
documents.push({
|
||||||
id: documents.length.toString(),
|
id: documents.length.toString(),
|
||||||
title: node.name,
|
title: node.name,
|
||||||
content: node.content || "",
|
content: node.content || "",
|
||||||
path: "/" + filePath.replace(/^output\//, "")
|
path: node.url,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------
|
|
||||||
Lunr index
|
|
||||||
-------------------------------- */
|
|
||||||
function buildIndex() {
|
function buildIndex() {
|
||||||
lunrIndex = lunr(function () {
|
lunrIndex = lunr(function () {
|
||||||
this.ref("id");
|
this.ref("id");
|
||||||
this.field("title", { boost: 10 });
|
this.field("title", { boost: 10 });
|
||||||
|
this.field("url");
|
||||||
this.field("content");
|
this.field("content");
|
||||||
|
|
||||||
documents.forEach(doc => this.add(doc));
|
documents.forEach((doc) => this.add(doc));
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Lunr index built with", documents.length, "documents");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initSearch() {
|
async function initSearch() {
|
||||||
const response = await fetch("test.json");
|
const response = await fetch("/test.json");
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
|
|
||||||
extractDocuments(json);
|
extractDocuments(json);
|
||||||
@@ -54,47 +62,90 @@ async function initSearch() {
|
|||||||
|
|
||||||
initSearch();
|
initSearch();
|
||||||
|
|
||||||
/* ------------------------------
|
function buildExactQuery(query) {
|
||||||
Fuzzy search
|
|
||||||
-------------------------------- */
|
|
||||||
function buildFuzzyQuery(query) {
|
|
||||||
return query
|
return query
|
||||||
|
.toLowerCase()
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.map(term => `${term}~1`)
|
.filter((term) => term.length > 1)
|
||||||
|
.map((term) => `${term}*`)
|
||||||
.join(" ");
|
.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, "<mark>$1</mark>");
|
||||||
|
}
|
||||||
|
|
||||||
function search(query) {
|
function search(query) {
|
||||||
if (!lunrIndex) return [];
|
if (!lunrIndex) return [];
|
||||||
|
|
||||||
const results = lunrIndex.search(buildFuzzyQuery(query));
|
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 results.map(r => {
|
|
||||||
const doc = documents.find(d => d.id === r.ref);
|
|
||||||
return {
|
return {
|
||||||
title: doc.title,
|
title: highlight(doc.title, terms),
|
||||||
path: doc.path,
|
path: doc.path,
|
||||||
score: r.score,
|
score: r.score,
|
||||||
preview: doc.content.slice(0, 200) + "…"
|
preview: highlight(snippet, terms),
|
||||||
};
|
};
|
||||||
});
|
})
|
||||||
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------
|
|
||||||
DOM elements
|
|
||||||
-------------------------------- */
|
|
||||||
const searchInput = document.getElementById("search-input");
|
|
||||||
const searchBtn = document.getElementById("search-btn");
|
|
||||||
|
|
||||||
/* ------------------------------
|
|
||||||
Live dropdown results
|
|
||||||
-------------------------------- */
|
|
||||||
const resultsBox = document.createElement("div");
|
|
||||||
resultsBox.id = "search-results";
|
|
||||||
document.body.appendChild(resultsBox);
|
|
||||||
|
|
||||||
function showResults(results) {
|
function showResults(results) {
|
||||||
resultsBox.innerHTML = "";
|
resultsBox.innerHTML = "";
|
||||||
|
activeIndex = -1;
|
||||||
|
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
resultsBox.style.display = "none";
|
resultsBox.style.display = "none";
|
||||||
@@ -106,88 +157,119 @@ function showResults(results) {
|
|||||||
resultsBox.style.left = `${rect.left + window.scrollX}px`;
|
resultsBox.style.left = `${rect.left + window.scrollX}px`;
|
||||||
resultsBox.style.width = `${rect.width}px`;
|
resultsBox.style.width = `${rect.width}px`;
|
||||||
|
|
||||||
results.slice(0, 5).forEach(result => {
|
results.forEach((result, i) => {
|
||||||
const item = document.createElement("div");
|
const item = document.createElement("div");
|
||||||
item.className = "search-result";
|
item.className = "search-result";
|
||||||
|
item.dataset.index = i;
|
||||||
|
|
||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
<div class="search-result-title">${result.title}</div>
|
<div class="search-result-title">${result.title}</div>
|
||||||
<div class="search-result-preview">${result.preview}</div>
|
<div class="search-result-preview">${result.preview}</div>
|
||||||
`;
|
`;
|
||||||
item.onclick = () => window.location.href = result.path;
|
|
||||||
|
item.onclick = () => {
|
||||||
|
window.location.href = result.path;
|
||||||
|
};
|
||||||
|
|
||||||
resultsBox.appendChild(item);
|
resultsBox.appendChild(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
resultsBox.style.display = "block";
|
resultsBox.style.display = "block";
|
||||||
}
|
}
|
||||||
|
|
||||||
searchInput.addEventListener("input", () => {
|
function updateActiveResult() {
|
||||||
const q = searchInput.value.trim();
|
const items = resultsBox.querySelectorAll(".search-result");
|
||||||
if (!q) {
|
|
||||||
resultsBox.style.display = "none";
|
items.forEach((item) => item.classList.remove("active"));
|
||||||
return;
|
|
||||||
}
|
if (activeIndex >= 0 && activeIndex < items.length) {
|
||||||
showResults(search(q));
|
const activeItem = items[activeIndex];
|
||||||
|
|
||||||
|
activeItem.classList.add("active");
|
||||||
|
|
||||||
|
activeItem.scrollIntoView({
|
||||||
|
block: "nearest",
|
||||||
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
|
}
|
||||||
/* ------------------------------
|
}
|
||||||
Modal popup
|
|
||||||
-------------------------------- */
|
|
||||||
const modal = document.createElement("div");
|
|
||||||
modal.id = "search-modal";
|
|
||||||
modal.innerHTML = `
|
|
||||||
<div class="search-modal-backdrop"></div>
|
|
||||||
<div class="search-modal-content">
|
|
||||||
<h2>Search results</h2>
|
|
||||||
<div id="search-modal-results"></div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
document.body.appendChild(modal);
|
|
||||||
|
|
||||||
const modalResults = modal.querySelector("#search-modal-results");
|
|
||||||
|
|
||||||
function openSearchModal(results) {
|
function openSearchModal(results) {
|
||||||
|
const modal = document.getElementById("search-modal");
|
||||||
|
const modalResults = modal?.querySelector("#search-modal-results");
|
||||||
|
modal.tabIndex = -1;
|
||||||
|
|
||||||
|
if (!modal || !modalResults) {
|
||||||
|
console.warn("Search modal not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
modalResults.innerHTML = "";
|
modalResults.innerHTML = "";
|
||||||
|
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
modalResults.innerHTML = "<p>No results found.</p>";
|
modalResults.innerHTML = "<p>No results found.</p>";
|
||||||
}
|
}
|
||||||
|
|
||||||
results.forEach(result => {
|
results.forEach((result) => {
|
||||||
const item = document.createElement("div");
|
const item = document.createElement("div");
|
||||||
item.className = "search-modal-result";
|
item.className = "search-modal-result";
|
||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
<div class="search-modal-title">${result.title}</div>
|
<div class="search-modal-title">${result.title}</div>
|
||||||
<div class="search-modal-preview">${result.preview}</div>
|
<div class="search-modal-preview">${result.preview}</div>
|
||||||
`;
|
`;
|
||||||
item.onclick = () => window.location.href = result.path;
|
item.onclick = () => (window.location.href = result.path);
|
||||||
modalResults.appendChild(item);
|
modalResults.appendChild(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
modal.style.display = "block";
|
modal.style.display = "block";
|
||||||
|
modal.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------
|
|
||||||
Submit search (Enter / 🔍)
|
|
||||||
-------------------------------- */
|
|
||||||
function submitSearch() {
|
function submitSearch() {
|
||||||
const q = searchInput.value.trim();
|
const q = searchInput.value.trim();
|
||||||
if (!q) return;
|
if (!q) return;
|
||||||
openSearchModal(search(q));
|
openSearchModal(search(q));
|
||||||
}
|
}
|
||||||
|
|
||||||
searchInput.addEventListener("keydown", e => {
|
/* EVENTS */
|
||||||
if (e.key === "Enter") {
|
searchInput.addEventListener("keydown", (e) => {
|
||||||
|
const items = resultsBox.querySelectorAll(".search-result");
|
||||||
|
if (!items.length) return;
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case "ArrowDown":
|
||||||
e.preventDefault();
|
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();
|
submitSearch();
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "Escape":
|
||||||
|
resultsBox.style.display = "none";
|
||||||
|
activeIndex = -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
searchBtn.addEventListener("click", submitSearch);
|
document.addEventListener("click", (e) => {
|
||||||
|
|
||||||
/* ------------------------------
|
|
||||||
Close behaviours
|
|
||||||
-------------------------------- */
|
|
||||||
document.addEventListener("click", e => {
|
|
||||||
if (
|
if (
|
||||||
!resultsBox.contains(e.target) &&
|
!resultsBox.contains(e.target) &&
|
||||||
e.target !== searchInput &&
|
e.target !== searchInput &&
|
||||||
@@ -197,14 +279,51 @@ document.addEventListener("click", e => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
modal.addEventListener("click", e => {
|
modal.addEventListener("click", (e) => {
|
||||||
if (e.target.classList.contains("search-modal-backdrop")) {
|
if (e.target.classList.contains("search-modal-backdrop")) {
|
||||||
modal.style.display = "none";
|
modal.style.display = "none";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
modal.addEventListener("keydown", (e) => {
|
||||||
document.addEventListener("keydown", e => {
|
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
modal.style.display = "none";
|
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);
|
||||||
|
|
||||||
|
searchInput.addEventListener("input", () => {
|
||||||
|
const q = searchInput.value.trim();
|
||||||
|
|
||||||
|
if (q.length < 3) {
|
||||||
|
resultsBox.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showResults(search(q));
|
||||||
|
});
|
||||||
|
|
||||||
|
/* OLD FUNCTIONS */
|
||||||
|
function buildFuzzyQuery(query) {
|
||||||
|
return query
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter((term) => term.length > 2)
|
||||||
|
.map((term) => `${term}~1`)
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|||||||
@@ -345,3 +345,21 @@ nav {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: #666;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ See the categories: @@html:<a href="../categories.html">Categories</a>@@
|
|||||||
|
|
||||||
* Blogs:
|
* Blogs:
|
||||||
- [[file:2026/01-january/04-01-week-review.org][[04-01-2026] - Weekly Review]] @@html:<span class="post-date">04-01-2026 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
- [[file:2026/01-january/04-01-week-review.org][[04-01-2026] - Weekly Review]] @@html:<span class="post-date">04-01-2026 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||||
- [[file:2026/2026-list.org][2026 List]] @@html:<span class="post-date">30-12-2025 22:33</span>@@
|
- [[file:2026/2026-list.org][2026 List]] @@html:<span class="post-date">31-12-2025 17:49</span>@@
|
||||||
- [[file:2025/2025-list.org][2025 List]] @@html:<span class="post-date">30-12-2025 22:33</span>@@
|
- [[file:2025/2025-list.org][2025 List]] @@html:<span class="post-date">31-12-2025 17:49</span>@@
|
||||||
- [[file:2025/12-december/28-12-week-review.org][[28-12-2025] - Weekly Review]] @@html:<span class="post-date">28-12-2025 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
- [[file:2025/12-december/28-12-week-review.org][[28-12-2025] - Weekly Review]] @@html:<span class="post-date">28-12-2025 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||||
- [[file:2025/12-december/21-12-week-review.org][[21-12-2025] - Weekly Review]] @@html:<span class="post-date">21-12-2025 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
- [[file:2025/12-december/21-12-week-review.org][[21-12-2025] - Weekly Review]] @@html:<span class="post-date">21-12-2025 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||||
- [[file:2025/12-december/14-12-week-review.org][[14-12-2025] - Weekly Review]] @@html:<span class="post-date">09-12-2025 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
- [[file:2025/12-december/14-12-week-review.org][[14-12-2025] - Weekly Review]] @@html:<span class="post-date">09-12-2025 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
See the categories: @@html:<a href="../categories.html">Categories</a>@@
|
See the categories: @@html:<a href="../categories.html">Categories</a>@@
|
||||||
|
|
||||||
* Posts:
|
* Posts:
|
||||||
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">30-12-2025 22:33</span>@@
|
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">31-12-2025 17:49</span>@@
|
||||||
- [[file:career/probation-objectives.org][Probation Objectives:]] @@html:<span class="post-date">08-12-2025 17:55</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@ @@html:<a href="/tags/notes.html"> <span class="post-tag">notes</span> </a>@@
|
- [[file:career/probation-objectives.org][Probation Objectives:]] @@html:<span class="post-date">08-12-2025 17:55</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@ @@html:<a href="/tags/notes.html"> <span class="post-tag">notes</span> </a>@@
|
||||||
- [[file:career/airflow.org][Datamarts, Airflow and DAG's]] @@html:<span class="post-date">15-11-2025 18:37</span>@@ @@html:<a href="/tags/learning.html"> <span class="post-tag">learning</span> </a>@@ @@html:<a href="/tags/notes.html"> <span class="post-tag">notes</span> </a>@@
|
- [[file:career/airflow.org][Datamarts, Airflow and DAG's]] @@html:<span class="post-date">15-11-2025 18:37</span>@@ @@html:<a href="/tags/learning.html"> <span class="post-tag">learning</span> </a>@@ @@html:<a href="/tags/notes.html"> <span class="post-tag">notes</span> </a>@@
|
||||||
- [[file:career/normalisation.org][Benefits of Normalisation]] @@html:<span class="post-date">10-11-2025 18:04</span>@@ @@html:<a href="/tags/learning.html"> <span class="post-tag">learning</span> </a>@@ @@html:<a href="/tags/notes.html"> <span class="post-tag">notes</span> </a>@@
|
- [[file:career/normalisation.org][Benefits of Normalisation]] @@html:<span class="post-date">10-11-2025 18:04</span>@@ @@html:<a href="/tags/learning.html"> <span class="post-tag">learning</span> </a>@@ @@html:<a href="/tags/notes.html"> <span class="post-tag">notes</span> </a>@@
|
||||||
|
|||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
beautifulsoup4
|
||||||
|
lxml
|
||||||
@@ -4,15 +4,11 @@ from bs4 import BeautifulSoup
|
|||||||
|
|
||||||
def strip_html_tags(html):
|
def strip_html_tags(html):
|
||||||
soup = BeautifulSoup(html, "html.parser")
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
|
||||||
# Remove script and style elements completely
|
|
||||||
for tag in soup(["script", "style"]):
|
for tag in soup(["script", "style"]):
|
||||||
tag.decompose()
|
tag.decompose()
|
||||||
|
|
||||||
# Extract plain text
|
|
||||||
text = soup.get_text(separator="\n")
|
text = soup.get_text(separator="\n")
|
||||||
|
|
||||||
# Clean up whitespace
|
|
||||||
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|||||||
@@ -71,11 +71,11 @@
|
|||||||
- tags
|
- tags
|
||||||
- [[file:tags/learning.org][Tag: learning]]
|
- [[file:tags/learning.org][Tag: learning]]
|
||||||
- [[file:tags/introduction.org][Tag: introduction]]
|
- [[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/notes.org][Tag: notes]]
|
||||||
- [[file:tags/review.org][Tag: review]]
|
- [[file:tags/review.org][Tag: review]]
|
||||||
- [[file:tags/emacs.org][Tag: emacs]]
|
- [[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/reading.org][Tag: reading]]
|
||||||
- [[file:tags/insights.org][Tag: insights]]
|
|
||||||
- [[file:tags/maths.org][Tag: maths]]
|
- [[file:tags/maths.org][Tag: maths]]
|
||||||
|
- [[file:tags/insights.org][Tag: insights]]
|
||||||
Reference in New Issue
Block a user