Add search functionality

This commit is contained in:
ChaoticByte 2024-08-28 19:49:23 +02:00
parent 149841bdd1
commit 85470a277e
No known key found for this signature in database
8 changed files with 138 additions and 24 deletions

View file

@ -4,12 +4,39 @@ import (
"io/fs"
"log"
"os"
"slices"
"strings"
"golang.org/x/text/search"
)
type Database struct {
Keys []string
Entries map[string]string
matcher *search.Matcher
}
func (db *Database) search(query string) []string { // returns keys (entry names)
results := []string{}
// compile patterns
queryPatterns := []*search.Pattern{}
for _, q := range strings.Split(query, " ") { // per word
queryPatterns = append(queryPatterns, db.matcher.CompileString(q))
}
// search
for _, k := range db.Keys {
patternsFound := 0
for _, p := range queryPatterns {
if s, _ := p.IndexString(db.Entries[k]); s != -1 {
patternsFound++ // this pattern was found
}
}
if patternsFound == len(queryPatterns) && !slices.Contains(results, k) {
// if all patterns were found, add the key (entry name) to the list
results = append(results, k)
}
}
return results
}
func BuildDB(directory string) Database {
@ -19,14 +46,16 @@ func BuildDB(directory string) Database {
var keys []string
entries := map[string]string{}
// get files in directory and read them
directory = strings.TrimRight(directory, "/") // we don't need that last / -> if '/' is used as directory, you are dumb.
directory = strings.TrimRight(directory, "/") // we don't need that last /, don't use the root directory /
entriesDirFs := os.DirFS(directory)
keys, err := fs.Glob(entriesDirFs, "*")
if err != nil { logger.Panicln(err) }
for _, k := range keys {
contentB, err := os.ReadFile(directory + "/" + k)
if err != nil { logger.Panicln(err) }
entries[k] = string(contentB)
content := string(contentB)
entries[k] = content
}
return Database{Keys: keys, Entries: entries}
matcher := search.New(ContentLanguage, search.IgnoreCase, search.IgnoreDiacritics)
return Database{Keys: keys, Entries: entries, matcher: matcher}
}

2
go.mod
View file

@ -1,3 +1,5 @@
module github.com/ChaoticByte/plaintext-encyclopedia
go 1.22.4
require golang.org/x/text v0.17.0

2
go.sum Normal file
View file

@ -0,0 +1,2 @@
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=

25
main.go
View file

@ -5,6 +5,7 @@ import (
"log"
"net/http"
"os"
"path"
"strings"
)
@ -28,16 +29,13 @@ func loadTemplate() {
func handleApplication(w http.ResponseWriter, req *http.Request) {
var entry string
var err error
entryName := strings.Trim(req.URL.Path, "/")
if entryName != "" {
if strings.Contains(entryName, "/") || strings.Contains(entryName, "..") {
// path traversal
logger.Println("Possible path traversal attempt from", req.RemoteAddr, "to", entryName)
w.WriteHeader(http.StatusForbidden)
return
}
entryName := path.Base(req.URL.Path)
if entryName != "/" {
// load entry
entry = db.Entries[entryName]
if entry == "" { // redirect if entry doesn't exist (or is empty)
http.Redirect(w, req, "/", http.StatusTemporaryRedirect)
}
}
err = appTemplate.ExecuteTemplate(
w, "app",
@ -45,6 +43,16 @@ func handleApplication(w http.ResponseWriter, req *http.Request) {
if err != nil { logger.Println(err) }
}
func handleSearchAPI(w http.ResponseWriter, req *http.Request) {
searchQuery := path.Base(req.URL.Path)
if searchQuery == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
results := db.search(searchQuery)
w.Write([]byte(strings.Join(results, "\n")))
}
func main() {
// get logger
logger = log.Default()
@ -57,6 +65,7 @@ func main() {
http.Handle("/static/", staticHandler)
// handle application
http.HandleFunc("/", handleApplication)
http.HandleFunc("/search/", handleSearchAPI)
// start server
logger.Println("Starting server on", ServerListen)
err := http.ListenAndServe(ServerListen, nil)

View file

@ -17,11 +17,16 @@
</div>
</div>
{{- else -}}
<div class="toc">
<div class="home-main">
<input type="text" id="search-box" placeholder="search">
<div id="search-results" class="hidden"></div>
<div class="toc" id="toc">
{{- range .TOC -}}
<div><a href="{{ . }}">{{ . }}</a></div>
{{- end -}}
</div>
</div>
<script src="/static/search.js"></script>
{{- end}}
</body>
</html>

45
public/static/search.js Normal file
View file

@ -0,0 +1,45 @@
(() => {
let searchBox = document.getElementById("search-box");
let searchResults = document.getElementById("search-results");
let toc = document.getElementById("toc");
/**
* @param {string} results
*/
function updateSearchResults(results) {
if (results.length > 0) {
searchResults.innerHTML = "";
for (let i = 0; i < results.length; i++) {
let resultElem = document.createElement("div");
let resultAnchor = document.createElement("a");
resultAnchor.href = results[i]; // we should be at /, so this is right
resultAnchor.innerText = results[i];
resultElem.appendChild(resultAnchor);
searchResults.appendChild(resultElem);
}
toc.classList.add("hidden");
searchResults.classList.remove("hidden");
} else {
toc.classList.remove("hidden");
searchResults.classList.add("hidden");
}
}
async function handleSearchInput() {
// get search query
const query = searchBox.value;
if (query == "") {
updateSearchResults([]);
return
}
// make request
const response = await fetch("/search/" + query);
if (!response.ok) {
throw new Error("Couldn't search, status code ", response.status);
}
let result = await response.text();
updateSearchResults(result.split('\n'));
}
searchBox.addEventListener("input", handleSearchInput);
})();

View file

@ -7,10 +7,11 @@ body {
width: 100vw;
font-family: sans-serif;
}
a:hover { text-decoration: underline; }
a:hover, a:focus { text-decoration: underline; }
a {
color: black;
text-decoration: none;
outline: none;
}
.homebtn {
@ -36,16 +37,34 @@ a {
}
.content { white-space: pre-line; }
.toc {
box-sizing: border-box;
padding: 2rem;
.home-main {
width: 100%;
padding: 2rem;
box-sizing: border-box;
gap: 1rem;
}
.home-main, .toc, #search-results {
display: flex;
flex-direction: column;
}
.toc, #search-results, #search-box { padding: 0 .2rem; }
.toc, #search-results { gap: .2rem; }
#search-box {
width: 100%;
border: none;
outline: none;
border-bottom: 1px solid #00000040;
height: 2rem;
font-size: 1rem;
}
#search-box:active, #search-box:focus, #search-box:hover {
border-bottom: 1px solid black;
}
.hidden { display: none !important; }
@media only screen and (max-width: 750px) {
.main {
margin-top: 4rem;
}
.main { margin-top: 4rem; }
.main > h1, .content {
width: 90vw;
max-width: unset;

View file

@ -1,7 +1,10 @@
package main
var ServerListen = ":7000"
var EntriesDirectory = "./entries"
var TemplateFile = "./public/index.html"
var StaticDirectory = "./public/static"
var MainTitle = "Encyclopedia"
import "golang.org/x/text/language"
const ServerListen = ":7000"
const EntriesDirectory = "./entries"
const TemplateFile = "./public/index.html"
const StaticDirectory = "./public/static"
const MainTitle = "Encyclopedia"
var ContentLanguage = language.English