Merge pull request #38 from xubaiw/win-find

Windows find support among other things.
main
Henrik Böving 2022-02-23 19:44:47 +01:00 committed by GitHub
commit 9dd5e316c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 712 additions and 329 deletions

View File

@ -10,6 +10,7 @@ import DocGen4.Output.Index
import DocGen4.Output.Module import DocGen4.Output.Module
import DocGen4.Output.NotFound import DocGen4.Output.NotFound
import DocGen4.Output.Find import DocGen4.Output.Find
import DocGen4.Output.Semantic
namespace DocGen4 namespace DocGen4
@ -65,28 +66,46 @@ def htmlOutput (result : AnalyzerResult) (root : String) : IO Unit := do
let config := { root := root, result := result, currentName := none, sourceLinker := ←sourceLinker} let config := { root := root, result := result, currentName := none, sourceLinker := ←sourceLinker}
let basePath := FilePath.mk "." / "build" / "doc" let basePath := FilePath.mk "." / "build" / "doc"
let indexHtml := ReaderT.run index config let indexHtml := ReaderT.run index config
let findHtml := ReaderT.run find config
let notFoundHtml := ReaderT.run notFound config let notFoundHtml := ReaderT.run notFound config
FS.createDirAll basePath FS.createDirAll basePath
FS.createDirAll (basePath / "find") FS.createDirAll (basePath / "find")
FS.createDirAll (basePath / "semantic")
let mut declList := #[] let mut declList := #[]
for (_, mod) in result.moduleInfo.toArray do for (_, mod) in result.moduleInfo.toArray do
for decl in filterMapDocInfo mod.members do for decl in filterMapDocInfo mod.members do
let findHtml := ReaderT.run (findRedirectHtml decl.getName) config let name := decl.getName.toString
let findDir := basePath / "find" / decl.getName.toString let doc := decl.getDocString.getD ""
FS.createDirAll findDir let link := root ++ s!"semantic/{decl.getName.hash}.xml#"
FS.writeFile (findDir / "index.html") findHtml.toString let docLink := Id.run <| ReaderT.run (declNameToLink decl.getName) config
let obj := Json.mkObj [("name", decl.getName.toString), ("description", decl.getDocString.getD "")] let sourceLink := Id.run <| ReaderT.run (getSourceUrl mod.name decl.getDeclarationRange) config
let obj := Json.mkObj [("name", name), ("doc", doc), ("link", link), ("docLink", docLink), ("sourceLink", sourceLink)]
declList := declList.push obj declList := declList.push obj
let xml := toString <| Id.run <| ReaderT.run (semanticXml decl) config
FS.writeFile (basePath / "semantic" / s!"{decl.getName.hash}.xml") xml
let json := Json.arr declList let json := Json.arr declList
FS.writeFile (basePath / "searchable_data.bmp") json.compress FS.writeFile (basePath / "semantic" / "docgen4.xml") <| toString <| Id.run <| ReaderT.run schemaXml config
FS.writeFile (basePath / "index.html") indexHtml.toString FS.writeFile (basePath / "index.html") indexHtml.toString
FS.writeFile (basePath / "style.css") styleCss
FS.writeFile (basePath / "404.html") notFoundHtml.toString FS.writeFile (basePath / "404.html") notFoundHtml.toString
FS.writeFile (basePath / "find" / "index.html") findHtml.toString
FS.writeFile (basePath / "style.css") styleCss
let declarationDataPath := basePath / "declaration-data.bmp"
FS.writeFile declarationDataPath json.compress
FS.writeFile (basePath / "declaration-data.timestamp") <| toString (←declarationDataPath.metadata).modified.sec
FS.writeFile (basePath / "site-root.js") (siteRootJs.replace "{siteRoot}" config.root)
FS.writeFile (basePath / "declaration-data.js") declarationDataCenterJs
FS.writeFile (basePath / "nav.js") navJs FS.writeFile (basePath / "nav.js") navJs
FS.writeFile (basePath / "find" / "find.js") findJs
FS.writeFile (basePath / "how-about.js") howAboutJs
FS.writeFile (basePath / "search.js") searchJs FS.writeFile (basePath / "search.js") searchJs
FS.writeFile (basePath / "mathjax-config.js") mathjaxConfigJs FS.writeFile (basePath / "mathjax-config.js") mathjaxConfigJs
for (module, content) in result.moduleInfo.toArray do for (module, content) in result.moduleInfo.toArray do
let moduleHtml := ReaderT.run (moduleToHtml content) config let moduleHtml := ReaderT.run (moduleToHtml content) config
let path := moduleNameToFile basePath module let path := moduleNameToFile basePath module

View File

@ -47,8 +47,12 @@ def moduleNameToDirectory (basePath : FilePath) (n : Name) : FilePath :=
section Static section Static
def styleCss : String := include_str "./static/style.css" def styleCss : String := include_str "./static/style.css"
def siteRootJs : String := include_str "./static/site-root.js"
def declarationDataCenterJs : String := include_str "./static/declaration-data.js"
def navJs : String := include_str "./static/nav.js" def navJs : String := include_str "./static/nav.js"
def howAboutJs : String := include_str "./static/how-about.js"
def searchJs : String := include_str "./static/search.js" def searchJs : String := include_str "./static/search.js"
def findJs : String := include_str "./static/find/find.js"
def mathjaxConfigJs : String := include_str "./static/mathjax-config.js" def mathjaxConfigJs : String := include_str "./static/mathjax-config.js"
end Static end Static

View File

@ -6,10 +6,16 @@ namespace Output
open scoped DocGen4.Jsx open scoped DocGen4.Jsx
open Lean open Lean
def findRedirectHtml (decl : Name) : HtmlM Html := do def find : HtmlM Html := do
let res ← getResult pure
let url ← declNameToLink decl <html lang="en">
let contentString := s!"0;url={url}" <head>
pure $ Html.element "meta" false #[("http-equiv", "refresh"), ("content", contentString)] #[] <link rel="preload" href={s!"{←getRoot}declaration-data.bmp"}/>
<script type="module" async="true" src={s!"./find.js"}></script>
</head>
<body></body>
</html>
end Output end Output
end DocGen4 end DocGen4

View File

@ -0,0 +1,58 @@
import DocGen4.Output.Template
import DocGen4.Output.DocString
import Lean.Data.Xml
open Lean Xml
namespace DocGen4
namespace Output
instance : ToString $ Array Element where
toString xs := xs.map toString |>.foldl String.append ""
instance : Coe Element Content where
coe e := Content.Element e
-- TODO: syntax metaprogramming and basic semantic data
def semanticXml (i : DocInfo) : HtmlM $ Array Element := do
pure #[
Element.Element
"rdf:RDF"
(Std.RBMap.fromList [
("xmlns:rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
("xmlns:docgen4", s!"{←getRoot}semactic/docgen4.xml#")
] _)
#[
Element.Element
"rdf:Description"
(Std.RBMap.fromList [
("rdf:about", s!"{←getRoot}semactic/{i.getName.hash}.xml#")
] _)
#[]
]
]
def schemaXml : HtmlM $ Array Element := do
pure #[
Element.Element
"rdf:RDF"
(Std.RBMap.fromList [
("xmlns:rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
("xmlns:docgen4", s!"{←getRoot}semactic/docgen4.xml#")
] _)
#[
Element.Element
"docgen4:hasInstance"
Std.RBMap.empty
#[
Element.Element
"rdfs:type"
Std.RBMap.empty
#[Content.Character "rdf:Property"]
]
]
]
end Output
end DocGen4

View File

@ -15,46 +15,48 @@ def baseHtmlArray (title : String) (site : Array Html) : HtmlM Html := do
pure pure
<html lang="en"> <html lang="en">
<head> <head>
<title>{title}</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href={s!"{←getRoot}style.css"}/> <link rel="stylesheet" href={s!"{←getRoot}style.css"}/>
<link rel="stylesheet" href={s!"{←getRoot}pygments.css"}/> <link rel="stylesheet" href={s!"{←getRoot}pygments.css"}/>
<link rel="shortcut icon" href={s!"{←getRoot}favicon.ico"}/> <link rel="shortcut icon" href={s!"{←getRoot}favicon.ico"}/>
<title>{title}</title> <link rel="prefetch" href={s!"{←getRoot}declaration-data.bmp"}/>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/> <script defer="true" src={s!"{←getRoot}mathjax-config.js"}></script>
<script defer="true" src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script defer="true" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script type="module" src={s!"{←getRoot}nav.js"}></script>
<script type="module" src={s!"{←getRoot}search.js"}></script>
<script type="module" src={s!"{←getRoot}how-about.js"}></script>
</head> </head>
<body> <body>
<input id="nav_toggle" type="checkbox"/> <input id="nav_toggle" type="checkbox"/>
<header> <header>
<h1><label «for»="nav_toggle"></label>Documentation</h1> <h1><label «for»="nav_toggle"></label>Documentation</h1>
<p «class»="header_filename break_within">{title}</p> <p «class»="header_filename break_within">{title}</p>
-- TODO: Replace this form with our own search -- TODO: Replace this form with our own search
<form action="https://google.com/search" method="get" id="search_form"> <form action="https://google.com/search" method="get" id="search_form">
<input type="hidden" name="sitesearch" value="https://leanprover-community.github.io/mathlib_docs"/> <input type="hidden" name="sitesearch" value="https://leanprover-community.github.io/mathlib_docs"/>
<input type="text" name="q" autocomplete="off"/> <input type="text" name="q" autocomplete="off"/>
<button>Google site search</button> <button>Google site search</button>
</form> </form>
</header> </header>
[site] [site]
{←navbar} {←navbar}
-- Lean in JS in HTML in Lean...very meta
<script>
siteRoot = "{←getRoot}";
</script>
-- TODO Add more js stuff
<script src={s!"{←getRoot}nav.js"}></script>
<script src={s!"{←getRoot}search.js"}></script>
-- mathjax
<script src={s!"{←getRoot}mathjax-config.js"}></script>
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
</body> </body>
</html> </html>

243
static/declaration-data.js Normal file
View File

@ -0,0 +1,243 @@
/**
* This module is a wrapper that facilitates manipulating the declaration data.
*
* Please see {@link DeclarationDataCenter} for more information.
*/
import { SITE_ROOT } from "./site-root.js";
const CACHE_DB_NAME = "declaration-data";
const CACHE_DB_VERSION = 1;
/**
* The DeclarationDataCenter is used for declaration searching.
*
* For usage, see the {@link init} and {@link search} methods.
*/
export class DeclarationDataCenter {
/**
* The declaration data. Users should not interact directly with this field.
*
* *NOTE:* This is not made private to support legacy browsers.
*/
declarationData = null;
/**
* Used to implement the singleton, in case we need to fetch data mutiple times in the same page.
*/
static singleton = null;
/**
* Construct a DeclarationDataCenter with given data.
*
* Please use {@link DeclarationDataCenter.init} instead, which automates the data fetching process.
* @param {*} declarationData
*/
constructor(declarationData) {
this.declarationData = declarationData;
}
/**
* The actual constructor of DeclarationDataCenter
* @returns {Promise<DeclarationDataCenter>}
*/
static async init() {
if (!DeclarationDataCenter.singleton) {
const timestampUrl = new URL(
`${SITE_ROOT}declaration-data.timestamp`,
window.location
);
const dataUrl = new URL(
`${SITE_ROOT}declaration-data.bmp`,
window.location
);
const timestampRes = await fetch(timestampUrl);
const timestamp = await timestampRes.text();
// try to use cache first
const data = await fetchCachedDeclarationData(timestamp);
if (data) {
// if data is defined, use the cached one.
DeclarationDataCenter.singleton = new DeclarationDataCenter(data);
} else {
// undefined. then fetch the data from the server.
const dataRes = await fetch(dataUrl);
const dataJson = await dataRes.json();
// the data is a map of name (original case) to declaration data.
const data = new Map(
dataJson.map(({ name, doc, link, docLink, sourceLink }) => [
name,
{
name,
lowerName: name.toLowerCase(),
lowerDoc: doc.toLowerCase(),
link,
docLink,
sourceLink,
},
])
);
await cacheDeclarationData(timestamp, data);
DeclarationDataCenter.singleton = new DeclarationDataCenter(data);
}
}
return DeclarationDataCenter.singleton;
}
/**
* Search for a declaration.
* @returns {Array<any>}
*/
search(pattern, strict = true) {
if (!pattern) {
return [];
}
if (strict) {
let decl = this.declarationData.get(pattern);
return decl ? [decl] : [];
} else {
return getMatches(this.declarationData, pattern);
}
}
}
function isSeparater(char) {
return char === "." || char === "_";
}
// HACK: the fuzzy matching is quite hacky
function matchCaseSensitive(declName, lowerDeclName, pattern) {
let i = 0,
j = 0,
err = 0,
lastMatch = 0;
while (i < declName.length && j < pattern.length) {
if (pattern[j] === declName[i] || pattern[j] === lowerDeclName[i]) {
err += (isSeparater(pattern[j]) ? 0.125 : 1) * (i - lastMatch);
if (pattern[j] !== declName[i]) err += 0.5;
lastMatch = i + 1;
j++;
} else if (isSeparater(declName[i])) {
err += 0.125 * (i + 1 - lastMatch);
lastMatch = i + 1;
}
i++;
}
err += 0.125 * (declName.length - lastMatch);
if (j === pattern.length) {
return err;
}
}
function getMatches(declarations, pattern, maxResults = 30) {
const lowerPats = pattern.toLowerCase().split(/\s/g);
const patNoSpaces = pattern.replace(/\s/g, "");
const results = [];
for (const {
name,
lowerName,
lowerDoc,
link,
docLink,
sourceLink,
} of declarations.values()) {
let err = matchCaseSensitive(name, lowerName, patNoSpaces);
// match all words as substrings of docstring
if (
err >= 3 &&
pattern.length > 3 &&
lowerPats.every((l) => lowerDoc.indexOf(l) != -1)
) {
err = 3;
}
if (err !== undefined) {
results.push({
name,
err,
lowerName,
lowerDoc,
link,
docLink,
sourceLink,
});
}
}
return results.sort(({ err: a }, { err: b }) => a - b).slice(0, maxResults);
}
// TODO: refactor the indexedDB part to be more robust
/**
* Get the indexedDB database, automatically initialized.
* @returns {Promise<IDBDatabase>}
*/
async function getDeclarationDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(CACHE_DB_NAME, CACHE_DB_VERSION);
request.onerror = function (event) {
reject(
new Error(
`fail to open indexedDB ${CACHE_DB_NAME} of version ${CACHE_DB_VERSION}`
)
);
};
request.onupgradeneeded = function (event) {
let db = event.target.result;
// We only need to store one object, so no key path or increment is needed.
db.createObjectStore("declaration");
};
request.onsuccess = function (event) {
resolve(event.target.result);
};
});
}
/**
* Store data in indexedDB object store.
* @param {string} timestamp
* @param {Map<string, any>} data
*/
async function cacheDeclarationData(timestamp, data) {
let db = await getDeclarationDatabase();
let store = db
.transaction("declaration", "readwrite")
.objectStore("declaration");
return new Promise((resolve, reject) => {
let clearRequest = store.clear();
let addRequest = store.add(data, timestamp);
addRequest.onsuccess = function (event) {
resolve();
};
addRequest.onerror = function (event) {
reject(new Error(`fail to store declaration data`));
};
clearRequest.onerror = function (event) {
reject(new Error("fail to clear object store"));
};
});
}
/**
* Retrieve data from indexedDB database.
* @param {string} timestamp
* @returns {Promise<Map<string, any>|undefined>}
*/
async function fetchCachedDeclarationData(timestamp) {
let db = await getDeclarationDatabase();
let store = db
.transaction("declaration", "readonly")
.objectStore("declaration");
return new Promise((resolve, reject) => {
let transactionRequest = store.get(timestamp);
transactionRequest.onsuccess = function (event) {
resolve(event.result);
};
transactionRequest.onerror = function (event) {
reject(new Error(`fail to store declaration data`));
};
});
}

75
static/find/find.js Normal file
View File

@ -0,0 +1,75 @@
/**
* This module is used for the `/find` endpoint.
*
* Two basic kinds of search syntax are supported:
*
* 1. Query-Fragment syntax: `/find?pattern=Nat.add#doc` for documentation and `/find?pattern=Nat.add#src` for source code.
* 2. Fragment-Only syntax:`/find/#doc/Nat.add` for documentation and `/find/#src/Nat.add` for source code.
*
* Though both of them are valid, the first one is highly recommended, and the second one is for compatibility and taste.
*
* There are some extended usage for the QF syntax. For example, appending `strict=false` to the query part
* (`/find?pattern=Nat.add&strict=false#doc`) will allow you to use the fuzzy search, rather than strict match.
* Also, the fragment is extensible as well. For now only `#doc` and `#src` are implement, and the plain query without
* hash (`/find?pattern=Nat.add`) is used for computer-friendly data (semantic web is great! :P), while all other fragments
* fallback to the `#doc` view.
*/
import { SITE_ROOT } from "../site-root.js";
import { DeclarationDataCenter } from "../declaration-data.js";
/**
* We don't use browser's default hash and searchParams in case Lean declaration name
* can be like `«#»`, rather we manually handle the `window.location.href` with regex.
*/
const LEAN_FRIENDLY_URL_REGEX = /^[^?#]+(?:\?((?:[^«#»]|«.*»)*))?(?:#(.*))?$/;
const LEAN_FRIENDLY_AND_SEPARATOR = /(?<!«[^»]*)&/;
const LEAN_FRIENDLY_EQUAL_SEPARATOR = /(?<!«[^»]*)=/;
const LEAN_FRIENDLY_SLASH_SEPARATOR = /(?<!«[^»]*)\//;
const [_, query, fragment] = LEAN_FRIENDLY_URL_REGEX.exec(window.location.href);
const queryParams = new Map(
query
?.split(LEAN_FRIENDLY_AND_SEPARATOR)
?.map((p) => p.split(LEAN_FRIENDLY_EQUAL_SEPARATOR))
?.filter((l) => l.length == 2 && l[0].length > 0)
);
const fragmentPaths = fragment?.split(LEAN_FRIENDLY_SLASH_SEPARATOR) ?? [];
const pattern = queryParams.get("pattern") ?? fragmentPaths[1]; // if first fail then second, may be undefined
const strict = (queryParams.get("strict") ?? "true") === "true"; // default to true
const view = fragmentPaths[0];
findAndRedirect(pattern, strict, view);
/**
* Find the result and redirect to the result page.
* @param {string} pattern the pattern to search for
* @param {string} view the view of the find result (`"doc"` or `"src"` for now)
*/
async function findAndRedirect(pattern, strict, view) {
// if no pattern provided, directly redirect to the 404 page
if (!pattern) {
window.location.replace(`${SITE_ROOT}404.html`);
}
// search for result
const dataCenter = await DeclarationDataCenter.init();
let result = (dataCenter.search(pattern, strict) ?? [])[0]; // in case return non array
// if no result found, redirect to the 404 page
if (!result) {
// TODO: better url semantic for 404, current implementation will lead to duplicate search for fuzzy match if not found.
window.location.replace(`${SITE_ROOT}404.html#${pattern ?? ""}`);
} else {
// success, redirect to doc or source page, or to the semantic rdf.
if (!view) {
window.location.replace(result.link);
} else if (view == "doc") {
window.location.replace(result.docLink);
} else if (view == "src") {
window.location.replace(result.sourceLink);
} else {
// fallback to doc page
window.location.replace(result.docLink);
}
}
}

39
static/how-about.js Normal file
View File

@ -0,0 +1,39 @@
/**
* This module implements the `howabout` functionality in the 404 page.
*/
import { DeclarationDataCenter } from "./declaration-data.js";
const HOW_ABOUT = document.querySelector("#howabout");
// Show url of the missing page
if (HOW_ABOUT) {
HOW_ABOUT.parentNode
.insertBefore(document.createElement("pre"), HOW_ABOUT)
.appendChild(document.createElement("code")).innerText =
window.location.href.replace(/[/]/g, "/\u200b");
// TODO: add how about functionality for similar page as well.
const pattern = window.location.hash.replace("#", "");
// try to search for similar declarations
if (pattern) {
HOW_ABOUT.innerText = "Please wait a second. I'll try to help you.";
DeclarationDataCenter.init().then((dataCenter) => {
let results = dataCenter.search(pattern, false);
if (results.length > 0) {
HOW_ABOUT.innerText = "How about one of these instead:";
const ul = HOW_ABOUT.appendChild(document.createElement("ul"));
for (const { name, docLink } of results) {
const li = ul.appendChild(document.createElement("li"));
const a = li.appendChild(document.createElement("a"));
a.href = docLink;
a.appendChild(document.createElement("code")).innerText = name;
}
} else {
HOW_ABOUT.innerText =
"Sorry, I cannot find any similar declarations. Check the link or use the module navigation to find what you want :P";
}
});
}
}

View File

@ -1,17 +1,41 @@
/*
* This file is for configing MathJax behavior.
* See https://docs.mathjax.org/en/latest/web/configuration.html
*
* This configuration is copied from old doc-gen3
* https://github.com/leanprover-community/doc-gen
*/
MathJax = { MathJax = {
tex: { tex: {
inlineMath: [['$', '$']], inlineMath: [["$", "$"]],
displayMath: [['$$', '$$']] displayMath: [["$$", "$$"]],
}, },
options: { options: {
skipHtmlTags: [ skipHtmlTags: [
'script', 'noscript', 'style', 'textarea', 'pre', "script",
'code', 'annotation', 'annotation-xml', "noscript",
'decl', 'decl_meta', 'attributes', 'decl_args', 'decl_header', 'decl_name', "style",
'decl_type', 'equation', 'equations', 'structure_field', 'structure_fields', "textarea",
'constructor', 'constructors', 'instances' "pre",
], "code",
ignoreHtmlClass: 'tex2jax_ignore', "annotation",
processHtmlClass: 'tex2jax_process', "annotation-xml",
}, "decl",
"decl_meta",
"attributes",
"decl_args",
"decl_header",
"decl_name",
"decl_type",
"equation",
"equations",
"structure_field",
"structure_fields",
"constructor",
"constructors",
"instances",
],
ignoreHtmlClass: "tex2jax_ignore",
processHtmlClass: "tex2jax_process",
},
}; };

View File

@ -1,248 +1,43 @@
// Persistent expansion cookie for the file tree /**
// --------------------------------------------- * This module is used to implement persistent navbar expansion.
*/
// The variable to store the expansion information.
let expanded = {}; let expanded = {};
for (const e of (sessionStorage.getItem('expanded') || '').split(',')) {
if (e !== '') { // Load expansion information from sessionStorage.
for (const e of (sessionStorage.getItem("expanded") || "").split(",")) {
if (e !== "") {
expanded[e] = true; expanded[e] = true;
} }
} }
/**
* Save expansion information to sessionStorage.
*/
function saveExpanded() { function saveExpanded() {
sessionStorage.setItem("expanded", sessionStorage.setItem(
Object.getOwnPropertyNames(expanded).filter((e) => expanded[e]).join(",")); "expanded",
Object.getOwnPropertyNames(expanded)
.filter((e) => expanded[e])
.join(",")
);
} }
for (const elem of document.getElementsByClassName('nav_sect')) { // save expansion information when user change the expansion.
const id = elem.getAttribute('data-path'); for (const elem of document.getElementsByClassName("nav_sect")) {
const id = elem.getAttribute("data-path");
if (!id) continue; if (!id) continue;
if (expanded[id]) { if (expanded[id]) {
elem.open = true; elem.open = true;
} }
elem.addEventListener('toggle', () => { elem.addEventListener("toggle", () => {
expanded[id] = elem.open; expanded[id] = elem.open;
saveExpanded(); saveExpanded();
}); });
} }
for (const currentFileLink of document.getElementsByClassName('visible')) { // Scroll to center.
currentFileLink.scrollIntoView({block: 'center'}); for (const currentFileLink of document.getElementsByClassName("visible")) {
} currentFileLink.scrollIntoView({ block: "center" });
// Tactic list tag filter
// ----------------------
function filterSelectionClass(tagNames, classname) {
if (tagNames.length == 0) {
for (const elem of document.getElementsByClassName(classname)) {
elem.classList.remove("hide");
}
} else {
// Add the "show" class (display:block) to the filtered elements, and remove the "show" class from the elements that are not selected
for (const elem of document.getElementsByClassName(classname)) {
elem.classList.add("hide");
for (const tagName of tagNames) {
if (elem.classList.contains(tagName)) {
elem.classList.remove("hide");
}
}
}
}
}
function filterSelection(c) {
filterSelectionClass(c, "tactic");
filterSelectionClass(c, "taclink");
}
var filterBoxes = document.getElementsByClassName("tagfilter");
function updateDisplay() {
filterSelection(getSelectValues());
}
function getSelectValues() {
var result = [];
for (const opt of filterBoxes) {
if (opt.checked) {
result.push(opt.value);
}
}
return result;
}
function setSelectVal(val) {
for (const opt of filterBoxes) {
opt.checked = val;
}
}
updateDisplay();
for (const opt of filterBoxes) {
opt.addEventListener('change', updateDisplay);
}
const tse = document.getElementById("tagfilter-selectall")
if (tse != null) {
tse.addEventListener('change', function() {
setSelectVal(this.checked);
updateDisplay();
});
}
// Simple declaration search
// -------------------------
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')
.appendChild(document.createElement('div'))
.id = srId;
function handleSearchCursorUpDown(down) {
const sel = document.querySelector(`#${srId} .selected`);
const sr = document.getElementById(srId);
if (sel) {
sel.classList.remove('selected');
const toSelect = down ?
sel.nextSibling || sr.firstChild:
sel.previousSibling || sr.lastChild;
toSelect && toSelect.classList.add('selected');
} else {
const toSelect = down ? sr.firstChild : sr.lastChild;
toSelect && toSelect.classList.add('selected');
}
}
function handleSearchEnter() {
const sel = document.querySelector(`#${srId} .selected`)
|| document.getElementById(srId).firstChild;
sel.click();
}
const searchInput = document.querySelector('#search_form input[name=q]');
searchInput.addEventListener('keydown', (ev) => {
switch (ev.key) {
case 'Down':
case 'ArrowDown':
ev.preventDefault();
handleSearchCursorUpDown(true);
break;
case 'Up':
case 'ArrowUp':
ev.preventDefault();
handleSearchCursorUpDown(false);
break;
case 'Enter':
ev.preventDefault();
handleSearchEnter();
break;
}
});
searchInput.addEventListener('input', async (ev) => {
const text = ev.target.value;
if (!text) {
const sr = document.getElementById(srId);
sr.removeAttribute('state');
sr.replaceWith(sr.cloneNode(false));
return;
}
document.getElementById(srId).setAttribute('state', 'loading');
const result = await declSearch(text);
if (ev.target.value != text) return;
const oldSR = document.getElementById('search_results');
const sr = oldSR.cloneNode(false);
for (const {decl} of result) {
const d = sr.appendChild(document.createElement('a'));
d.innerText = decl;
d.title = decl;
d.href = `${siteRoot}find/${decl}`;
}
sr.setAttribute('state', 'done');
oldSR.replaceWith(sr);
});
// 404 page goodies
// ----------------
const howabout = document.getElementById('howabout');
if (howabout) {
howabout.innerText = "Please wait a second. I'll try to help you.";
howabout.parentNode
.insertBefore(document.createElement('pre'), howabout)
.appendChild(document.createElement('code'))
.innerText = window.location.href.replace(/[/]/g, '/\u200b');
const query = window.location.href.match(/[/]([^/]+)(?:\.html|[/])?$/)[1];
declSearch(query).then((results) => {
howabout.innerText = 'How about one of these instead:';
const ul = howabout.appendChild(document.createElement('ul'));
for (const {decl} of results) {
const li = ul.appendChild(document.createElement('li'));
const a = li.appendChild(document.createElement('a'));
a.href = `${siteRoot}find/${decl}`;
a.appendChild(document.createElement('code')).innerText = decl;
}
});
}
// Rewrite GitHub links
// --------------------
for (const elem of document.getElementsByClassName('gh_link')) {
const a = elem.firstElementChild;
// commit is set in add_commit.js
for (const [prefix, replacement] of commit) {
if (a.href.startsWith(prefix)) {
a.href = a.href.replace(prefix, replacement);
break;
}
}
} }

View File

@ -1,51 +1,104 @@
function isSep(c) { /**
return c === '.' || c === '_'; * This module is used to handle user's interaction with the search form.
*/
import { DeclarationDataCenter } from "./declaration-data.js";
const SEARCH_FORM = document.querySelector("#search_form");
const SEARCH_INPUT = SEARCH_FORM.querySelector("input[name=q]");
// Create an `div#search_results` to hold all search results.
let sr = document.createElement("div");
sr.id = "search_results";
SEARCH_FORM.appendChild(sr);
/**
* Attach `selected` class to the the selected search result.
*/
function handleSearchCursorUpDown(down) {
const sel = sr.querySelector(`.selected`);
if (sel) {
sel.classList.remove("selected");
const toSelect = down
? sel.nextSibling || sr.firstChild
: sel.previousSibling || sr.lastChild;
toSelect && toSelect.classList.add("selected");
} else {
const toSelect = down ? sr.firstChild : sr.lastChild;
toSelect && toSelect.classList.add("selected");
}
} }
function matchCaseSensitive(declName, lowerDeclName, pat) { /**
let i = 0, j = 0, err = 0, lastMatch = 0 * Perform search (when enter is pressed).
while (i < declName.length && j < pat.length) { */
if (pat[j] === declName[i] || pat[j] === lowerDeclName[i]) { function handleSearchEnter() {
err += (isSep(pat[j]) ? 0.125 : 1) * (i - lastMatch); const sel =
if (pat[j] !== declName[i]) err += 0.5; sr.querySelector(`.selected`) ||
lastMatch = i + 1; sr.firstChild;
j++; sel.click();
} 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()]) * Allow user to navigate search results with up/down arrow keys, and choose with enter.
*/
SEARCH_INPUT.addEventListener("keydown", (ev) => {
switch (ev.key) {
case "Down":
case "ArrowDown":
ev.preventDefault();
handleSearchCursorUpDown(true);
break;
case "Up":
case "ArrowUp":
ev.preventDefault();
handleSearchCursorUpDown(false);
break;
case "Enter":
ev.preventDefault();
handleSearchEnter();
break;
}
});
/**
* Remove all children of a DOM node.
*/
function removeAllChildren(node) {
while (node.firstChild) {
node.removeChild(node.lastChild);
}
} }
function getMatches(decls, pat, maxResults = 30) { /**
const lowerPats = pat.toLowerCase().split(/\s/g); * Search autocompletion.
const patNoSpaces = pat.replace(/\s/g, ''); */
const results = []; SEARCH_INPUT.addEventListener("input", async (ev) => {
for (const [decl, lowerDecl, lowerDoc] of decls) { const text = ev.target.value;
let err = matchCaseSensitive(decl, lowerDecl, patNoSpaces);
// match all words as substrings of docstring // If no input clear all.
if (!(err < 3) && pat.length > 3 && lowerPats.every(l => lowerDoc.indexOf(l) != -1)) { if (!text) {
err = 3; sr.removeAttribute("state");
} removeAllChildren(sr);
return;
}
if (err !== undefined) { // searching
results.push({decl, err}); sr.setAttribute("state", "loading");
} const dataCenter = await DeclarationDataCenter.init();
} const result = dataCenter.search(text, false);
return results.sort(({err: a}, {err: b}) => a - b).slice(0, maxResults);
}
if (typeof process === 'object') { // NodeJS // in case user has updated the input.
const data = loadDecls(JSON.parse(require('fs').readFileSync('searchable_data.bmp').toString())); if (ev.target.value != text) return;
console.log(getMatches(data, process.argv[2] || 'ltltle'));
} // update search results
removeAllChildren(sr);
for (const { name, docLink } of result) {
const d = sr.appendChild(document.createElement("a"));
d.innerText = name;
d.title = name;
d.href = docLink;
}
sr.setAttribute("state", "done");
});

4
static/site-root.js Normal file
View File

@ -0,0 +1,4 @@
/**
* Get the site root, {siteRoot} is to be replaced by doc-gen4.
*/
export const SITE_ROOT = "{siteRoot}";

61
static/tactic.js Normal file
View File

@ -0,0 +1,61 @@
// TODO: The tactic part seem to be unimplemented now.
function filterSelectionClass(tagNames, classname) {
if (tagNames.length == 0) {
for (const elem of document.getElementsByClassName(classname)) {
elem.classList.remove("hide");
}
} else {
// Add the "show" class (display:block) to the filtered elements, and remove the "show" class from the elements that are not selected
for (const elem of document.getElementsByClassName(classname)) {
elem.classList.add("hide");
for (const tagName of tagNames) {
if (elem.classList.contains(tagName)) {
elem.classList.remove("hide");
}
}
}
}
}
function filterSelection(c) {
filterSelectionClass(c, "tactic");
filterSelectionClass(c, "taclink");
}
var filterBoxes = document.getElementsByClassName("tagfilter");
function updateDisplay() {
filterSelection(getSelectValues());
}
function getSelectValues() {
var result = [];
for (const opt of filterBoxes) {
if (opt.checked) {
result.push(opt.value);
}
}
return result;
}
function setSelectVal(val) {
for (const opt of filterBoxes) {
opt.checked = val;
}
}
updateDisplay();
for (const opt of filterBoxes) {
opt.addEventListener("change", updateDisplay);
}
const tse = document.getElementById("tagfilter-selectall");
if (tse != null) {
tse.addEventListener("change", function () {
setSelectVal(this.checked);
updateDisplay();
});
}