SPB Git

spb/spbgit Public MIT

SPB Git — the platform hosting itself

JavaScript 73.9% CSS 11.7% Nunjucks 11.6% Shell 2.7%
3.8 KB · 108 lines javascript
Raw Blame History
1/**2 * ─────────────────────────────────────────────3 *  SPB Git — Personal Git Platform4 * ─────────────────────────────────────────────5 *  Author  : Simon-Pierre Boucher6 *  Contact : contact@spboucher.ai7 *  File    : src/lib/search.mjs8 *  Purpose : Tiny inverted index over names, topics, descriptions, READMEs9 *  License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213import { allOverviews } from './overview.mjs';1415/** Strip markdown/code noise from a README for indexing + snippets. */16export function stripMarkdown(source) {17  return String(source)18    .replace(/```[\s\S]*?```/g, ' ')19    .replace(/`[^`]*`/g, ' ')20    .replace(/!\[[^\]]*\]\([^)]*\)/g, ' ')21    .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')22    .replace(/<[^>]+>/g, ' ')23    .replace(/[#>*_~|-]{1,}/g, ' ')24    .replace(/\s+/g, ' ')25    .trim();26}2728/**29 * Build the search index (cached globally, refreshed on push).30 * @param {{config, repos, meta, cache}} ctx31 * @returns {Promise<{entries: object[]}>}32 */33export async function buildSearchIndex(ctx) {34  const cachePath = ctx.cache.path('global', 'search-index.json');35  const cached = ctx.cache.getJSON(cachePath);36  if (cached) return cached;37  const overviews = await allOverviews(ctx);38  const entries = [];39  for (const o of overviews) {40    let readmeText = '';41    if (o.head) {42      const readme = await ctx.repos.readme(o.name, o.head);43      if (readme) readmeText = stripMarkdown(readme.content.toString('utf8')).slice(0, 20000);44    }45    entries.push({46      name: o.name,47      description: o.description,48      topics: o.topics,49      topLanguage: o.topLanguage,50      lastPush: o.lastPush,51      readmeText,52    });53  }54  const index = { entries, builtAt: new Date().toISOString() };55  ctx.cache.setJSON(cachePath, index);56  return index;57}5859/**60 * Query the index. Returns grouped, scored results.61 * @param {{config, repos, meta, cache}} ctx62 * @param {string} query63 * @returns {Promise<{repos: object[], readmes: object[]}>}64 */65export async function search(ctx, query) {66  const terms = String(query ?? '')67    .toLowerCase()68    .split(/\s+/)69    .filter((t) => t.length >= 2)70    .slice(0, 8);71  if (terms.length === 0) return { repos: [], readmes: [] };72  const { entries } = await buildSearchIndex(ctx);73  const repoHits = [];74  const readmeHits = [];75  for (const entry of entries) {76    const name = entry.name.toLowerCase();77    const description = (entry.description ?? '').toLowerCase();78    const topics = (entry.topics ?? []).map((t) => t.toLowerCase());79    const readme = (entry.readmeText ?? '').toLowerCase();80    let score = 0;81    let readmeMatch = false;82    for (const term of terms) {83      if (name === term) score += 100;84      else if (name.includes(term)) score += 40;85      if (description.includes(term)) score += 15;86      if (topics.some((t) => t.includes(term))) score += 25;87      if (readme.includes(term)) {88        score += 5;89        readmeMatch = true;90      }91    }92    if (score === 0) continue;93    const hit = { ...entry, score };94    if (name.includes(terms[0]) || description.includes(terms[0]) || topics.some((t) => t.includes(terms[0]))) {95      repoHits.push(hit);96    }97    if (readmeMatch) {98      const idx = readme.indexOf(terms[0]);99      const start = Math.max(0, idx - 80);100      const snippet = entry.readmeText.slice(start, start + 220).trim();101      readmeHits.push({ ...hit, snippet: (start > 0 ? '…' : '') + snippet + '…' });102    }103  }104  repoHits.sort((a, b) => b.score - a.score);105  readmeHits.sort((a, b) => b.score - a.score);106  return { repos: repoHits.slice(0, 20), readmes: readmeHits.slice(0, 20) };107}108