You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
308 lines
8.6 KiB
308 lines
8.6 KiB
import {Repl} from './repl.js'; |
|
|
|
let buffer = ''; |
|
const input = document.querySelector('#hidden-input'); |
|
const history = document.querySelector('#history'); |
|
const text = document.querySelector('#text'); |
|
const prefix = document.querySelector('#prefix'); |
|
|
|
const canvas = document.querySelector('#canvas'); |
|
const canvasSwitch = document.querySelector('#canvas-switch'); |
|
let showCanvas = false; |
|
|
|
const ctx = canvas.getContext('2d'); |
|
canvas.width = window.innerWidth; |
|
canvas.height = window.innerHeight; |
|
const clearScreen = () => { |
|
canvas.width = window.innerWidth; |
|
canvas.height = window.innerHeight; |
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
}; |
|
const fillRect = (color, x, y, w, h) => { |
|
ctx.fillStyle = color; |
|
ctx.fillRect(x, y, w, h); |
|
}; |
|
|
|
canvasSwitch.addEventListener('click', () => { |
|
showCanvas = !showCanvas; |
|
if (showCanvas) { |
|
canvas.style.display = 'block'; |
|
canvasSwitch.textContent = 'REPL'; |
|
} else { |
|
canvas.style.display = 'none'; |
|
canvasSwitch.textContent = 'CANVAS'; |
|
} |
|
}) |
|
|
|
const repl = new Repl(loadFromFile, loadTest, async () => { |
|
const onwrite = repl.stdout.onwrite; |
|
prefix.innerHTML = '<br>'; |
|
const text = await read('<br>'); |
|
|
|
repl.stdout.onwrite = result => { |
|
history.innerHTML += `${result.replaceAll('\n', '<br>')}`; |
|
repl.stdout.onwrite = onwrite; |
|
}; |
|
prefix.textContent = ''; |
|
repl.stdin.pushData(text); |
|
}); |
|
|
|
function evalp(text) { |
|
if (!text.trim()) { |
|
return false; |
|
} |
|
return Array.from(text).reduce((op, c) => { |
|
if (c === '(') { |
|
return op + 1; |
|
} |
|
if (c === ')' && op > 0) { |
|
return op - 1; |
|
} |
|
return op; |
|
}, 0) === 0; |
|
} |
|
|
|
function read(prefix = '') { |
|
return new Promise(resolve => { |
|
const controller = new AbortController(); |
|
input.addEventListener( |
|
'keypress', |
|
(e) => { |
|
switch(e.key) { |
|
case 'Enter': |
|
if (evalp(input.value) && |
|
(input.selectionStart == input.value.length || e.shiftKey)) { |
|
let text = input.value; |
|
history.innerHTML += `${prefix}${text}`; |
|
input.value = ''; |
|
updateText(); |
|
resolve(text); |
|
e.preventDefault(); |
|
controller.abort(); |
|
} |
|
break; |
|
} |
|
}, |
|
{ signal: controller.signal }) |
|
}); |
|
} |
|
|
|
function loadFromFile() { |
|
return new Promise((resolve) => { |
|
const input = document.createElement('input'); |
|
input.setAttribute('type', 'file'); |
|
input.setAttribute('accept', '.ptlisp'); |
|
input.setAttribute('class', 'hidden'); |
|
document.body.appendChild(input); |
|
input.addEventListener('change', e => { |
|
const reader = new FileReader(); |
|
reader.addEventListener('load', e => { |
|
resolve(e.target.result) |
|
document.body.removeChild(input); |
|
}); |
|
reader.readAsText(e.target.files[0]); |
|
}); |
|
input.click(); |
|
}); |
|
} |
|
|
|
async function loadTest() { |
|
let response = await fetch("test.ptlisp"); |
|
|
|
if (response.status != 200) { |
|
throw new Error("Server Error"); |
|
} |
|
|
|
return await response.text(); |
|
} |
|
|
|
function readInput() { |
|
prefix.textContent = 'USER > '; |
|
read(prefix.textContent).then(text => { |
|
prefix.textContent = ''; |
|
|
|
repl.stdout.onwrite = result => { |
|
history.innerHTML += `${result.replaceAll('\n', '<br>')}`; |
|
readInput(); |
|
}; |
|
|
|
repl.stdin.pushData(text); |
|
}); |
|
} |
|
|
|
readInput(); |
|
|
|
function getNextBracket (chars, searched, alternate) { |
|
let alternates = 0; |
|
let i = 0; |
|
for (let c of chars) { |
|
if (alternates == 0 && c == searched) { |
|
return i; |
|
} else if (c == alternate) { |
|
alternates++; |
|
} else if (c == searched) { |
|
alternates--; |
|
} |
|
i++; |
|
} |
|
} |
|
|
|
function getNextClosingBracket(text, index) { |
|
return getNextBracket(Array.from(text.substr(index)), ')', '(') + index; |
|
} |
|
|
|
function getPreviousOpenBracket(text, index) { |
|
return index - getNextBracket(Array.from(text.substr(0, index)).reverse(), '(', ')') - 1; |
|
} |
|
|
|
function updateText(selectionStart, selectionEnd) { |
|
while(text.firstChild) { |
|
text.removeChild(text.lastChild); |
|
} |
|
|
|
selectionEnd += selectionEnd - selectionStart ? 0 : 1; |
|
let cursorText = input.value.substr(selectionStart, selectionEnd - selectionStart) || ' '; |
|
|
|
let secondCursorIdx = -1; |
|
switch (cursorText) { |
|
case '(': secondCursorIdx = getNextClosingBracket(input.value, selectionEnd); break; |
|
case ')': secondCursorIdx = getPreviousOpenBracket(input.value, selectionStart); break; |
|
} |
|
|
|
if (secondCursorIdx >= 0 && selectionStart > secondCursorIdx) { |
|
text.appendChild(document.createTextNode(input.value.substr(0, secondCursorIdx))); |
|
const secondCursor = document.createElement('SPAN'); |
|
secondCursor.classList.add('second-cursor'); |
|
secondCursor.appendChild(document.createTextNode(input.value.substr(secondCursorIdx, 1))); |
|
text.appendChild(secondCursor); |
|
if (secondCursorIdx + 1 != selectionStart) { |
|
text.appendChild( |
|
document.createTextNode( |
|
input.value.substr(secondCursorIdx + 1, selectionStart - (secondCursorIdx + 1)))); |
|
} |
|
} else if (selectionStart > 0) { |
|
text.appendChild(document.createTextNode(input.value.substr(0, selectionStart))); |
|
} |
|
|
|
let cursor = document.createElement('SPAN'); |
|
cursor.classList.add('cursor'); |
|
// cursor.appendChild(document.createTextNode(cursorText)); |
|
cursor.innerHTML = cursorText; |
|
if (cursorText == '\n') { |
|
cursor.classList.add('end-of-line'); |
|
} |
|
text.appendChild(cursor); |
|
|
|
if (secondCursorIdx >= selectionEnd) { |
|
text.appendChild( |
|
document.createTextNode( |
|
input.value.substr(selectionEnd, secondCursorIdx - selectionEnd))); |
|
const secondCursor = document.createElement('SPAN'); |
|
secondCursor.classList.add('second-cursor'); |
|
secondCursor.appendChild(document.createTextNode(input.value.substr(secondCursorIdx, 1))); |
|
text.appendChild(secondCursor); |
|
text.appendChild( |
|
document.createTextNode( |
|
input.value.substr(secondCursorIdx + 1))); |
|
} else if (selectionEnd < input.value.length) { |
|
text.appendChild(document.createTextNode(input.value.substr(selectionEnd))); |
|
} |
|
} |
|
|
|
input.addEventListener('input', (e) => { |
|
var selection = input.selectionStart; |
|
if (e.data == '(') { |
|
input.value = input.value.slice(0, selection) + ')' + input.value.slice(selection); |
|
input.setSelectionRange(selection, selection); |
|
} |
|
|
|
updateText(input.selectionStart, input.selectionEnd); |
|
buffer = input.value; |
|
}) |
|
|
|
input.addEventListener('keydown', (e) => { |
|
if (e.key == 'Backspace' && |
|
input.selectionEnd == input.selectionStart) { |
|
if (input.value.substr(input.selectionStart - 1, 1) == '(' && |
|
input.value.substr(input.selectionStart, 1) == ')') { |
|
input.value = input.value.substr(0, input.selectionStart - 1) + input.value.substr(input.selectionStart + 1); |
|
input.selectionStart = input.selectionEnd = input.selectionStart - 1; |
|
e.preventDefault(); |
|
} else if (input.value.substr(input.selectionStart - 1, 1) == ')'){ |
|
input.selectionStart = input.selectionEnd = input.selectionStart - 1; |
|
e.preventDefault(); |
|
} |
|
} else if (e.key == ')' && |
|
input.selectionStart - input.selectionEnd == 0 && |
|
input.value.substr(input.selectionStart, 1) == ')') { |
|
input.selectionEnd = ++input.selectionStart; |
|
e.preventDefault(); |
|
} |
|
setTimeout(() => { |
|
updateText(input.selectionStart, input.selectionEnd); |
|
}); |
|
}) |
|
|
|
document.addEventListener('mouseup', (e) => { |
|
const selection = document.getSelection(); |
|
if (!(selection.anchorNode.parentNode == text || |
|
selection.anchorNode.parentNode.parentNode == text)) { |
|
return; |
|
} |
|
|
|
let textNodes = [], node, walk = document.createTreeWalker(text, NodeFilter.SHOW_TEXT, null, false); |
|
while (node = walk.nextNode()) { |
|
textNodes.push(node); |
|
} |
|
let startNodeIdx = textNodes.indexOf(selection.anchorNode); |
|
let endNodeIdx = textNodes.indexOf(selection.focusNode); |
|
let startNodeOffset = selection.anchorOffset; |
|
let endNodeOffset = selection.focusOffset; |
|
for (let i = 0; i < startNodeIdx; i++) { |
|
startNodeOffset += textNodes[i].textContent.length; |
|
} |
|
|
|
for (let i = 0; i < endNodeIdx; i++) { |
|
endNodeOffset += textNodes[i].textContent.length; |
|
} |
|
|
|
input.setSelectionRange(Math.min(startNodeOffset, endNodeOffset), |
|
Math.max(startNodeOffset, endNodeOffset)); |
|
updateText(input.selectionStart, input.selectionEnd); |
|
}); |
|
|
|
|
|
|
|
function focusInput() { |
|
input.focus(); |
|
} |
|
|
|
window.addEventListener('focus', focusInput); |
|
|
|
window.addEventListener('click', focusInput); |
|
|
|
focusInput(); |
|
|
|
|
|
|
|
|
|
|
|
// WASM |
|
const response = await fetch('./lisp.wasm'); |
|
const bytes = await response.arrayBuffer(); |
|
const results = await WebAssembly.instantiate(bytes, { |
|
env: { |
|
jsprint: (byteOffset) => { |
|
let s = ''; |
|
const a = new Uint8Array(memory.buffer); |
|
for (var i = byteOffset; a[i]; i++) { |
|
s += String.fromCharCode(a[i]); |
|
} |
|
|
|
console.log(s); |
|
} |
|
} |
|
}); |
|
const instance = results.instance; |
|
const memory = instance.exports.pagememory; |
|
instance.exports.helloworld();
|
|
|