UPdate.
This commit is contained in:
@@ -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
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
})();
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
},
|
||||
|
||||
isRelativeUrl: function(url) {
|
||||
return /^[a-zA-Z0-9\-_\.\/]+$/.test(url) && !url.includes('//');
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
ManualSearch.init();
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user