feat: add wasm build targets, profile hooks, os_demo example, and restructure manual source

- Add `wasm`, `wasm-clean`, and `install-emscripten` phony targets to Makefile for WebAssembly cross-compilation
- Insert `WREN_PROFILE_ENTER`/`WREN_PROFILE_EXIT` macros in `wren_vm.h` and `wren_vm.c` to support optional runtime profiling via `WREN_PROFILE_ENABLED`
- Create `example/os_demo.wren` demonstrating Platform, Process, and conditional exit usage
- Update `example/regex_demo.wren` to import `Match` and exercise Match object properties, groups, and `matchAll`
- Remove static HTML manual pages (`base64.html`, `dns.html`, `json.html`) and replace with structured `manual_src/` directory containing Jinja2 templates, YAML metadata, and content pages
- Expand `README.md` with build targets table, manual building instructions, source layout, and guide for adding new module documentation
This commit is contained in:
2026-01-26 04:12:14 +00:00
parent 79ff93c9a2
commit 04e467e09b
143 changed files with 13333 additions and 15112 deletions
+215
View File
@@ -0,0 +1,215 @@
/* retoor <retoor@molodetz.nl> */
.playground-container {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 100%;
}
.playground-toolbar {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
padding: 0.75rem;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px 4px 0 0;
}
.playground-toolbar button {
padding: 0.5rem 1rem;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
font-family: inherit;
font-size: 0.875rem;
transition: all 0.15s ease;
}
.playground-toolbar button:hover:not(:disabled) {
background: #e9e9e9;
}
.playground-toolbar button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.playground-toolbar button.primary {
background: #4a90d9;
color: white;
border-color: #3a7bc8;
}
.playground-toolbar button.primary:hover:not(:disabled) {
background: #3a7bc8;
}
.playground-toolbar select {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
font-family: inherit;
font-size: 0.875rem;
min-width: 150px;
}
.wasm-status {
margin-left: auto;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
}
.wasm-status.loading {
background: #fff3cd;
color: #856404;
}
.wasm-status.ready {
background: #d4edda;
color: #155724;
}
.wasm-status.error {
background: #f8d7da;
color: #721c24;
}
.playground-editor-wrapper {
position: relative;
}
#wren-editor {
width: 100%;
min-height: 300px;
padding: 1rem;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Consolas', monospace;
font-size: 14px;
line-height: 1.5;
border: 1px solid #ddd;
border-top: none;
border-radius: 0;
resize: vertical;
background: #fafafa;
color: #333;
tab-size: 2;
box-sizing: border-box;
}
#wren-editor:focus {
outline: none;
background: #fff;
border-color: #4a90d9;
}
.output-panel {
border: 1px solid #333;
border-radius: 0 0 4px 4px;
background: #1e1e1e;
overflow: hidden;
}
.output-header {
padding: 0.5rem 1rem;
background: #2d2d2d;
color: #ccc;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid #444;
}
#wren-output {
min-height: 150px;
max-height: 300px;
padding: 1rem;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Consolas', monospace;
font-size: 13px;
line-height: 1.5;
color: #d4d4d4;
overflow-y: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
#wren-output .output-output {
color: #d4d4d4;
}
#wren-output .output-error {
color: #f48771;
}
#wren-output .output-info {
color: #6a9955;
font-style: italic;
}
.playground-help {
margin-top: 0.5rem;
padding: 0.75rem 1rem;
background: #e8f4fd;
border: 1px solid #b6d4fe;
border-radius: 4px;
font-size: 0.875rem;
color: #084298;
}
.playground-help kbd {
display: inline-block;
padding: 0.15rem 0.4rem;
font-family: inherit;
font-size: 0.8rem;
background: #fff;
border: 1px solid #ccc;
border-radius: 3px;
box-shadow: 0 1px 1px rgba(0,0,0,0.1);
}
.modules-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.5rem;
margin-top: 1rem;
}
.module-badge {
display: inline-block;
padding: 0.4rem 0.75rem;
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 4px;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Consolas', monospace;
font-size: 0.875rem;
color: #555;
text-align: center;
}
@media (max-width: 768px) {
.playground-toolbar {
flex-direction: column;
align-items: stretch;
}
.wasm-status {
margin-left: 0;
text-align: center;
}
#wren-editor {
min-height: 200px;
font-size: 13px;
}
#wren-output {
min-height: 100px;
font-size: 12px;
}
}
File diff suppressed because it is too large Load Diff
+195
View File
@@ -0,0 +1,195 @@
// retoor <retoor@molodetz.nl>
(function() {
'use strict';
function initSidebar() {
var toggle = document.querySelector('.mobile-menu-toggle');
var sidebar = document.querySelector('.sidebar');
var overlay = document.createElement('div');
overlay.className = 'sidebar-overlay';
document.body.appendChild(overlay);
if (toggle) {
toggle.addEventListener('click', function() {
var isOpen = sidebar.classList.toggle('open');
overlay.classList.toggle('visible');
toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
}
overlay.addEventListener('click', function() {
sidebar.classList.remove('open');
overlay.classList.remove('visible');
if (toggle) {
toggle.setAttribute('aria-expanded', 'false');
}
});
var currentPath = window.location.pathname;
var links = document.querySelectorAll('.sidebar-nav a');
links.forEach(function(link) {
var href = link.getAttribute('href');
if (href && currentPath.endsWith(href.replace(/^\.\.\//, '').replace(/^\.\//, ''))) {
link.classList.add('active');
}
});
}
function initCopyButtons() {
var codeBlocks = document.querySelectorAll('pre');
codeBlocks.forEach(function(pre) {
var wrapper = document.createElement('div');
wrapper.className = 'code-block';
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
var btn = document.createElement('button');
btn.className = 'copy-btn';
btn.textContent = 'Copy';
wrapper.appendChild(btn);
btn.addEventListener('click', function() {
var code = pre.querySelector('code');
var text = code ? code.textContent : pre.textContent;
navigator.clipboard.writeText(text).then(function() {
btn.textContent = 'Copied!';
btn.classList.add('copied');
setTimeout(function() {
btn.textContent = 'Copy';
btn.classList.remove('copied');
}, 2000);
}).catch(function() {
btn.textContent = 'Failed';
setTimeout(function() {
btn.textContent = 'Copy';
}, 2000);
});
});
});
}
function initSmoothScroll() {
document.querySelectorAll('a[href^="#"]').forEach(function(anchor) {
anchor.addEventListener('click', function(e) {
var targetId = this.getAttribute('href').slice(1);
var target = document.getElementById(targetId);
if (target) {
e.preventDefault();
target.scrollIntoView({ behavior: 'smooth' });
history.pushState(null, null, '#' + targetId);
}
});
});
}
function initKeyboardNav() {
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
return;
}
if (!e.ctrlKey) {
return;
}
switch(e.key) {
case 'ArrowLeft':
e.preventDefault();
var firstSidebarLink = document.querySelector('.sidebar-nav a');
if (firstSidebarLink) firstSidebarLink.focus();
break;
case 'ArrowRight':
e.preventDefault();
var mainContent = document.getElementById('main-content');
if (mainContent) {
mainContent.setAttribute('tabindex', '-1');
mainContent.focus();
}
break;
case 'ArrowUp':
e.preventDefault();
var prevLink = document.querySelector('.page-footer a[href]:first-of-type');
if (prevLink && prevLink.textContent.includes('Previous')) {
window.location.href = prevLink.href;
}
break;
case 'ArrowDown':
e.preventDefault();
var nextLink = document.querySelector('.page-footer a[href]:last-of-type');
if (nextLink && nextLink.textContent.includes('Next')) {
window.location.href = nextLink.href;
}
break;
}
});
}
function highlightCode() {
var keywords = ['import', 'for', 'class', 'static', 'var', 'if', 'else', 'while',
'return', 'true', 'false', 'null', 'this', 'super', 'is', 'new',
'foreign', 'construct', 'break', 'continue', 'in'];
var keywordSet = {};
keywords.forEach(function(kw) { keywordSet[kw] = true; });
function escapeHtml(text) {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
document.querySelectorAll('pre code').forEach(function(block) {
if (block.classList.contains('highlighted')) return;
block.classList.add('highlighted');
var text = block.textContent;
var result = [];
var i = 0;
while (i < text.length) {
if (text[i] === '/' && text[i + 1] === '/') {
var end = text.indexOf('\n', i);
if (end === -1) end = text.length;
result.push('<span class="hl-comment">' + escapeHtml(text.slice(i, end)) + '</span>');
i = end;
continue;
}
if (text[i] === '"') {
var j = i + 1;
while (j < text.length && text[j] !== '"') {
if (text[j] === '\\') j++;
j++;
}
j++;
result.push('<span class="hl-string">' + escapeHtml(text.slice(i, j)) + '</span>');
i = j;
continue;
}
if (/[a-zA-Z_]/.test(text[i])) {
var start = i;
while (i < text.length && /[a-zA-Z0-9_]/.test(text[i])) i++;
var word = text.slice(start, i);
if (keywordSet[word]) {
result.push('<span class="hl-keyword">' + escapeHtml(word) + '</span>');
} else {
result.push(escapeHtml(word));
}
continue;
}
result.push(escapeHtml(text[i]));
i++;
}
block.innerHTML = result.join('');
});
}
document.addEventListener('DOMContentLoaded', function() {
initSidebar();
initCopyButtons();
initSmoothScroll();
highlightCode();
initKeyboardNav();
});
})();
+312
View File
@@ -0,0 +1,312 @@
// retoor <retoor@molodetz.nl>
class WrenPlayground {
constructor() {
this.vm = null;
this.isReady = false;
this.editor = null;
this.output = null;
this.runButton = null;
this.clearButton = null;
this.exampleSelect = null;
this.statusIndicator = null;
}
async init() {
this.editor = document.getElementById('wren-editor');
this.output = document.getElementById('wren-output');
this.runButton = document.getElementById('run-button');
this.clearButton = document.getElementById('clear-button');
this.exampleSelect = document.getElementById('example-select');
this.statusIndicator = document.getElementById('wasm-status');
if (!this.editor || !this.output) return;
this.setupEventListeners();
await this.loadWasm();
}
setupEventListeners() {
if (this.runButton) {
this.runButton.addEventListener('click', () => this.run());
}
if (this.clearButton) {
this.clearButton.addEventListener('click', () => this.clearOutput());
}
if (this.exampleSelect) {
this.exampleSelect.addEventListener('change', (e) => this.loadExample(e.target.value));
}
if (this.editor) {
this.editor.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
this.insertTab();
}
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
this.run();
}
});
}
}
insertTab() {
const start = this.editor.selectionStart;
const end = this.editor.selectionEnd;
const value = this.editor.value;
this.editor.value = value.substring(0, start) + ' ' + value.substring(end);
this.editor.selectionStart = this.editor.selectionEnd = start + 2;
}
async loadWasm() {
this.setStatus('loading', 'Loading WebAssembly...');
try {
const wasmModule = await WrenVM();
this.vm = {
init: wasmModule.cwrap('wasm_init', 'number', []),
interpret: wasmModule.cwrap('wasm_interpret', 'string', ['string']),
free: wasmModule.cwrap('wasm_free', null, []),
getVersion: wasmModule.cwrap('wasm_get_version', 'string', [])
};
const initResult = this.vm.init();
if (initResult !== 0) {
throw new Error('Failed to initialize Wren VM');
}
const version = this.vm.getVersion();
this.setStatus('ready', 'Ready (Wren ' + version + ')');
this.isReady = true;
if (this.runButton) {
this.runButton.disabled = false;
}
} catch (error) {
console.error('Failed to load WASM:', error);
this.setStatus('error', 'Failed to load WebAssembly');
}
}
setStatus(state, message) {
if (!this.statusIndicator) return;
this.statusIndicator.textContent = message;
this.statusIndicator.className = 'wasm-status ' + state;
}
run() {
if (!this.isReady || !this.editor) return;
const source = this.editor.value;
if (!source.trim()) {
this.appendOutput('(empty program)\n', 'info');
return;
}
this.appendOutput('>>> Running...\n', 'info');
try {
const result = this.vm.interpret(source);
if (result) {
this.appendOutput(result, result.includes('Error') ? 'error' : 'output');
} else {
this.appendOutput('(no output)\n', 'info');
}
} catch (error) {
this.appendOutput('Execution error: ' + error.message + '\n', 'error');
}
}
appendOutput(text, type) {
if (!this.output) return;
const span = document.createElement('span');
span.className = 'output-' + type;
span.textContent = text;
this.output.appendChild(span);
this.output.scrollTop = this.output.scrollHeight;
}
clearOutput() {
if (this.output) {
this.output.innerHTML = '';
}
}
loadExample(name) {
if (!this.editor || !name) return;
const examples = {
'hello': 'System.print("Hello, World!")',
'fibonacci':
`var fib = Fn.new { |n|
if (n <= 1) return n
return fib.call(n - 1) + fib.call(n - 2)
}
for (i in 0..10) {
System.print("fib(%(i)) = %(fib.call(i))")
}`,
'classes':
`class Animal {
construct new(name) {
_name = name
}
name { _name }
speak() { System.print("%(name) says something") }
}
class Dog is Animal {
construct new(name, breed) {
super(name)
_breed = breed
}
breed { _breed }
speak() { System.print("%(name) the %(breed) barks!") }
}
var dog = Dog.new("Rex", "German Shepherd")
dog.speak()`,
'math':
`import "math" for Math
System.print("=== Math Module ===")
System.print("Pi: %(Math.pi)")
System.print("E: %(Math.e)")
System.print("")
System.print("sin(pi/2) = %(Math.sin(Math.pi / 2))")
System.print("cos(0) = %(Math.cos(0))")
System.print("sqrt(16) = %(Math.sqrt(16))")
System.print("pow(2, 10) = %(Math.pow(2, 10))")
System.print("log(Math.e) = %(Math.log(Math.e))")`,
'json':
`import "json" for Json
var data = {
"name": "Wren",
"version": "0.4.0",
"features": ["classes", "fibers", "closures"]
}
System.print("Original map:")
System.print(data)
var jsonStr = Json.stringify(data)
System.print("")
System.print("JSON string:")
System.print(jsonStr)
var parsed = Json.parse(jsonStr)
System.print("")
System.print("Parsed name: %(parsed["name"])")
System.print("Features: %(parsed["features"])")`,
'datetime':
`import "datetime" for DateTime
var now = DateTime.now()
System.print("Current time: %(now)")
System.print("ISO format: %(now.toIso8601)")
System.print("")
System.print("Year: %(now.year)")
System.print("Month: %(now.month)")
System.print("Day: %(now.day)")
System.print("Hour: %(now.hour)")
System.print("Minute: %(now.minute)")
System.print("Second: %(now.second)")`,
'base64':
`import "base64" for Base64
var original = "Hello, Wren!"
System.print("Original: %(original)")
var encoded = Base64.encode(original)
System.print("Encoded: %(encoded)")
var decoded = Base64.decode(encoded)
System.print("Decoded: %(decoded)")`,
'strutil':
`import "strutil" for Str
var text = "Hello World"
System.print("Original: %(text)")
System.print("Lower: %(Str.toLower(text))")
System.print("Upper: %(Str.toUpper(text))")
System.print("")
var html = "<script>alert('xss')</script>"
System.print("Original HTML: %(html)")
System.print("Escaped: %(Str.escapeHtml(html))")
System.print("")
var hex = Str.hexEncode("ABC")
System.print("Hex encoded 'ABC': %(hex)")
System.print("Hex decoded: %(Str.hexDecode(hex))")`,
'faker':
`import "faker" for Faker
System.print("=== Faker Demo ===")
System.print("")
System.print("--- Person ---")
System.print("Name: %(Faker.name())")
System.print("Email: %(Faker.email())")
System.print("Phone: %(Faker.phoneNumber())")
System.print("")
System.print("--- Address ---")
System.print("City: %(Faker.city())")
System.print("State: %(Faker.state())")
System.print("Address: %(Faker.address())")
System.print("")
System.print("--- Business ---")
System.print("Company: %(Faker.company())")
System.print("Job Title: %(Faker.jobTitle())")
System.print("")
System.print("--- Internet ---")
System.print("Username: %(Faker.username())")
System.print("Domain: %(Faker.domainName())")
System.print("IPv4: %(Faker.ipv4())")`,
'lists':
`var numbers = [1, 2, 3, 4, 5]
System.print("Numbers: %(numbers)")
var doubled = numbers.map { |n| n * 2 }.toList
System.print("Doubled: %(doubled)")
var evens = numbers.where { |n| n % 2 == 0 }.toList
System.print("Evens: %(evens)")
var sum = numbers.reduce(0) { |acc, n| acc + n }
System.print("Sum: %(sum)")
System.print("")
System.print("First 3: %(numbers.take(3).toList)")
System.print("Skip 2: %(numbers.skip(2).toList)")`
};
if (examples[name]) {
this.editor.value = examples[name];
this.clearOutput();
}
}
}
document.addEventListener('DOMContentLoaded', () => {
window.playground = new WrenPlayground();
window.playground.init();
});
+320
View File
@@ -0,0 +1,320 @@
// retoor <retoor@molodetz.nl>
(function() {
'use strict';
var ManualSearch = {
index: null,
input: null,
results: null,
focusedIndex: -1,
init: function() {
this.input = document.getElementById('search-input');
this.results = document.getElementById('search-results');
if (!this.input || !this.results) return;
this.loadIndex();
this.bindEvents();
},
loadIndex: function() {
if (window.SEARCH_INDEX) {
this.index = window.SEARCH_INDEX;
return;
}
var self = this;
var basePath = this.getBasePath();
fetch(basePath + 'search-index.json')
.then(function(response) {
return response.json();
})
.then(function(data) {
self.index = data;
})
.catch(function() {
self.index = { pages: [] };
});
},
getBasePath: function() {
var path = window.location.pathname;
var parts = path.split('/').filter(function(p) { return p; });
var manualIdx = parts.indexOf('manual');
if (manualIdx === -1) return './';
var depth = parts.length - manualIdx - 2;
if (depth <= 0) return './';
var prefix = '';
for (var i = 0; i < depth; i++) {
prefix += '../';
}
return prefix;
},
levenshtein: function(a, b) {
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
var matrix = [];
for (var i = 0; i <= b.length; i++) matrix[i] = [i];
for (var j = 0; j <= a.length; j++) matrix[0][j] = j;
for (var i = 1; i <= b.length; i++) {
for (var j = 1; j <= a.length; j++) {
var cost = a[j - 1] === b[i - 1] ? 0 : 1;
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + cost
);
}
}
return matrix[b.length][a.length];
},
tokenize: function(text) {
return text.toLowerCase().split(/[\s\-_\/\.]+/).filter(function(w) {
return w.length > 0;
});
},
scoreMatch: function(term, text, words) {
var textLower = text.toLowerCase();
var score = 0;
if (textLower === term) {
score += 25;
}
for (var i = 0; i < words.length; i++) {
if (words[i] === term) {
score += 20;
break;
}
}
for (var i = 0; i < words.length; i++) {
if (words[i].indexOf(term) === 0) {
score += 15;
break;
}
}
if (textLower.indexOf(term) !== -1) {
score += 10;
}
if (term.length >= 4) {
for (var i = 0; i < words.length; i++) {
if (words[i].length >= 4) {
var dist = this.levenshtein(term, words[i]);
var maxDist = Math.floor(term.length / 3);
if (dist <= maxDist && dist > 0) {
score += Math.max(1, 8 - dist * 2);
break;
}
}
}
}
return score;
},
search: function(query) {
if (!query || query.length < 2 || !this.index) return [];
var terms = this.tokenize(query);
if (terms.length === 0) return [];
var scored = [];
var self = this;
for (var i = 0; i < this.index.pages.length; i++) {
var page = this.index.pages[i];
var score = 0;
var matchedTerms = 0;
var titleWords = this.tokenize(page.title);
var urlWords = this.tokenize(page.url);
var sectionWords = this.tokenize(page.section);
var descWords = page.description ? this.tokenize(page.description) : [];
for (var j = 0; j < terms.length; j++) {
var term = terms[j];
var termScore = 0;
var titleScore = this.scoreMatch(term, page.title, titleWords);
if (titleScore > 0) termScore += titleScore * 2.0;
var urlScore = this.scoreMatch(term, page.url, urlWords);
if (urlScore > 0) termScore += urlScore * 1.4;
var methodScore = 0;
if (page.methods) {
for (var k = 0; k < page.methods.length; k++) {
var mWords = this.tokenize(page.methods[k]);
var ms = this.scoreMatch(term, page.methods[k], mWords);
if (ms > methodScore) methodScore = ms;
}
}
if (methodScore > 0) termScore += methodScore * 1.6;
var descScore = this.scoreMatch(term, page.description || '', descWords);
if (descScore > 0) termScore += descScore * 1.0;
var sectionScore = this.scoreMatch(term, page.section, sectionWords);
if (sectionScore > 0) termScore += sectionScore * 0.6;
var contentScore = 0;
if (page.content) {
var contentWords = this.tokenize(page.content);
contentScore = this.scoreMatch(term, page.content, contentWords);
}
if (contentScore > 0) termScore += contentScore * 0.4;
if (termScore > 0) {
matchedTerms++;
score += termScore;
}
}
if (matchedTerms === terms.length) {
score *= 1.5;
} else if (matchedTerms === 0) {
score = 0;
}
if (score > 0) {
scored.push({ page: page, score: score });
}
}
scored.sort(function(a, b) { return b.score - a.score; });
return scored.slice(0, 10);
},
bindEvents: function() {
var self = this;
this.input.addEventListener('input', function(e) {
self.onSearch(e.target.value);
});
this.input.addEventListener('focus', function() {
if (self.input.value.length >= 2) {
self.results.classList.add('visible');
}
});
document.addEventListener('click', function(e) {
if (!e.target.closest('.search-container')) {
self.results.classList.remove('visible');
}
});
document.addEventListener('keydown', function(e) {
if (e.key === '/' && document.activeElement !== self.input) {
e.preventDefault();
self.input.focus();
}
if (e.key === 'Escape') {
self.results.classList.remove('visible');
self.input.setAttribute('aria-expanded', 'false');
self.input.blur();
}
});
this.input.addEventListener('keydown', function(e) {
if (!self.results.classList.contains('visible')) return;
var items = self.results.querySelectorAll('.search-result');
if (items.length === 0) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
self.focusedIndex = Math.min(self.focusedIndex + 1, items.length - 1);
self.updateFocusedResult(items);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
self.focusedIndex = Math.max(self.focusedIndex - 1, -1);
self.updateFocusedResult(items);
} else if (e.key === 'Enter' && self.focusedIndex >= 0) {
e.preventDefault();
items[self.focusedIndex].click();
}
});
},
updateFocusedResult: function(items) {
for (var i = 0; i < items.length; i++) {
items[i].classList.remove('focused');
items[i].setAttribute('aria-selected', 'false');
}
if (this.focusedIndex >= 0 && this.focusedIndex < items.length) {
items[this.focusedIndex].classList.add('focused');
items[this.focusedIndex].setAttribute('aria-selected', 'true');
items[this.focusedIndex].scrollIntoView({ block: 'nearest' });
this.input.setAttribute('aria-activedescendant', 'search-result-' + this.focusedIndex);
} else {
this.input.removeAttribute('aria-activedescendant');
}
},
onSearch: function(query) {
var matches = this.search(query);
this.renderResults(matches, query);
},
renderResults: function(matches, query) {
this.focusedIndex = -1;
this.input.removeAttribute('aria-activedescendant');
if (!query || query.length < 2) {
this.results.classList.remove('visible');
this.input.setAttribute('aria-expanded', 'false');
return;
}
if (matches.length === 0) {
this.results.innerHTML = '<div class="no-results" aria-live="polite">No results</div>';
this.results.classList.add('visible');
this.input.setAttribute('aria-expanded', 'true');
return;
}
var basePath = this.getBasePath();
var html = '';
for (var i = 0; i < matches.length; i++) {
var page = matches[i].page;
var fullUrl = basePath + page.url;
if (!this.isRelativeUrl(fullUrl)) continue;
html += '<a href="' + this.escapeAttr(fullUrl) + '" class="search-result" role="option" id="search-result-' + i + '" aria-selected="false">' +
'<span class="result-title">' + this.escapeHtml(page.title) + '</span>' +
'<span class="result-section">' + this.escapeHtml(page.section) + '</span>' +
'</a>';
}
this.results.innerHTML = html;
this.results.classList.add('visible');
this.input.setAttribute('aria-expanded', 'true');
},
escapeHtml: function(text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
escapeAttr: function(text) {
return text.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
},
isRelativeUrl: function(url) {
return /^[a-zA-Z0-9\-_\.\/]+$/.test(url) && !url.includes('//');
}
};
document.addEventListener('DOMContentLoaded', function() {
ManualSearch.init();
});
})();