From 6dbc6f0c0fe760b07d1dd691f8ed34c76f878835 Mon Sep 17 00:00:00 2001 From: Polina Boneva
([^<]+)
',
@@ -303,6 +253,9 @@ def linkify_type(string):
lambda p: '{}'.format(linkify_type(p.group(1))), string)
return string
+def link_to_decl(decl_name, loc_map):
+ return f'{SITE_ROOT}{loc_map[decl_name].url}#{decl_name}'
+
def plaintext_summary(markdown, max_chars = 200):
# collapse lines
text = re.compile(r'([a-zA-Z`(),;\$\-]) *\n *([a-zA-Z`()\$])').sub(r'\1 \2', markdown)
@@ -320,7 +273,8 @@ def plaintext_summary(markdown, max_chars = 200):
]
remove_patterns = [
r'^\s{0,3}>\s?', r'^={2,}', r'`{3}.*$', r'~~', r'^[=\-]{2,}\s*$',
- r'^-{3,}\s*$', r'^\s*']
+ r'^-{3,}\s*$', r'^\s*'
+ ]
text = reduce(lambda text, p: re.compile(p, re.MULTILINE).sub(r'\1', text), remove_keep_contents_patterns, text)
text = reduce(lambda text, p: re.compile(p, re.MULTILINE).sub('', text), remove_patterns, text)
@@ -329,51 +283,67 @@ def plaintext_summary(markdown, max_chars = 200):
text = re.compile(r'\s*\.?\n').sub('. ', text)
return textwrap.shorten(text, width = max_chars, placeholder="…")
+
+def htmlify_name(n):
+ return '.'.join([f'{ html.escape(part) }' for part in n.split('.')])
-def link_to_decl(decl_name, loc_map):
- return f'{site_root}{loc_map[decl_name].url}#{decl_name}'
+def library_link(filename: ImportName, line=None):
+ mathlib_github_src_root = "{0}/blob/{1}/src/".format(MATHLIB_GITHUB_ROOT, MATHLIB_COMMIT)
+ lean_root = 'https://github.com/leanprover-community/lean/blob/{}/library/'.format(LEAN_COMMIT)
-def kind_of_decl(decl):
- kind = 'structure' if len(decl['structure_fields']) > 0 else 'inductive' if len(decl['constructors']) > 0 else decl['kind']
- if kind == 'thm': kind = 'theorem'
- elif kind == 'cnst': kind = 'constant'
- elif kind == 'ax': kind = 'axiom'
- return kind
-env.globals['kind_of_decl'] = kind_of_decl
+ # TODO: allow extending this for third-party projects
+ library_link_roots = {
+ 'core': lean_root,
+ 'mathlib': mathlib_github_src_root,
+ }
-def htmlify_name(n):
- return '.'.join([f'{ html.escape(part) }' for part in n.split('.')])
-env.filters['htmlify_name'] = htmlify_name
+ try:
+ root = library_link_roots[filename.project]
+ except KeyError:
+ return "" # empty string is handled as a self-link
-# returns (pagetitle, intro_block), [(tactic_name, tactic_block)]
-def split_tactic_list(markdown):
- entries = re.findall(r'(?<=# )(.*)([\s\S]*?)(?=(##|\Z))', markdown)
- return entries[0], entries[1:]
+ root += '/'.join(filename.parts) + '.lean'
+ if line is not None:
+ root += f'#L{line}'
+ return root
-def import_options(loc_map, decl_name, import_string):
- direct_import_paths = []
- if decl_name in loc_map:
- direct_import_paths.append(loc_map[decl_name].name)
- if import_string != '' and import_string not in direct_import_paths:
- direct_import_paths.append(import_string)
- if any(i.startswith('init.') for i in direct_import_paths):
- return 'No declarations or comments match your search.
'; + +const createResultsHTML = (results) => { + let html = `Found ${results.length} matches, showing ${maxCountResults > results.length ? results.length : maxCountResults}.
`; + html += results.map((result, index) => { + return createSingleResultHTML(result, index); + }).join(''); + return html; +} + +const createSingleResultHTML = (result, i) => { + const { module, name, description, match, terms } = result; + const resultUrl = `${siteRoot}${module}#${name}`; + const descriptionDisplay = description && description.length > 0 ? `${description.slice(0, 150)}..` : '' + + const html = ``; + + return html; +} + // 404 page goodies // ---------------- - const suggestionsElmnt = document.getElementById('howabout'); if (suggestionsElmnt) { suggestionsElmnt.innerText = "Please wait a second. I'll try to help you."; @@ -187,7 +233,7 @@ if (suggestionsElmnt) { .innerText = window.location.href.replace(/[/]/g, '/\u200b'); const query = window.location.href.match(/[/]([^/]+)(?:\.html|[/])?$/)[1]; - declSearch(query).then((results) => { + searchIndexedData(query).then((results) => { suggestionsElmnt.innerText = 'How about one of these instead:'; const ul = suggestionsElmnt.appendChild(document.createElement('ul')); for (const { decl } of results) { diff --git a/searchWorker.js b/searchWorker.js index 6fadb54..9272981 100644 --- a/searchWorker.js +++ b/searchWorker.js @@ -1,27 +1,66 @@ +// Access indexed data structure to be used for searching through the documentation const req = new XMLHttpRequest(); -req.open('GET', 'decl.bmp', false /* blocking */); -req.responseType = 'text'; -req.send(); - -const declNames = req.responseText.split('\n'); -// Adapted from the default tokenizer and -// https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5Cp%7BZ%7D&abb=on&c=on&esc=on&g=&i= -const SEPARATOR = /[._\n\r \u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]+/u +req.open('GET', 'searchable_data.json', false /* blocking */); +req.responseType = 'json'; +req.send(); importScripts('https://cdn.jsdelivr.net/npm/minisearch@2.4.1/dist/umd/index.min.js'); + // Include options as per API specs: https://lucaong.github.io/minisearch/modules/_minisearch_.html const miniSearch = new MiniSearch({ - fields: ['decl'], - storeFields: ['decl'], - tokenize: text => text.split(SEPARATOR), + idField: 'name', + fields: ['module', 'name', 'description'], + storeFields: ['module', 'name', 'description', 'attributes', 'kind'] }); -miniSearch.addAll(declNames.map((decl, id) => ({decl, id}))); + +const indexedData = req.response; +miniSearch.addAll(indexedData); onconnect = ({ports: [port]}) => - port.onmessage = ({data}) => { - const results = miniSearch.search(data.q, { - prefix: (term) => term.length > 3, - fuzzy: (term) => term.length > 3 && 0.2, - }); - port.postMessage(results.slice(0, 20)); - }; +port.onmessage = ({ data }) => { + const { query, filters, maxCount } = data; + const results = miniSearch.search(query, { + boost: { module: 1, description: 2, name: 3 }, + combineWith: 'AND', + filter: (result) => filterItemResult(result, filters), + // prefix: (term) => term.length > 3, + // fuzzy: (term) => term.length > 3 && 0.2, + }); + + console.log(results); + const response = typeof maxCount === "number" ? results.slice(0, maxCount) : results; + port.postMessage(response); +}; + +const filterItemResult = (result, filters = {}) => { + const { attributes: attrFilter, kind: kindFilter } = filters; + const hasAttrFilter = attrFilter && attrFilter.length > 0; + const hasKindFilter = kindFilter && kindFilter.length > 0; + + if (!hasAttrFilter && !hasKindFilter) { + return true; + } + + const { attributes: attrRes, kind: kindRes } = result; + let isResultAttrIncluded = false; + let isResultKindIncluded = false; + + if (hasKindFilter) { + isResultKindIncluded = kindFilter.includes(kindRes); + } + + if (hasAttrFilter) { + for (let attribute of attrRes) { + if (attrFilter.includes(attribute)) { + isResultAttrIncluded = true; + break; + } + } + } + + return hasKindFilter && hasAttrFilter ? + (isResultAttrIncluded && isResultKindIncluded) : + hasAttrFilter ? + isResultAttrIncluded : + isResultKindIncluded; +} \ No newline at end of file diff --git a/style.css b/style.css index 78efc65..212b7cb 100644 --- a/style.css +++ b/style.css @@ -114,8 +114,7 @@ header { @media screen and (max-width: 700px) { header h1 span { display: none; } :root { --header-side-padding: 1ex; } - #search_form button { display: none; } - #search_form input { width: 100%; } + #search_form input { width: 60%; } header #search_results { left: 1ex; right: 1ex; @@ -145,63 +144,6 @@ header header_filename { } } -/* inserted by nav.js */ -#search_results { - position: absolute; - top: var(--header-height); - right: calc(var(--header-side-padding)); - width: calc(20em + 4em); - z-index: 1; - background: var(--header-bg); - border: 1px solid #aaa; - border-top: none; - overflow-x: hidden; - overflow-y: auto; - max-height: calc(100vh - var(--header-height)); -} - -#search_results:empty { - display: none; -} - -#search_results[state="loading"]:empty { - display: block; - cursor: progress; -} -#search_results[state="loading"]:empty::before { - display: block; - content: ' 🐙 🐙 🐙 🐙 🐙 🐙 🐙 '; - padding: 1ex; - animation: marquee 10s linear infinite; -} -@keyframes marquee { - 0% { transform: translate(100%, 0); } - 100% { transform: translate(-100%, 0); } -} - -#search_results[state="done"]:empty { - display: block; - text-align: center; - padding: 1ex; -} -#search_results[state="done"]:empty::before { - content: '(no results)'; - font-style: italic; -} - -#search_results a { - display: block; - color: inherit; - padding: 1ex; - border-left: 0.5ex solid transparent; - padding-left: 0.5ex; - cursor: pointer; -} -#search_results .selected { - background: white; - border-color: #f0a202; -} - main, nav { margin-top: calc(var(--header-height) + 1em); } @@ -560,3 +502,71 @@ a:hover { margin-top: 2em; margin-bottom: 2em; } + + + +/* Search interface in form for search */ +#search_results { + position: absolute; + top: var(--header-height); + right: calc(var(--header-side-padding)); + width: 35em; + z-index: 1; + background: var(--header-bg); + border: 1px solid #aaa; + border-top: none; + overflow-x: auto; + overflow-y: auto; + max-height: calc(95vh - var(--header-height)); +} + +#search_results:empty { + display: none; +} + +#search_results[state="loading"]:empty { + display: block; + cursor: progress; +} +#search_results[state="loading"]:empty::before { + display: block; + content: ' 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 🐙 '; + padding: 1ex; + animation: marquee 10s linear infinite; +} +@keyframes marquee { + 0% { transform: translate(100%, 0); } + 100% { transform: translate(-100%, 0); } +} + +#search_results[state="done"]:empty { + display: block; + text-align: center; + padding: 1ex; +} +#search_results[state="done"]:empty::before { + content: '(no results)'; + font-style: italic; +} + +#search_results a { + display: block; + color: inherit; + padding: 4px; + border-top: 1px dashed #70a0d0; + cursor: pointer; +} +#search_results a .result_module { + font-size: 0.7em; + margin-top: 1px; +} +#search_results a .result_comment { + font-size: 0.8em; + margin-top: -0.5em; + font-family: 'Open Sans', sans-serif; +} + +#search_results .selected { + background: white; + border-color: #f0a202; +} \ No newline at end of file diff --git a/templates/base.j2 b/templates/base.j2 index 129c625..f68be9d 100644 --- a/templates/base.j2 +++ b/templates/base.j2 @@ -28,10 +28,11 @@{% block doctitle %}{{ self.title() }}{% endblock %}
- From 5432001b36efef84e5f03337ef456f644c5b7caf Mon Sep 17 00:00:00 2001 From: Polina BonevaNo declarations or comments match your search.
'; + +const createResultsHTML = (results) => { + let html = `Found ${results.length} matches, showing ${maxCountResults > results.length ? results.length : maxCountResults}.
`; + html += results.map((result, index) => { + return createSingleResultHTML(result, index); + }).join(''); + return html; +} + +const createSingleResultHTML = (result, i) => { + const { module, name, description, match, terms } = result; + const resultUrl = `${siteRoot}${module}#${name}`; + const descriptionDisplay = description && description.length > 0 ? `${description.slice(0, 150)}..` : '' + + const html = ``; + + return html; +} + +/* Keyboard navigation through search input and results */ +searchQueryInput.addEventListener('keydown', (ev) => { + if (!searchQueryInput.value || searchQueryInput.value.length === 0) { + searchResultsContainer.innerHTML = ''; } else { switch (ev.key) { case 'Down': @@ -119,7 +183,7 @@ searchQuery.addEventListener('keydown', (ev) => { }); -searchResults.addEventListener('keydown', (ev) => { +searchResultsContainer.addEventListener('keydown', (ev) => { switch (ev.key) { case 'Down': case 'ArrowDown': @@ -152,74 +216,50 @@ function handleSearchCursorUpDown(isDownwards) { } function handleSearchItemSelected() { - // todo goto link made up of the siteRood+module+name + // todo goto link made up of the siteRoot+module+name const selectedResult = document.querySelector(`#${resultsElmntId} .selected`) selectedResult.click(); } -// Searching through the index with a specific query and filters -const searchWorkerURL = new URL(`${siteRoot}searchWorker.js`, window.location); -const worker = new SharedWorker(searchWorkerURL); -const searchIndexedData = (query) => new Promise((resolve, reject) => { - // todo remove when UI filters done - const filters = { - attributes: ['nolint'], - // kind: ['def'] - }; - worker.port.start(); - worker.port.onmessage = ({ data }) => resolve(data); - worker.port.onmessageerror = (e) => reject(e); - worker.port.postMessage({ query, maxCount: maxCountResults, filters }); -}); -const submitSearchFormHandler = async (ev) => { - ev.preventDefault(); - const query = searchQuery.value; +// Simple filtering by *attributes* and *kind* per declaration +// ------------------------- - if (!query && query.length <= 0) { - // todo not needed? - return; +/* Get all elements for filtering */ +const filtersToggleButton = document.getElementById('search_filtes_btn'); +const filtersContainer = document.getElementById('filters_container'); +const filtersForm = document.getElementById('filters_form'); +const closeFiltersBtn = document.getElementById('close_filters'); + +/* Handle opening/closing filters container */ +filtersToggleButton.addEventListener("click", function() { + const isOpen = filtersContainer.style.display !== 'none'; + + if (isOpen) { + filtersContainer.style.display = 'none'; + } else { + filtersContainer.style.display = 'block'; } +}); - searchResults.setAttribute('state', 'loading'); - await fillInSearchResultsContainer(query); - searchResults.setAttribute('state', 'done'); +/* Handle submit chosen filters */ +const submitFiltersFormHandler = async (ev) => { + ev.preventDefault(); + // todo set filter values to a filter{} and move on + attributesFilters = filtersForm.querySelectorAll('input[name=attributes]:checked').map(e => e.value); + kindFilters = filtersForm.querySelectorAll('input[name=kind]:checked').map(e => e.value); }; -searchForm.addEventListener('submit', submitSearchFormHandler); +filtersForm.addEventListener('submit', submitFiltersFormHandler); -const fillInSearchResultsContainer = async (query) => { - const results = await searchIndexedData(query); - results.sort((a, b) => (a && typeof a.score === "number" && b && typeof b.score === "number") ? (b.score - a.score) : 0); - searchResults.innerHTML = results.length < 1 ? createNoResultsHTML() : createResultsHTML(results); -} +/* Handle closing filters box */ +closeFiltersBtn.addEventListener("click", function() { + filtersContainer.style.display = 'none'; +}); -const createNoResultsHTML = () => 'No declarations or comments match your search.
'; -const createResultsHTML = (results) => { - let html = `Found ${results.length} matches, showing ${maxCountResults > results.length ? results.length : maxCountResults}.
`; - html += results.map((result, index) => { - return createSingleResultHTML(result, index); - }).join(''); - return html; -} -const createSingleResultHTML = (result, i) => { - const { module, name, description, match, terms } = result; - const resultUrl = `${siteRoot}${module}#${name}`; - const descriptionDisplay = description && description.length > 0 ? `${description.slice(0, 150)}..` : '' - - const html = ``; - return html; -} // 404 page goodies // ---------------- diff --git a/searchWorker.js b/searchWorker.js index 9272981..f5ee65c 100644 --- a/searchWorker.js +++ b/searchWorker.js @@ -28,7 +28,7 @@ port.onmessage = ({ data }) => { }); console.log(results); - const response = typeof maxCount === "number" ? results.slice(0, maxCount) : results; + const response = typeof maxCount === "number" && maxCount >= 0 ? results.slice(0, maxCount) : results; port.postMessage(response); }; diff --git a/style.css b/style.css index 212b7cb..192ab1c 100644 --- a/style.css +++ b/style.css @@ -504,20 +504,53 @@ a:hover { } +/* Form for filtering search through declarations */ +#filters_container { + display: none; + width: 20em; + height: 20em; + position: absolute; + right: calc(var(--header-side-padding)); + top: var(--header-height); + z-index: 0.9; + background: var(--header-bg); + border: 1px solid rgb(218, 216, 216); + border-top: none; + overflow-x: scroll; + overflow-y: hidden; +} -/* Search interface in form for search */ +#filters_container #filters_form { + display: flex; + flex-wrap: wrap; + justify-content: space-around; +} + +/* Style the close button (span) */ +#filters_container #close_filters { + cursor: pointer; + position: absolute; + top: 50%; + right: 0%; + padding: 12px 16px; + transform: translate(0%, -50%); + } + +#filters_container #close_filters:hover {background: #bbb;} + +/* Form for searching through declarations */ #search_results { position: absolute; top: var(--header-height); right: calc(var(--header-side-padding)); - width: 35em; + width: 20em; z-index: 1; background: var(--header-bg); border: 1px solid #aaa; border-top: none; - overflow-x: auto; - overflow-y: auto; - max-height: calc(95vh - var(--header-height)); + overflow-x: scroll; + overflow-y: scroll; + max-height: calc(60vh - var(--header-height)); } #search_results:empty { diff --git a/templates/base.j2 b/templates/base.j2 index f68be9d..07f81f2 100644 --- a/templates/base.j2 +++ b/templates/base.j2 @@ -31,8 +31,93 @@ From 4c78d9cb2152de6eb829d40d1f89911f01ff37f0 Mon Sep 17 00:00:00 2001 From: Polina BonevaNo declarations or comments match your search.
'; +const fillInSearchResultsContainer = async (query, showAll = false) => { + const resultsCount = showAll ? -1 : MAX_COUNT_RESULTS; + const {response: results, total} = await searchIndexedData(query, resultsCount); + results.sort((a, b) => (a && typeof a.score === "number" && b && typeof b.score === "number") ? (b.score - a.score) : 0); + + const searchResultsCloseBtn = 'x'; + searchResultsContainer.innerHTML = results.length < 1 ? createNoResultsHTML(searchResultsCloseBtn) : createResultsHTML(results, total, showAll, searchResultsCloseBtn); +} -const createResultsHTML = (results) => { - let html = `Found ${results.length} matches, showing ${maxCountResults > results.length ? results.length : maxCountResults}.
`; - html += results.map((result, index) => { - return createSingleResultHTML(result, index); +const createNoResultsHTML = (html) => `No declarations or comments match your search.
${html}`; + +const createResultsHTML = (results, total, showAll, html) => { + const descriptionMaxLength = showAll ? 350 : 80; + let resultHtml = `Found ${total} matches + ${!showAll + ? + `, showing ${MAX_COUNT_RESULTS > results.length ? results.length : MAX_COUNT_RESULTS}.
+ Show all` + : + '' + } + ${html}`; + resultHtml += results.map((result, index) => { + return createSingleResultHTML(result, descriptionMaxLength, index); }).join(''); - return html; + return resultHtml; } -const createSingleResultHTML = (result, i) => { +const createSingleResultHTML = (result, descriptionMaxLength, i) => { const { module, name, description, match, terms } = result; const resultUrl = `${siteRoot}${module}#${name}`; - const descriptionDisplay = description && description.length > 0 ? `${description.slice(0, 150)}..` : '' - + const descriptionDisplay = description && description.length > 0 + ? + `${description.slice(0, descriptionMaxLength)}${description.length > descriptionMaxLength ? '..' : ''}` + : + ''; + const html = `