<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta
      name="viewport"
      content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=6.0"
    />
    <title>Resources (@watheia/iron-ui.theme.sty*) | FOSS Git Repositories &amp; NPM Packages</title>
    <meta name="description" content="Getting Started with ts-liveview - a server-side rendering realtime webapp framework with progressive enhancement" />
  </head>
  <body>
    <div id="noscript" aria-hidden="true">
      <style>
        #noscript {
          margin: 1em;
          padding: 1em;
          outline: 1px solid red;
        }
      </style>
      Javascript is not enabled. This site can still works but it'll be more
      interactive when javascript is enabled.
    </div>
    <style>
      body {
        padding-bottom: 2.5rem;
      }
      #noscript {
        display: none;
      }
      #ws_status {
        position: fixed;
        bottom: 1em;
        left: 1em;
        padding: 0.25em;
        background: white;
        border: 1px solid black;
        border-radius: 0.2em;
        z-index: 1;
      }
      #ws_status:hover {
        opacity: 0.2;
        user-select: none;
      }
      abbr[title]:after {
        content: ' (' attr(title) ')';
      }
      @media screen and (min-width: 1025px) {
        abbr[title]:after {
          content: '';
        }
      }
      .dark-theme {
        filter: invert();
        background-color: black;
      }
    </style>
    <div id="ws_status" hidden role="none">loading...</div>
    <script>
      document.getElementById('noscript').remove()
      document.getElementById('ws_status').removeAttribute('hidden')
      let template_name = 'web'
    </script>
    <div class="app"><style>pre[class*=language-],code[class*=language-]{white-space:pre-wrap!important;display:inline-block!important;max-width:90vw}details.source-code{margin-top:.5rem}details.source-code details summary{margin-top:.5rem;margin-inline-start:1rem}.source-code summary{cursor:pointer}.error{border:1px solid red;padding:.75rem;width:fit-content}.update-message{color:green;font-size:smaller}.text-no-wrap{display:inline-block;width:max-content}.text-center{text-align:center}.d-flex{display:flex}.flex-wrap{display:flex;flex-wrap:wrap}.flex-grow{flex-grow:1}.flex-center{display:flex;justify-content:center;align-items:center}.flex-column{display:flex;flex-direction:column}.inline-block{display:inline-block}[hidden]{display:none!important}img{max-width:100%;max-height:100%}.w-100{width:100%}code.inline-code{background:#afb8c133;padding:.25rem .5rem;border-radius:.25rem;font-family:monospace}.no-decoration{text-decoration:none}.ws_status--safe-area{margin-top:3rem}.hint-block{border-inline-start:3px solid #748;background-color:#edf;padding:1rem;margin:.5rem 0;width:fit-content}.common-table{border-collapse:collapse}.common-table th{background-color:#f0f0f0}.common-table th,.common-table td{border:1px solid #ccc;padding:.25rem .5rem}@media(max-width:525px){.hide-on-phone{display:none}}@media(min-width:526px){.hide-on-desktop{display:none}}.inline-edit-field[data-mode=view] .edit-mode,.inline-edit-field[data-mode=edit] .view-mode{display:none}.inline-edit-field input{margin-inline:.5rem;padding:.25rem}.inline-edit-field button{margin-inline-start:.5rem}form .input-container{margin-top:.25rem}.separator-line:before,.separator-line:after{content:"";flex:1;border-bottom:1px solid #888;margin:1rem .75rem}body{font-family:sans-serif}h1{font-size:2rem}h2{font-size:1.8rem}h3{font-size:1.75rem}h4{font-size:1.5rem}h5{font-size:1.25rem}h6{font-size:1.2rem}h1,h2,h3,h4,h5,h6{margin:1em 0 .5em}.app{max-width:1089px;margin:auto}
</style><nav class="navbar"><style>.navbar{display:flex;justify-content:space-between;align-items:center}.navbar .navbar-menu{display:flex;flex-wrap:wrap;align-items:center}.navbar .navbar-menu-toggle{display:flex;justify-content:center;align-items:center}.navbar .navbar-menu-toggle .icon{width:2rem;height:2rem}.navbar [name=navbar-menu-toggle],.navbar .navbar-menu-toggle{display:none}.navbar .navbar-menu-item{border-bottom:1px solid black;text-decoration:none;margin:.5rem;padding-bottom:.25rem;display:inline-block}.navbar .navbar-menu-item.selected{border-bottom:2px solid black;margin-bottom:calc(.5rem - 1px)}@media(max-width:768px){.navbar .navbar-menu-toggle{display:initial}.navbar .navbar-menu{display:none;position:fixed;background-color:#eee;inset:0;margin-top:3.5rem;overflow:auto;overscroll-behavior:contain;padding-bottom:2.5rem}.navbar [name=navbar-menu-toggle]:checked~.navbar-menu{display:initial;z-index:2}.navbar .navbar-menu-item{border-bottom:none;display:block;margin-inline-start:1rem;margin-block-start:1rem;font-size:1.25rem;width:fit-content}}
</style><div class="navbar-brand"><div style="color: darkblue; font-weight: bold"><a onclick="emitHref(event)" style="font-size: 1.7rem; text-decoration: none" class="text-no-wrap" href="/">Resources Index</a> <div class="text-no-wrap"><a href="https://github.com/beenotung/res-index" target="_blank">git</a></div></div></div><label class="navbar-menu-toggle" for="navbar-menu-toggle" aria-label="toggle navigation menu"><svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 512 512" width="512" height="512" aria-hidden="true"><span style="font-size: 2rem">☰</span><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="48" d="M88 152h336M88 256h336M88 360h336"></path></svg></label><input name="navbar-menu-toggle" type="checkbox" id="navbar-menu-toggle"><div class="navbar-menu"><a class="navbar-menu-item" href="/" onclick="emitHref(event)">Search</a><a class="navbar-menu-item" href="/about" onclick="emitHref(event)">About</a><a class="navbar-menu-item" href="/dataset" onclick="emitHref(event)">Dataset</a><a class="navbar-menu-item" href="/user-agents" onclick="emitHref(event)">Visitor Stats</a><a class="navbar-menu-item" href="/privacy" onclick="emitHref(event)">Privacy Policy</a><div style="text-align: end; margin-inline: 1rem; flex-grow: 1">🌏 Language: <a onclick="switchLang(event, &#39;en&#39;)" href="/set-lang/en?return_url=%2F%3Fprefix=%2540watheia%252Firon-ui.theme.sty"><button>English</button></a> | <a onclick="switchLang(event, &#39;zh-hk&#39;)" href="/set-lang/zh-hk?return_url=%2F%3Fprefix=%2540watheia%252Firon-ui.theme.sty"><button>繁體中文</button></a> | <a onclick="switchLang(event, &#39;zh-cn&#39;)" href="/set-lang/zh-cn?return_url=%2F%3Fprefix=%2540watheia%252Firon-ui.theme.sty"><button>简体中文</button></a>
<script>
function switchLang(event, lang){
  document.cookie = 'lang=' + lang + ';SameSite=Lax;path=/;max-age=631139040'
  remount()
  event.preventDefault()
}
</script>
</div></div></nav><hr><script>console.original_debug=console.debug,console.debug=()=>{};
</script><script>_navigation_type_="express",_navigation_method_="GET";
</script><script src="/js/bundle.min.js" type="module" defer></script><div id="home"><style>#searchForm label{display:block;width:100%;margin:.25rem}.hint{border-inline-start:3px solid #748;background-color:#edf;padding:1rem;margin:.5rem 0;width:fit-content}.hint code{background-color:#fef;outline:1px solid #aaa;border-radius:.25rem;padding:.1rem;display:inline-block}.hide-hints .hint,.hide-hints #hideHintsBtn,#showHintsBtn{display:none}.hide-hints #showHintsBtn{display:block}.list{padding:.25rem}.res-group,.res{padding:.25rem .25rem .5rem}.res-desc{margin-top:.25rem}
</style><h1>FOSS Git Repository &amp; NPM Package Index</h1><form id="searchForm" data-trim-empty onsubmit="emitForm(event); loadingMessage.textContent=&#39;searching...&#39;"><input name="form_action" value="search" hidden><label>Repo Host: <input name="host" placeholder="e.g. npmjs"></label><label>Username: <input name="username" placeholder="e.g. beeno"></label><label>Repo/Package name: <input name="name" placeholder="e.g. react event"></label><label style="width: fit-content">Programming Languages: <input name="language" placeholder="e.g. typescript javascript"><br><details><summary>Counts by language</summary><select style="width: 100%;" multiple oninput="searchForm.language.value=Array.from(this.selectedOptions,option=&gt;option.value).join(&#39; &#39;)" id="languageSelect"><option value="Javascript">Javascript (2)</option></select></details></label><label>Description: <input name="desc" placeholder="e.g. 倉頡"></label><input type="submit" value="Search"><div style="margin-top: 0.5rem"><button type="button" id="hideHintsBtn" onclick="hideHints()" title="Hide hints for 24 hours">Hide Hints</button><button type="button" id="showHintsBtn" onclick="showHints()">Show Hints</button></div><p class="hint">Hint: you can search by multiple keywords, separated by space, e.g. <code>react event</code> as searching for repos containing <code>react</code> and <code>event</code> in the name (appearing any order).</p><p class="hint">Hint: you can indicate negative keywords with hyphen prefix, e.g. <code>-react -ng- chart</code> as searching for <code>chart</code> libraries while excluding those framework-specific libraries having <code>react</code> or <code>ng-</code> in the name.</p><p class="hint">Hint: multiple keywords are combined with "and" for most fields, but they're combined with "or" for programming languages.</p><p class="hint">Hint: the keyboards are matched partially for most fields, but is matched exactly for programming languages. So searching <code>Java</code> will not match <code>Javascript</code> repos.</p><p class="hint">Hint: if a keyword is wrapped with double quotes, it is matched in full. For example searching <code>speed</code> will matched for <code>frank-dspeed</code> but searching <code>"speed"</code> will not match for that user. This feature does not apply to the language field as it's always matched in full.</p><div id="result"><p id="loadingMessage"></p><p>repo/package prefix: @watheia/iron-ui.theme.sty*</p><p>52 matches</p><div class="list"><div class="res-group"><a onclick="emitHref(event)" href="/?prefix=%40watheia%2Firon-ui.theme.styl">pattern: @watheia/iron-ui.theme.styl*</a> (52 matches)</div></div></div></form><script>function autoFocusKeyword(){if(searchForm?.keyword){searchForm.keyword.focus();return}setTimeout(autoFocusKeyword,33)}autoFocusKeyword();function hideHints(){let e=+localStorage.getItem("hide_interval")||0;e?e*=1.5:e=1,localStorage.setItem("hide_interval",e);let t=Date.now()+e*864e5;localStorage.setItem("hide_hint_until",t),searchForm.classList.add("hide-hints")}function showHints(){localStorage.removeItem("hide_hint_until"),localStorage.removeItem("hide_interval"),searchForm.classList.remove("hide-hints")}function autoHideHints(){console.log("autoHideHints");let e=+localStorage.getItem("hide_hint_until");Date.now()<e&&searchForm.classList.add("hide-hints")}autoHideHints();
</script><details class="source-code"><summary><b>Source Code of <code>home.tsx</code></b></summary><link rel="stylesheet" href="/lib/prism/prism.css"><script src="/lib/prism/prism.js"></script><details><summary>(import statements omitted for simplicity, click to expand)</summary><pre><code class="language-tsx">import { o } from '../jsx/jsx.js'
import SourceCode from '../components/source-code.js'
import { mapArray } from '../components/fragment.js'
import { DynamicContext } from '../context.js'
import Style from '../components/style.js'
import { db } from '../../../db/db.js'
import { Script } from '../components/script.js'
import { EarlyTerminate } from '../../exception.js'
import { ProgrammingLanguageSpan } from '../components/programming-language.js'
import { Link } from '../components/router.js'
import { nodeToVNode } from '../jsx/vnode.js'
import { Element } from '../jsx/types.js'
import { DAY } from '@beenotung/tslib/time.js'
import { Routes } from '../routes.js'
import { compare } from '@beenotung/tslib/compare.js'
import { env } from '../../env.js'
import { readJsonFileSync, writeJsonFileSync } from '@beenotung/tslib/fs.js'</code></pre></details><pre><code class="language-tsx">// Calling &lt;Component/&gt; will transform the JSX into AST for each rendering.
// You can reuse a pre-compute AST like `let component = &lt;Component/&gt;`.

// If the expression is static (not depending on the render Context),
// you don't have to wrap it by a function at all.

let style = Style(/* css */ `
#searchForm label {
  display: block;
  width: 100%;
  margin: 0.25rem;
}
.hint {
  border-inline-start: 3px solid #748;
  background-color: #edf;
  padding: 1rem;
  margin: 0.5rem 0;
  width: fit-content;
}
.hint code {
  background-color: #fef;
  outline: 1px solid #aaa;
  border-radius: 0.25rem;
  padding: 0.1rem;
  display: inline-block;
}
.hide-hints .hint,
.hide-hints #hideHintsBtn
{
  display: none;
}
#showHintsBtn {
  display: none;
}
.hide-hints #showHintsBtn {
  display: block;
}
.list {
  padding: 0.25rem;
}
.res-group,
.res {
  padding: 0.25rem;
  padding-bottom: 0.5rem;
}
.res-desc {
  margin-top: 0.25rem;
}
`)

let script = Script(/* javascript */ `
function autoFocusKeyword() {
  if (searchForm?.keyword) {
    searchForm.keyword.focus()
    return
  }
  setTimeout(autoFocusKeyword, 33)
}
autoFocusKeyword()

function hideHints() {
  let hide_interval = +localStorage.getItem('hide_interval') || 0
  if (!hide_interval) {
    hide_interval = 1
  } else {
    hide_interval *= 1.5
  }
  localStorage.setItem('hide_interval', hide_interval)
  let hide_hint_until = Date.now() + hide_interval * ${DAY}
  localStorage.setItem('hide_hint_until', hide_hint_until)
  searchForm.classList.add('hide-hints')
}
function showHints() {
  localStorage.removeItem('hide_hint_until')
  localStorage.removeItem('hide_interval')
  searchForm.classList.remove('hide-hints')
}
function autoHideHints() {
  console.log('autoHideHints')
  let hide_hint_until = +localStorage.getItem('hide_hint_until')
  if (Date.now() &lt; hide_hint_until) {
    searchForm.classList.add('hide-hints')
  }
}
autoHideHints()
`)

type SelectedRepo = {
  name: string
  desc: string | null
  url: string
  programming_language: string | null
  username: string
  is_fork: number | null
  deprecated: number | null
  host: string | null
}

let select_repo = db.prepare&lt;void[], SelectedRepo&gt;(/* sql */ `
select
  repo.name
, repo.desc
, repo.url
, ifnull(
    programming_language.name,
    case npm_package.has_types
      when 1 then 'Typescript'
      when 0 then 'Javascript'
    end)
  as programming_language
, author.username
, repo.is_fork
, npm_package.deprecated
, domain.host
from repo
inner join author on author.id = repo.author_id
inner join domain on domain.id = repo.domain_id
left join programming_language on programming_language.id = repo.programming_language_id
left join npm_package on npm_package.repo_id = repo.id
where repo.is_public = 1
`)

type SelectedNpmPackage = {
  name: string
  username: string
  desc: string | null
  weekly_downloads: number | null
  programming_language: string | null
  deprecated: number | null
}

let select_npm_package = db.prepare&lt;void[], SelectedNpmPackage&gt;(/* sql */ `
select
  npm_package.name
, author.username
, npm_package.desc
, npm_package.weekly_downloads
, case npm_package.has_types
    when 1 then 'Typescript'
    when 0 then 'Javascript'
  end as programming_language
, npm_package.deprecated
from npm_package
left join author on author.id = author_id
where repo_id is null
  and npm_package.not_found_time is null
`)

type ResItem = {
  /* for filtering */
  sortKey: string
  host: string | null
  /* for display */
  name: string
  desc: string | null
  url: string
  programming_language: string | null
  username: string
  weekly_downloads: number | null
  is_fork: number | null
  deprecated: number | null
}

let all_file = 'data/all.json'

function select_all(): ResItem[] {
  let items: ResItem[] = []

  if (env.NODE_ENV != 'export') {
    items = readJsonFileSync(all_file)
    if (!(items.length &gt; 0)) {
      throw new Error('missing data in file: ' + all_file)
    }
    return items
  }

  let repos = select_repo.all()
  for (let repo of repos) {
    items.push(
      Object.assign(repo, {
        weekly_downloads: null,
        sortKey: repo.name.toLowerCase(),
      }),
    )
  }

  let npm_packages = select_npm_package.all()
  for (let npm_package of npm_packages) {
    items.push(
      Object.assign(npm_package, {
        url: `https://www.npmjs.com/package/${npm_package.name}`,
        is_fork: null,
        sortKey: npm_package.name.toLowerCase(),
        host: 'www.npmjs.com',
      }),
    )
  }

  return items
}

let allItems = select_all()

// deduplicate by url
allItems = Array.from(new Map(allItems.map(item =&gt; [item.url, item])).values())

// sort by name
allItems.sort((a, b) =&gt; compare(a.sortKey, b.sortKey))

function remove_unused_items() {
  allItems = allItems.filter(item =&gt; {
    if (item.name == '-' || item.name == '~') {
      return false
    }
    return true
  })
}

remove_unused_items()

if (env.NODE_ENV == 'export') {
  writeJsonFileSync(all_file, allItems)
}

function build_search_query(params: URLSearchParams) {
  let action = params.get('form_action')
  let host = params.get('host')
  let username = params.get('username')
  let name = params.get('name')
  let language = params.get('language')
  let desc = params.get('desc')
  let prefix = params.get('prefix')

  let matchedItems = allItems

  let skip_npm = false
  if (host) {
    let include_npm = false
    let include_other = false
    for (let part of host.split(' ')) {
      part = part.trim()
      if (!part) continue
      if (part.startsWith('-npm')) {
        skip_npm = true
        break
      }
      if (part.startsWith('npm')) {
        include_npm = true
        continue
      }
      include_other = true
    }
    skip_npm ||= include_other &amp;&amp; !include_npm
  }

  function search() {
    if (prefix) {
      let pattern = prefix.toLowerCase()
      matchedItems = matchedItems.filter(item =&gt;
        item.sortKey.startsWith(pattern),
      )
    }

    searchField(host, item =&gt; item.host)
    searchField(username, item =&gt; item.username)
    searchField(name, item =&gt; item.name)
    searchField(desc, item =&gt; item.desc)

    searchLanguage()

    return matchedItems
  }

  function searchField(
    value: string | null | undefined,
    viewFn: (item: ResItem) =&gt; string | null,
  ) {
    if (!value) return
    for (let part of value.split(' ')) {
      part = part.trim()
      if (!part) continue

      let not = part[0] == '-'
      if (not) {
        part = part.slice(1)
      }

      let full = part.startsWith('"') &amp;&amp; part.endsWith('"')
      if (full) {
        part = part.slice(1, -1)
      }

      part = part.toLowerCase()

      matchedItems = matchedItems.filter(item =&gt; {
        let value = viewFn(item)?.toLowerCase()
        let matched = full ? value == part : value &amp;&amp; value.includes(part)
        return not ? !matched : matched
      })
    }
  }

  function searchLanguage() {
    if (language) {
      let positive_languages: string[] = []
      let negative_languages: string[] = []
      for (let name of language.split(' ')) {
        if (!name) continue

        let target: string[]
        let not = name[0] == '-'
        if (not) {
          name = name.slice(1)
          target = negative_languages
        } else {
          target = positive_languages
        }

        name = name.toLowerCase()
        if (name == 'js') {
          name = 'javascript'
        } else if (name == 'ts') {
          name = 'typescript'
        }

        target.push(name)
      }
      if (positive_languages.length &gt; 0) {
        matchedItems = matchedItems.filter(item =&gt;
          positive_languages.includes(
            (item.programming_language || '').toLowerCase(),
          ),
        )
      }
      if (negative_languages.length &gt; 0) {
        matchedItems = matchedItems.filter(
          item =&gt;
            !negative_languages.includes(
              (item.programming_language || '').toLowerCase(),
            ),
        )
      }
    }
  }

  return {
    search,
    skip_npm,
    /* from params */
    action,
    host,
    username,
    name,
    language,
    desc,
    prefix,
  }
}

function Page(attrs: {}, context: SearchContext) {
  let { params, query } = context
  let { prefix } = query

  let matchedItems = query.search()
  let match_count = matchedItems.length

  let languageCounts: Record&lt;string, number&gt; = {}
  for (let item of matchedItems) {
    let name = item.programming_language
    if (!name) continue
    let count = languageCounts[name] || 0
    languageCounts[name] = count + 1
  }
  let languageOptions = mapArray(
    Object.entries(languageCounts).sort((a, b) =&gt; b[1] - a[1]),
    ([name, count]) =&gt; (
      &lt;option value={name}&gt;
        {name} ({count})
      &lt;/option&gt;
    ),
  )

  type Match =
    | { type: 'item'; item: ResItem; sortKey: string }
    | { type: 'group'; group: Group; sortKey: string }
  let matches: Match[]

  type Group = {
    prefix: string
    resItems: ResItem[]
  }

  let total_match_threshold = 36
  let group_match_threshold = 5
  if (match_count &gt; total_match_threshold) {
    let prefix_length = prefix ? prefix.length + 1 : 1
    let groupDict: Record&lt;string, Group&gt; = {}
    for (let repo of matchedItems) {
      let prefix = repo.name.slice(0, prefix_length).toLowerCase()
      let group = groupDict[prefix]
      if (!group) {
        group = { prefix, resItems: [repo] }
        groupDict[prefix] = group
      } else {
        group.resItems.push(repo)
      }
    }
    matches = Object.values(groupDict).map(group =&gt; ({
      type: 'group',
      group,
      sortKey: group.prefix.toLowerCase(),
    }))
  } else {
    matches = matchedItems.map(item =&gt; ({
      type: 'item',
      item,
      sortKey: item.name.toLowerCase(),
    }))
  }
  matches = matches
    .flatMap((match): Match[] | Match =&gt; {
      if (
        match.type != 'group' ||
        match.group.resItems.length &gt; group_match_threshold
      )
        return match
      return match.group.resItems.map(item =&gt; ({
        type: 'item',
        item,
        sortKey: item.name,
      }))
    })
    .sort((a, b) =&gt; {
      if (a.type == 'group' &amp;&amp; b.type != 'group') return +1
      if (a.type != 'group' &amp;&amp; b.type == 'group') return -1
      if (a.sortKey &lt; b.sortKey) return -1
      if (a.sortKey &gt; b.sortKey) return +1
      return 0
    })

  let result: Element = [
    'div#result',
    {},
    [
      &lt;p id="loadingMessage"&gt;&lt;/p&gt;,
      prefix ? &lt;p&gt;repo/package prefix: {prefix}*&lt;/p&gt; : null,
      &lt;p&gt;{match_count.toLocaleString()} matches&lt;/p&gt;,
      &lt;div class="list"&gt;
        {mapArray(matches, match =&gt; {
          if (match.type == 'item') {
            return MatchedItem(match.item)
          }
          let {
            group: { prefix, resItems: repos },
          } = match
          let count = repos.length
          if (count == 1) {
            return MatchedItem(repos[0])
          }
          params.set('prefix', prefix)
          let href = '/?' + params
          return (
            &lt;div class="res-group"&gt;
              &lt;Link href={href}&gt;pattern: {prefix}*&lt;/Link&gt; ({count} matches)
            &lt;/div&gt;
          )
        })}
      &lt;/div&gt;,
    ],
  ]
  if (query.action == 'search' &amp;&amp; context.type == 'ws') {
    context.ws.send(['update', nodeToVNode(result, context)])
    context.ws.send([
      'update-in',
      '#languageSelect',
      nodeToVNode(languageOptions, context),
    ])
    throw EarlyTerminate
  }
  return (
    &lt;form
      id="searchForm"
      data-trim-empty
      onsubmit="emitForm(event); loadingMessage.textContent='searching...'"
    &gt;
      &lt;input name="form_action" value="search" hidden /&gt;
      &lt;label&gt;
        Repo Host:{' '}
        &lt;input name="host" placeholder="e.g. npmjs" value={query.host} /&gt;
      &lt;/label&gt;
      &lt;label&gt;
        Username:{' '}
        &lt;input
          name="username"
          placeholder="e.g. beeno"
          value={query.username}
        /&gt;
      &lt;/label&gt;
      &lt;label&gt;
        Repo/Package name:{' '}
        &lt;input
          name="name"
          placeholder={'e.g. react event'}
          value={query.name}
        /&gt;
      &lt;/label&gt;
      &lt;label style="width: fit-content"&gt;
        Programming Languages:{' '}
        &lt;input
          name="language"
          placeholder={'e.g. typescript javascript'}
          value={query.language}
        /&gt;
        &lt;br /&gt;
        &lt;details&gt;
          &lt;summary&gt;Counts by language&lt;/summary&gt;
          &lt;select
            style="width: 100%;"
            multiple
            oninput="searchForm.language.value=Array.from(this.selectedOptions,option=&gt;option.value).join(' ')"
            id="languageSelect"
          &gt;
            {languageOptions}
          &lt;/select&gt;
        &lt;/details&gt;
      &lt;/label&gt;
      &lt;label&gt;
        Description:{' '}
        &lt;input name="desc" placeholder={'e.g. 倉頡'} value={query.desc} /&gt;
      &lt;/label&gt;
      &lt;input type="submit" value="Search" /&gt;
      &lt;div style="margin-top: 0.5rem"&gt;
        &lt;button
          type="button"
          id="hideHintsBtn"
          onclick="hideHints()"
          title="Hide hints for 24 hours"
        &gt;
          Hide Hints
        &lt;/button&gt;
        &lt;button type="button" id="showHintsBtn" onclick="showHints()"&gt;
          Show Hints
        &lt;/button&gt;
      &lt;/div&gt;
      &lt;p class="hint"&gt;
        Hint: you can search by multiple keywords, separated by space, e.g.{' '}
        &lt;code&gt;react event&lt;/code&gt; as searching for repos containing{' '}
        &lt;code&gt;react&lt;/code&gt; and &lt;code&gt;event&lt;/code&gt; in the name (appearing any
        order).
      &lt;/p&gt;
      &lt;p class="hint"&gt;
        Hint: you can indicate negative keywords with hyphen prefix, e.g.{' '}
        &lt;code&gt;-react -ng- chart&lt;/code&gt; as searching for &lt;code&gt;chart&lt;/code&gt;{' '}
        libraries while excluding those framework-specific libraries having{' '}
        &lt;code&gt;react&lt;/code&gt; or &lt;code&gt;ng-&lt;/code&gt; in the name.
      &lt;/p&gt;
      &lt;p class="hint"&gt;
        Hint: multiple keywords are combined with "and" for most fields, but
        they're combined with "or" for programming languages.
      &lt;/p&gt;
      &lt;p class="hint"&gt;
        Hint: the keyboards are matched partially for most fields, but is
        matched exactly for programming languages. So searching{' '}
        &lt;code&gt;Java&lt;/code&gt; will not match &lt;code&gt;Javascript&lt;/code&gt; repos.
      &lt;/p&gt;
      &lt;p class="hint"&gt;
        Hint: if a keyword is wrapped with double quotes, it is matched in full.
        For example searching &lt;code&gt;speed&lt;/code&gt; will matched for{' '}
        &lt;code&gt;frank-dspeed&lt;/code&gt; but searching &lt;code&gt;"speed"&lt;/code&gt; will not
        match for that user. This feature does not apply to the language field
        as it's always matched in full.
      &lt;/p&gt;
      {result}
    &lt;/form&gt;
  )
}

function MatchedItem(res: ResItem) {
  let { desc, programming_language } = res
  return (
    &lt;div class="res"&gt;
      &lt;div&gt;
        {ProgrammingLanguageSpan(programming_language)}
        &lt;b&gt;{res.name}&lt;/b&gt; {res.deprecated ? &lt;span&gt;(deprecated)&lt;/span&gt; : null}{' '}
        {res.username ? &lt;sub&gt;by {res.username}&lt;/sub&gt; : null}{' '}
        {res.is_fork ? &lt;sub&gt;(fork)&lt;/sub&gt; : null}{' '}
      &lt;/div&gt;
      &lt;a target="_blank" href={res.url}&gt;
        {res.url}
      &lt;/a&gt;
      {desc ? &lt;div class="res-desc"&gt;{desc}&lt;/div&gt; : null}
    &lt;/div&gt;
  )
}

function build_search_query_test() {
  allItems = []

  function seedRepo(id: number, url: string, programming_language: string) {
    // e.g. [ 'https:', '', 'github.com', 'beenotung', 'create-ts-liveview' ]
    let parts = url.split('/')
    let host = parts[2]
    let username = parts[3]
    let name = parts[4]
    let item: ResItem = {
      name,
      desc: null,
      url,
      programming_language,
      username,
      is_fork: null,
      deprecated: null,
      host,
      sortKey: name.toLowerCase(),
      weekly_downloads: null,
    }
    allItems.push(item)
    return item
  }
  let samples = [
    seedRepo(
      1,
      'https://github.com/beenotung/create-ts-liveview',
      'Javascript',
    ),
    seedRepo(2, 'https://github.com/beenotung/ts-liveview', 'Typescript'),
    seedRepo(
      3,
      'https://github.com/beenotung/better-sqlite3-proxy',
      'Typescript',
    ),
    seedRepo(4, 'https://github.com/beenotung/net-files', 'HTML'),
    seedRepo(5, 'https://github.com/beenotung/safepic', 'HTML'),
    seedRepo(6, 'https://github.com/beenotung/ga-experiment', 'Java'),
    seedRepo(7, 'https://github.com/beenotung/vue-datepicker', 'Vue'),
    seedRepo(8, 'https://github.com/beenotung/sodoku', 'C'),
    seedRepo(9, 'https://github.com/beenotung/fair-task-pool', 'Typescript'),
    seedRepo(10, 'https://github.com/valor-software/ng2-charts', 'Typescript'),
    seedRepo(11, 'https://github.com/help-me-mom/ng-mocks', 'Typescript'),
    seedRepo(12, 'https://github.com/DethAriel/ng-recaptcha', 'Typescript'),
  ]

  function testLanguage(name: string, language: string, expected: any[]) {
    name = `[Language TestSuit] ${name}`
    let params = new URLSearchParams({ language })
    test(name, params, expected)
  }
  function testName(name: string, repoName: string, expected: any[]) {
    name = `[Name TestSuit] ${name}`
    let params = new URLSearchParams({ name: repoName })
    test(name, params, expected)
  }
  function test(name: string, params: URLSearchParams, expected: any[]) {
    let query = build_search_query(params)

    let actual = query.search()

    if (JSON.stringify(actual) !== JSON.stringify(expected)) {
      console.error('[fail]', name, {
        expected,
        actual,
      })
      process.exit(1)
    }
    console.log('[pass]', name)
  }
  testLanguage('empty query', '', samples)
  testLanguage(
    'single positive language',
    'Typescript',
    samples.filter(repo =&gt; repo.programming_language == 'Typescript'),
  )
  testLanguage(
    'multiple positive languages',
    'Typescript Javascript',
    samples.filter(
      repo =&gt;
        repo.programming_language == 'Typescript' ||
        repo.programming_language == 'Javascript',
    ),
  )
  testLanguage(
    'single negative language',
    '-Javascript',
    samples.filter(repo =&gt; repo.programming_language != 'Javascript'),
  )
  testLanguage(
    'multiple negative languages',
    '-Typescript -Javascript -Java',
    samples.filter(
      repo =&gt;
        repo.programming_language != 'Typescript' &amp;&amp;
        repo.programming_language != 'Javascript' &amp;&amp;
        repo.programming_language != 'Java',
    ),
  )
  testLanguage(
    'mixed positive and negative languages',
    'Typescript -Javascript',
    samples.filter(repo =&gt; repo.programming_language == 'Typescript'),
  )
  testName(
    'single keyword',
    'ga',
    samples.filter(repo =&gt; repo.name.includes('ga')),
  )
  testName(
    'multiple keywords',
    'net files',
    samples.filter(
      repo =&gt; repo.name.includes('net') &amp;&amp; repo.name.includes('files'),
    ),
  )
  testName(
    'negative keyword',
    '-ng',
    samples.filter(repo =&gt; !repo.name.includes('ng')),
  )
  testName(
    'positive keyword with hyphen suffix',
    'ng-',
    samples.filter(repo =&gt; repo.name.includes('ng-')),
  )
  testName(
    'negative keyword with hyphen suffix',
    '-ng-',
    samples.filter(repo =&gt; !repo.name.includes('ng-')),
  )
  testName(
    'multiple keywords with hyphen suffix (continue)',
    'ng- mock',
    samples.filter(
      repo =&gt; repo.name.includes('ng-') &amp;&amp; repo.name.includes('mock'),
    ),
  )
  testName(
    'multiple keywords with hyphen suffix (separated)',
    'fair- pool',
    samples.filter(
      repo =&gt; repo.name.includes('fair-') &amp;&amp; repo.name.includes('pool'),
    ),
  )
  console.log('all passed')
}
if (env.NODE_ENV != 'export' &amp;&amp; process.argv[1] == import.meta.filename) {
  build_search_query_test()
}

// And it can be pre-rendered into html as well
// let Home = prerender(content)

type SearchContext = DynamicContext &amp; {
  params: URLSearchParams
  query: ReturnType&lt;typeof build_search_query&gt;
}

let content = (
  &lt;div id="home"&gt;
    {style}
    &lt;h1&gt;FOSS Git Repository &amp; NPM Package Index&lt;/h1&gt;
    &lt;Page /&gt;
    {script}
    &lt;SourceCode page="home.tsx" /&gt;
  &lt;/div&gt;
)

let routes = {
  '/': {
    menuText: 'Search',
    resolve(context) {
      let params = new URLSearchParams(context.routerMatch?.search)
      let query = build_search_query(params)

      let ctx = context as SearchContext
      ctx.params = params
      ctx.query = query

      function getTitle() {
        let acc = ''

        function add(text: string | null): void {
          if (!text) return
          if (acc) {
            acc += ' '
          }
          acc += text
        }

        add(query.desc)
        add(query.name || (acc ? 'resources' : 'Resources'))
        if (query.prefix) {
          add(`(${query.prefix}*)`)
        }
        if (query.language) {
          add('in ' + query.language)
        }
        if (query.host) {
          add('on ' + query.host)
        }
        if (query.username) {
          add('by ' + query.username)
        }

        return acc.trim()
      }

      return {
        title: getTitle() + ' | FOSS Git Repositories &amp; NPM Packages',
        description:
          'Getting Started with ts-liveview - a server-side rendering realtime webapp framework with progressive enhancement',
        node: content,
      }
    },
  },
} satisfies Routes

export default { routes }
</code></pre></details></div><footer style="border-top: 1px solid #aaa; padding-top: 0.5rem; margin-top: 0.5rem;"><div style="margin-bottom: 0.75rem">Made with 💝 by <a target="_blank" href="https://github.com/beenotung">Beeno</a></div><div id="stats" style="text-align: start"><table><tbody><tr><td>#visit</td><td class="visit">17,019,364</td></tr><tr><td>#session</td><td class="session">7,448</td></tr><tr><td>#live-session</td><td class="live">0</td></tr></tbody></table></div></footer></div>
  </body>
</html>
