560 lines
18 KiB
JavaScript
560 lines
18 KiB
JavaScript
window.searchState = {
|
|
timeout: null,
|
|
inputElem: document.getElementById("search-input"),
|
|
lastSearch: '',
|
|
clearInput: () => {
|
|
searchState.inputElem.value = "";
|
|
searchState.filterLints();
|
|
},
|
|
clearInputTimeout: () => {
|
|
if (searchState.timeout !== null) {
|
|
clearTimeout(searchState.timeout);
|
|
searchState.timeout = null
|
|
}
|
|
},
|
|
resetInputTimeout: () => {
|
|
searchState.clearInputTimeout();
|
|
setTimeout(searchState.filterLints, 50);
|
|
},
|
|
filterLints: () => {
|
|
function matchesSearch(lint, terms, searchStr) {
|
|
// Search by id
|
|
if (lint.elem.id.indexOf(searchStr) !== -1) {
|
|
return true;
|
|
}
|
|
// Search the description
|
|
// The use of `for`-loops instead of `foreach` enables us to return early
|
|
const docsLowerCase = lint.elem.textContent.toLowerCase();
|
|
for (const term of terms) {
|
|
// This is more likely and will therefore be checked first
|
|
if (docsLowerCase.indexOf(term) !== -1) {
|
|
return true;
|
|
}
|
|
|
|
if (lint.elem.id.indexOf(term) !== -1) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
searchState.clearInputTimeout();
|
|
|
|
let searchStr = searchState.inputElem.value.trim().toLowerCase();
|
|
if (searchStr.startsWith("clippy::")) {
|
|
searchStr = searchStr.slice(8);
|
|
}
|
|
if (searchState.lastSearch === searchStr) {
|
|
return;
|
|
}
|
|
searchState.lastSearch = searchStr;
|
|
const terms = searchStr.split(" ");
|
|
const cleanedSearchStr = searchStr.replaceAll("-", "_");
|
|
|
|
for (const lint of filters.getAllLints()) {
|
|
lint.searchFilteredOut = !matchesSearch(lint, terms, cleanedSearchStr);
|
|
if (lint.filteredOut) {
|
|
continue;
|
|
}
|
|
if (lint.searchFilteredOut) {
|
|
lint.elem.style.display = "none";
|
|
} else {
|
|
lint.elem.style.display = "";
|
|
}
|
|
}
|
|
if (searchStr.length > 0) {
|
|
window.location.hash = `/${searchStr}`;
|
|
} else {
|
|
window.location.hash = '';
|
|
}
|
|
},
|
|
};
|
|
|
|
function handleInputChanged(event) {
|
|
if (event.target !== document.activeElement) {
|
|
return;
|
|
}
|
|
searchState.resetInputTimeout();
|
|
}
|
|
|
|
function handleShortcut(ev) {
|
|
if (ev.ctrlKey || ev.altKey || ev.metaKey || disableShortcuts) {
|
|
return;
|
|
}
|
|
|
|
if (document.activeElement.tagName === "INPUT") {
|
|
if (ev.key === "Escape") {
|
|
document.activeElement.blur();
|
|
}
|
|
} else {
|
|
switch (ev.key) {
|
|
case "s":
|
|
case "S":
|
|
case "/":
|
|
ev.preventDefault(); // To prevent the key to be put into the input.
|
|
document.getElementById("search-input").focus();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function toggleElements(filter, value) {
|
|
let needsUpdate = false;
|
|
let count = 0;
|
|
|
|
const element = document.getElementById(filters[filter].id);
|
|
onEachLazy(
|
|
element.querySelectorAll("ul input"),
|
|
el => {
|
|
if (el.checked !== value) {
|
|
el.checked = value;
|
|
filters[filter][el.getAttribute("data-value")] = value;
|
|
needsUpdate = true;
|
|
}
|
|
count += 1;
|
|
}
|
|
);
|
|
element.querySelector(".badge").innerText = value ? count : 0;
|
|
if (needsUpdate) {
|
|
filters.filterLints();
|
|
}
|
|
}
|
|
|
|
function changeSetting(elem) {
|
|
if (elem.id === "disable-shortcuts") {
|
|
disableShortcuts = elem.checked;
|
|
storeValue(elem.id, elem.checked);
|
|
}
|
|
}
|
|
|
|
function onEachLazy(lazyArray, func) {
|
|
const arr = Array.prototype.slice.call(lazyArray);
|
|
for (const el of arr) {
|
|
func(el);
|
|
}
|
|
}
|
|
|
|
function highlightIfNeeded(lintId) {
|
|
onEachLazy(document.querySelectorAll(`#${lintId} pre > code:not(.hljs)`), el => {
|
|
hljs.highlightElement(el.parentElement)
|
|
el.classList.add("highlighted");
|
|
});
|
|
}
|
|
|
|
function expandLint(lintId) {
|
|
const elem = document.querySelector(`#${lintId} > input[type="checkbox"]`);
|
|
elem.checked = true;
|
|
highlightIfNeeded(lintId);
|
|
}
|
|
|
|
function copyToClipboard(event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const clipboard = event.target;
|
|
|
|
let resetClipboardTimeout = null;
|
|
const resetClipboardIcon = clipboard.innerHTML;
|
|
|
|
function resetClipboard() {
|
|
resetClipboardTimeout = null;
|
|
clipboard.innerHTML = resetClipboardIcon;
|
|
}
|
|
|
|
navigator.clipboard.writeText("clippy::" + clipboard.parentElement.id.slice(5));
|
|
|
|
clipboard.innerHTML = "✓";
|
|
if (resetClipboardTimeout !== null) {
|
|
clearTimeout(resetClipboardTimeout);
|
|
}
|
|
resetClipboardTimeout = setTimeout(resetClipboard, 1000);
|
|
}
|
|
|
|
function handleBlur(event, elementId) {
|
|
const parent = document.getElementById(elementId);
|
|
if (!parent.contains(document.activeElement) &&
|
|
!parent.contains(event.relatedTarget)
|
|
) {
|
|
parent.classList.remove("open");
|
|
}
|
|
}
|
|
|
|
function toggleExpansion(expand) {
|
|
onEachLazy(
|
|
document.querySelectorAll("article"),
|
|
expand ? el => {
|
|
el.classList.remove("collapsed");
|
|
highlightIfNeeded(el);
|
|
} : el => el.classList.add("collapsed"),
|
|
);
|
|
}
|
|
|
|
// Returns the current URL without any query parameter or hash.
|
|
function getNakedUrl() {
|
|
return window.location.href.split("?")[0].split("#")[0];
|
|
}
|
|
|
|
const GROUPS_FILTER_DEFAULT = {
|
|
cargo: true,
|
|
complexity: true,
|
|
correctness: true,
|
|
nursery: true,
|
|
pedantic: true,
|
|
perf: true,
|
|
restriction: true,
|
|
style: true,
|
|
suspicious: true,
|
|
deprecated: false,
|
|
};
|
|
const LEVEL_FILTERS_DEFAULT = {
|
|
allow: true,
|
|
warn: true,
|
|
deny: true,
|
|
none: true,
|
|
};
|
|
const APPLICABILITIES_FILTER_DEFAULT = {
|
|
Unspecified: true,
|
|
MachineApplicable: true,
|
|
MaybeIncorrect: true,
|
|
HasPlaceholders: true,
|
|
};
|
|
const URL_PARAMS_CORRESPONDANCE = {
|
|
"groups_filter": "groups",
|
|
"levels_filter": "levels",
|
|
"applicabilities_filter": "applicabilities",
|
|
"version_filter": "versions",
|
|
};
|
|
const VERSIONS_CORRESPONDANCE = {
|
|
"lte": "≤",
|
|
"gte": "≥",
|
|
"eq": "=",
|
|
};
|
|
|
|
window.filters = {
|
|
groups_filter: { id: "lint-groups", ...GROUPS_FILTER_DEFAULT },
|
|
levels_filter: { id: "lint-levels", ...LEVEL_FILTERS_DEFAULT },
|
|
applicabilities_filter: { id: "lint-applicabilities", ...APPLICABILITIES_FILTER_DEFAULT },
|
|
version_filter: {
|
|
"≥": null,
|
|
"≤": null,
|
|
"=": null,
|
|
},
|
|
allLints: null,
|
|
getAllLints: () => {
|
|
if (filters.allLints === null) {
|
|
filters.allLints = Array.prototype.slice.call(
|
|
document.getElementsByTagName("article"),
|
|
).map(elem => {
|
|
let version = elem.querySelector(".label-version").innerText;
|
|
// Strip the "pre " prefix for pre 1.29.0 lints
|
|
if (version.startsWith("pre ")) {
|
|
version = version.slice(4);
|
|
}
|
|
return {
|
|
elem: elem,
|
|
group: elem.querySelector(".label-lint-group").innerText,
|
|
level: elem.querySelector(".label-lint-level").innerText,
|
|
version: parseInt(version.split(".")[1]),
|
|
applicability: elem.querySelector(".label-applicability").innerText,
|
|
filteredOut: false,
|
|
searchFilteredOut: false,
|
|
};
|
|
});
|
|
}
|
|
return filters.allLints;
|
|
},
|
|
regenerateURLparams: () => {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
function compareObjects(obj1, obj2) {
|
|
return (JSON.stringify(obj1) === JSON.stringify({ id: obj1.id, ...obj2 }));
|
|
}
|
|
function updateIfNeeded(filterName, obj2) {
|
|
const obj1 = filters[filterName];
|
|
const name = URL_PARAMS_CORRESPONDANCE[filterName];
|
|
if (!compareObjects(obj1, obj2)) {
|
|
urlParams.set(
|
|
name,
|
|
Object.entries(obj1).filter(
|
|
([key, value]) => value && key !== "id"
|
|
).map(
|
|
([key, _]) => key
|
|
).join(","),
|
|
);
|
|
} else {
|
|
urlParams.delete(name);
|
|
}
|
|
}
|
|
|
|
updateIfNeeded("groups_filter", GROUPS_FILTER_DEFAULT);
|
|
updateIfNeeded("levels_filter", LEVEL_FILTERS_DEFAULT);
|
|
updateIfNeeded(
|
|
"applicabilities_filter", APPLICABILITIES_FILTER_DEFAULT);
|
|
|
|
const versions = [];
|
|
if (filters.version_filter["="] !== null) {
|
|
versions.push(`eq:${filters.version_filter["="]}`);
|
|
}
|
|
if (filters.version_filter["≥"] !== null) {
|
|
versions.push(`gte:${filters.version_filter["≥"]}`);
|
|
}
|
|
if (filters.version_filter["≤"] !== null) {
|
|
versions.push(`lte:${filters.version_filter["≤"]}`);
|
|
}
|
|
if (versions.length !== 0) {
|
|
urlParams.set(URL_PARAMS_CORRESPONDANCE["version_filter"], versions.join(","));
|
|
} else {
|
|
urlParams.delete(URL_PARAMS_CORRESPONDANCE["version_filter"]);
|
|
}
|
|
|
|
let params = urlParams.toString();
|
|
if (params.length !== 0) {
|
|
params = `?${params}`;
|
|
}
|
|
|
|
const url = getNakedUrl() + params + window.location.hash
|
|
if (!history.state) {
|
|
history.pushState(null, "", url);
|
|
} else {
|
|
history.replaceState(null, "", url);
|
|
}
|
|
},
|
|
filterLints: () => {
|
|
// First we regenerate the URL parameters.
|
|
filters.regenerateURLparams();
|
|
for (const lint of filters.getAllLints()) {
|
|
lint.filteredOut = (!filters.groups_filter[lint.group]
|
|
|| !filters.levels_filter[lint.level]
|
|
|| !filters.applicabilities_filter[lint.applicability]
|
|
|| !(filters.version_filter["="] === null || lint.version === filters.version_filter["="])
|
|
|| !(filters.version_filter["≥"] === null || lint.version > filters.version_filter["≥"])
|
|
|| !(filters.version_filter["≤"] === null || lint.version < filters.version_filter["≤"])
|
|
);
|
|
if (lint.filteredOut || lint.searchFilteredOut) {
|
|
lint.elem.style.display = "none";
|
|
} else {
|
|
lint.elem.style.display = "";
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
function updateFilter(elem, filter, skipLintsFiltering) {
|
|
const value = elem.getAttribute("data-value");
|
|
if (filters[filter][value] !== elem.checked) {
|
|
filters[filter][value] = elem.checked;
|
|
const counter = document.querySelector(`#${filters[filter].id} .badge`);
|
|
counter.innerText = parseInt(counter.innerText) + (elem.checked ? 1 : -1);
|
|
if (!skipLintsFiltering) {
|
|
filters.filterLints();
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateVersionFilters(elem, skipLintsFiltering) {
|
|
let value = elem.value.trim();
|
|
if (value.length === 0) {
|
|
value = null;
|
|
} else if (/^\d+$/.test(value)) {
|
|
value = parseInt(value);
|
|
} else {
|
|
console.error(`Failed to get version number from "${value}"`);
|
|
return;
|
|
}
|
|
|
|
const counter = document.querySelector("#version-filter .badge");
|
|
let count = 0;
|
|
onEachLazy(document.querySelectorAll("#version-filter input"), el => {
|
|
if (el.value.trim().length !== 0) {
|
|
count += 1;
|
|
}
|
|
});
|
|
counter.innerText = count;
|
|
|
|
const comparisonKind = elem.getAttribute("data-value");
|
|
if (filters.version_filter[comparisonKind] !== value) {
|
|
filters.version_filter[comparisonKind] = value;
|
|
if (!skipLintsFiltering) {
|
|
filters.filterLints();
|
|
}
|
|
}
|
|
}
|
|
|
|
function clearVersionFilters() {
|
|
let needsUpdate = false;
|
|
|
|
onEachLazy(document.querySelectorAll("#version-filter input"), el => {
|
|
el.value = "";
|
|
const comparisonKind = el.getAttribute("data-value");
|
|
if (filters.version_filter[comparisonKind] !== null) {
|
|
needsUpdate = true;
|
|
filters.version_filter[comparisonKind] = null;
|
|
}
|
|
});
|
|
document.querySelector("#version-filter .badge").innerText = 0;
|
|
if (needsUpdate) {
|
|
filters.filterLints();
|
|
}
|
|
}
|
|
|
|
function resetGroupsToDefault() {
|
|
let needsUpdate = false;
|
|
let count = 0;
|
|
|
|
onEachLazy(document.querySelectorAll("#lint-groups-selector input"), el => {
|
|
const key = el.getAttribute("data-value");
|
|
const value = GROUPS_FILTER_DEFAULT[key];
|
|
if (filters.groups_filter[key] !== value) {
|
|
filters.groups_filter[key] = value;
|
|
el.checked = value;
|
|
needsUpdate = true;
|
|
}
|
|
if (value) {
|
|
count += 1;
|
|
}
|
|
});
|
|
document.querySelector("#lint-groups .badge").innerText = count;
|
|
if (needsUpdate) {
|
|
filters.filterLints();
|
|
}
|
|
}
|
|
|
|
function generateListOfOptions(list, elementId, filter) {
|
|
let html = '';
|
|
let nbEnabled = 0;
|
|
for (const [key, value] of Object.entries(list)) {
|
|
const attr = value ? " checked" : "";
|
|
html += `\
|
|
<li class="checkbox">\
|
|
<label class="text-capitalize">\
|
|
<input type="checkbox" data-value="${key}" \
|
|
onchange="updateFilter(this, '${filter}')"${attr}/>${key}\
|
|
</label>\
|
|
</li>`;
|
|
if (value) {
|
|
nbEnabled += 1;
|
|
}
|
|
}
|
|
|
|
const elem = document.getElementById(`${elementId}-selector`);
|
|
elem.previousElementSibling.querySelector(".badge").innerText = `${nbEnabled}`;
|
|
elem.innerHTML += html;
|
|
|
|
setupDropdown(elementId);
|
|
}
|
|
|
|
function setupDropdown(elementId) {
|
|
const elem = document.getElementById(elementId);
|
|
const button = document.querySelector(`#${elementId} > button`);
|
|
button.onclick = () => elem.classList.toggle("open");
|
|
|
|
const setBlur = child => {
|
|
child.onblur = event => handleBlur(event, elementId);
|
|
};
|
|
onEachLazy(elem.children, setBlur);
|
|
onEachLazy(elem.querySelectorAll("select"), setBlur);
|
|
onEachLazy(elem.querySelectorAll("input"), setBlur);
|
|
onEachLazy(elem.querySelectorAll("ul button"), setBlur);
|
|
}
|
|
|
|
function generateSettings() {
|
|
setupDropdown("settings-dropdown");
|
|
|
|
generateListOfOptions(LEVEL_FILTERS_DEFAULT, "lint-levels", "levels_filter");
|
|
generateListOfOptions(GROUPS_FILTER_DEFAULT, "lint-groups", "groups_filter");
|
|
generateListOfOptions(
|
|
APPLICABILITIES_FILTER_DEFAULT, "lint-applicabilities", "applicabilities_filter");
|
|
|
|
let html = '';
|
|
for (const kind of ["≥", "≤", "="]) {
|
|
html += `\
|
|
<li class="checkbox">\
|
|
<label>${kind}</label>\
|
|
<span>1.</span> \
|
|
<input type="number" \
|
|
min="29" \
|
|
class="version-filter-input form-control filter-input" \
|
|
maxlength="2" \
|
|
data-value="${kind}" \
|
|
onchange="updateVersionFilters(this)" \
|
|
oninput="updateVersionFilters(this)" \
|
|
onkeydown="updateVersionFilters(this)" \
|
|
onkeyup="updateVersionFilters(this)" \
|
|
onpaste="updateVersionFilters(this)" \
|
|
/>
|
|
<span>.0</span>\
|
|
</li>`;
|
|
}
|
|
document.getElementById("version-filter-selector").innerHTML += html;
|
|
setupDropdown("version-filter");
|
|
}
|
|
|
|
function generateSearch() {
|
|
searchState.inputElem.addEventListener("change", handleInputChanged);
|
|
searchState.inputElem.addEventListener("input", handleInputChanged);
|
|
searchState.inputElem.addEventListener("keydown", handleInputChanged);
|
|
searchState.inputElem.addEventListener("keyup", handleInputChanged);
|
|
searchState.inputElem.addEventListener("paste", handleInputChanged);
|
|
}
|
|
|
|
function scrollToLint(lintId) {
|
|
const target = document.getElementById(lintId);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
target.scrollIntoView();
|
|
expandLint(lintId);
|
|
}
|
|
|
|
// If the page we arrive on has link to a given lint, we scroll to it.
|
|
function scrollToLintByURL() {
|
|
const lintId = window.location.hash.substring(1);
|
|
if (lintId.length > 0) {
|
|
scrollToLint(lintId);
|
|
}
|
|
}
|
|
|
|
function parseURLFilters() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
for (const [key, value] of urlParams.entries()) {
|
|
for (const [corres_key, corres_value] of Object.entries(URL_PARAMS_CORRESPONDANCE)) {
|
|
if (corres_value === key) {
|
|
if (key !== "versions") {
|
|
const settings = new Set(value.split(","));
|
|
onEachLazy(document.querySelectorAll(`#lint-${key} ul input`), elem => {
|
|
elem.checked = settings.has(elem.getAttribute("data-value"));
|
|
updateFilter(elem, corres_key, true);
|
|
});
|
|
} else {
|
|
const settings = value.split(",").map(elem => elem.split(":"));
|
|
|
|
for (const [kind, value] of settings) {
|
|
const elem = document.querySelector(
|
|
`#version-filter input[data-value="${VERSIONS_CORRESPONDANCE[kind]}"]`);
|
|
elem.value = value;
|
|
updateVersionFilters(elem, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
document.getElementById(`theme-choice`).value = loadValue("theme");
|
|
let disableShortcuts = loadValue('disable-shortcuts') === "true";
|
|
document.getElementById("disable-shortcuts").checked = disableShortcuts;
|
|
|
|
document.addEventListener("keypress", handleShortcut);
|
|
document.addEventListener("keydown", handleShortcut);
|
|
|
|
generateSettings();
|
|
generateSearch();
|
|
parseURLFilters();
|
|
scrollToLintByURL();
|
|
filters.filterLints();
|