Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/index.njk
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ The base integer <strong>I</strong> extension is the most widely implemented set
{% endfor %}
</ul>

<p class="secondary-callout">
<a href="/search/">Try the searchable instruction index.</a>
</p>

<style>
body { font-family: system-ui, sans-serif; line-height: 1.5; margin: 2rem auto; max-width: 900px; padding: 0 1rem; }
ul { list-style: none; padding: 0; }
Expand All @@ -34,4 +38,5 @@ a { text-decoration: none; color: #0366d6; }
a:hover { text-decoration: underline; }
.primary-callout { background: #f1f8ff; border-left: 4px solid #0366d6; padding: 12px; margin: 1rem 0; }
.extension-description { margin: 4px 0 0; color: #586069; font-size: 0.9rem; }
.secondary-callout { margin-top: 2rem; }
</style>
139 changes: 139 additions & 0 deletions src/instruction-search.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
title: Instruction Search
permalink: search/index.html
---
<h1>Instruction Search</h1>

<p>Search by mnemonic or long name. Hit Enter on an exact mnemonic match to jump straight to the instruction details.</p>

<input id="search" placeholder="Search mnemonic, e.g. addi" style="padding:8px; width: 100%; max-width: 480px;" />

{# Creates box divide between checkboxes and rest of page #}
<fieldset id="extension-filters-fieldset" class="filters">
<legend>Filter by extension</legend>
<div id="extension-filters" class="filter-options"></div>
</fieldset>

<ul id="instruction_list">
{% for inst in instructions %}
<li
data-search="{{ (inst.name ~ ' ' ~ inst.longName) | lower }}"
data-mnemonic="{{ inst.name | lower }}"
data-extension="{{ inst.extension }}"
data-extension-slug="{{ inst.extensionSlug }}"
>
<a href="/instructions/{{ inst.name }}/">{{ inst.name }}</a>
<span class="ext-name" data-extension-slug="{{ inst.extensionSlug }}" title="Extension">
- {{ inst.extension }}
</span>
</li>
{% endfor %}
</ul>

<script>
const input = document.getElementById('search');
const instruction_list = document.getElementById('instruction_list');
const filtersFieldset = document.getElementById('extension-filters-fieldset');
{# the blank area where we’ll insert checkboxes #}
const filtersContainer = document.getElementById('extension-filters');
const items = Array.from(instruction_list.querySelectorAll('li'));
const activeExtensions = new Set();

// Gather unique extension names from the rendered instruction <li> items.
const extensionSet = new Set();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't really understand why we are using a Set here. It was previously a Map. Can we just construct an array and then sort it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No because this is essentially creating a list of extensions based on the list of instructions. Since a lot of the instructions have the same extension this automatically gets rid of the duplicate extensions. If I did an array I would have to take out the duplicates.

for (const li of items) {
const extension = li.dataset.extension;
extensionSet.add(extension);
}

const extensions = Array.from(extensionSet).sort();
for (const extension of extensions) {
const id = `ext-${extension}`;
const label = document.createElement('label');
label.className = 'filter-option';

const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = extension;
checkbox.id = id;
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
activeExtensions.add(extension);
} else {
activeExtensions.delete(extension);
}
applyFilters();
});

const text = document.createElement('span');
text.textContent = extension;

label.appendChild(checkbox);
label.appendChild(text);
filtersContainer.appendChild(label);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, it looks like the strategy here is to populate the list of extension in the client, rather than when generating the HTML. Rather like the situation where we were generating SVG in the browser before, I think this could be a lot simpler if we simply generate the HTML for the checkboxes up front. This would avoid all the createElement and stuff, and it could be much easier to read.

I strongly suggest that we leave that change to a future PR as well! In the interest of getting this one merged, so the next step will be clear.

}


function applyFilters() {
const q = input.value.trim().toLowerCase();
const filtersActive = activeExtensions.size > 0;
for (const li of items) {

{# Check if the item matches the text search
- If the search box is empty, match everything
- Otherwise, match if the search string includes the query #}
const matchesQuery = !q || (li.dataset.search || '').includes(q);

{# Check if the item matches an active extension filter
- If no filters are active, match everything
- Otherwise, show only if its extension is selected #}
const matchesExtension = !filtersActive || activeExtensions.has(li.dataset.extension);

{# Show or hide this <li> depending on whether both match conditions are true #}
li.style.display = (matchesQuery && matchesExtension) ? '' : 'none';
}
}

input.addEventListener('input', applyFilters);

input.addEventListener('keydown', (e) => {
{# Only act if Enter is pressed #}
if (e.key !== 'Enter') return;

const q = input.value.trim().toLowerCase();

{# Do nothing if the box is empty #}
if (!q) return;

const filtersActive = activeExtensions.size > 0;

{# Look for an exact mnemonic match among all instructions
- If filters are active, also ensure it belongs to one of the selected extensions #}
const exact = items.find(li => {
if (li.dataset.mnemonic !== q) return false;
if (!filtersActive) return true;
return activeExtensions.has(li.dataset.extension);
});

{# If an exact match is found, redirect to that instruction's detail page #}
if (exact) {
const link = exact.querySelector('a').getAttribute('href');
location.href = link;
}
});

applyFilters();
</script>

<style>
body { font-family: system-ui, sans-serif; line-height: 1.5; margin: 2rem auto; max-width: 900px; padding: 0 1rem; }
ul { instruction_list-style: none; padding: 0; }
li { margin: 6px 0; }
a { text-decoration: none; color: #0366d6; }
a:hover { text-decoration: underline; }
#extension-filters-fieldset { margin: 1rem 0; padding: 0.75rem 1rem; border: 1px solid #d0d7de; border-radius: 6px; }
#extension-filters-fieldset legend { font-weight: 600; font-size: 0.95rem; padding: 0 6px; }
.filter-options { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; margin-top: 0.5rem; align-items: center; }
.filter-option { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.95rem; white-space: nowrap; }
.filter-option input { width: 16px; height: 16px; }
</style>
10 changes: 10 additions & 0 deletions src/instructions/instruction.njk
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ eleventyComputed:

</section>

<section>
<h2>RISC-V Instruction Encoder/Decoder</h2>
<p>
<a href="https://luplab.gitlab.io/rvcodecjs/#q={{ inst.name | urlencode }}&abi=false&isa=AUTO"
target="_blank" rel="noopener">
open {{ inst.name }} in rvcodecjs
</a>
</p>
</section>

<section>
<h2>Availability</h2>
<ul>
Expand Down