diff --git a/DocGen4/Output.lean b/DocGen4/Output.lean index 85b90f4..0af12bb 100644 --- a/DocGen4/Output.lean +++ b/DocGen4/Output.lean @@ -70,6 +70,7 @@ def htmlOutput (result : AnalyzerResult) (root : String) : IO Unit := do FS.writeFile (basePath / "style.css") styleCss FS.writeFile (basePath / "404.html") notFoundHtml.toString FS.writeFile (basePath / "nav.js") navJs + FS.writeFile (basePath / "search.js") searchJs for (module, content) in result.moduleInfo.toArray do let moduleHtml := ReaderT.run (moduleToHtml content) config let path := moduleNameToFile basePath module diff --git a/DocGen4/Output/Base.lean b/DocGen4/Output/Base.lean index f66a37e..485739b 100644 --- a/DocGen4/Output/Base.lean +++ b/DocGen4/Output/Base.lean @@ -48,6 +48,7 @@ def moduleNameToDirectory (basePath : FilePath) (n : Name) : FilePath := section Static def styleCss : String := include_str "./static/style.css" def navJs : String := include_str "./static/nav.js" + def searchJs : String := include_str "./static/search.js" end Static def declNameToLink (name : Name) : HtmlM String := do diff --git a/static/nav.js b/static/nav.js index fe1a170..7d5347d 100644 --- a/static/nav.js +++ b/static/nav.js @@ -106,14 +106,23 @@ if (tse != null) { // Simple declaration search // ------------------------- -const searchWorkerURL = new URL(`${siteRoot}searchWorker.js`, window.location); -const declSearch = (q) => new Promise((resolve, reject) => { - const worker = new SharedWorker(searchWorkerURL); - worker.port.start(); - worker.port.onmessage = ({data}) => resolve(data); - worker.port.onmessageerror = (e) => reject(e); - worker.port.postMessage({q}); -}); +const declURL = new URL(`${siteRoot}searchable_data.bmp`, window.location); +const getDecls = (() => { + let decls; + return () => { + if (!decls) decls = new Promise((resolve, reject) => { + const req = new XMLHttpRequest(); + req.responseType = 'json'; + req.addEventListener('load', () => resolve(loadDecls(req.response))); + req.addEventListener('error', () => reject()); + req.open('GET', declURL); + req.send(); + }) + return decls; + } +})() + +const declSearch = async (q) => getMatches(await getDecls(), q); const srId = 'search_results'; document.getElementById('search_form') diff --git a/static/search.js b/static/search.js new file mode 100644 index 0000000..836ef0e --- /dev/null +++ b/static/search.js @@ -0,0 +1,51 @@ +function isSep(c) { + return c === '.' || c === '_'; +} + +function matchCaseSensitive(declName, lowerDeclName, pat) { + let i = 0, j = 0, err = 0, lastMatch = 0 + while (i < declName.length && j < pat.length) { + if (pat[j] === declName[i] || pat[j] === lowerDeclName[i]) { + err += (isSep(pat[j]) ? 0.125 : 1) * (i - lastMatch); + if (pat[j] !== declName[i]) err += 0.5; + lastMatch = i + 1; + j++; + } else if (isSep(declName[i])) { + err += 0.125 * (i + 1 - lastMatch); + lastMatch = i + 1; + } + i++; + } + err += 0.125 * (declName.length - lastMatch); + if (j === pat.length) { + return err; + } +} + +function loadDecls(searchableDataCnt) { + return searchableDataCnt.map(({name, description}) => [name, name.toLowerCase(), description.toLowerCase()]) +} + +function getMatches(decls, pat, maxResults = 30) { + const lowerPats = pat.toLowerCase().split(/\s/g); + const patNoSpaces = pat.replace(/\s/g, ''); + const results = []; + for (const [decl, lowerDecl, lowerDoc] of decls) { + let err = matchCaseSensitive(decl, lowerDecl, patNoSpaces); + + // match all words as substrings of docstring + if (!(err < 3) && pat.length > 3 && lowerPats.every(l => lowerDoc.indexOf(l) != -1)) { + err = 3; + } + + if (err !== undefined) { + results.push({decl, err}); + } + } + return results.sort(({err: a}, {err: b}) => a - b).slice(0, maxResults); +} + +if (typeof process === 'object') { // NodeJS + const data = loadDecls(JSON.parse(require('fs').readFileSync('searchable_data.bmp').toString())); + console.log(getMatches(data, process.argv[2] || 'ltltle')); +} \ No newline at end of file