Skip to content

Commit 87ea29c

Browse files
Merge pull request #109 from tylerchilds/js-repl
wip js repl
2 parents 2482244 + fc4dffb commit 87ea29c

File tree

3 files changed

+364
-0
lines changed

3 files changed

+364
-0
lines changed

src/pages/plan1.html

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ <h4>Demo</h4>
5757
<qr-code src="https://github.com/p2plabsxyz/peersky-browser" data-fg="saddlebrown" data-bg="lemonchiffon"></qr-code>
5858
</div>
5959

60+
<h3>JS Repl</h3>
61+
<p>
62+
A JavaScript Read-Evaluate-Print-Loop. A game. A boring game for super nerds.
63+
</p>
64+
65+
<h4>Demo</h4>
66+
67+
<div class="example">
68+
<js-repl></js-repl>
69+
</div>
70+
6071
<hr>
6172

6273
<h2>Elvish</h2>
@@ -138,6 +149,18 @@ <h4>Demo</h4>
138149
</div>
139150
</div>
140151

152+
<!--
153+
154+
iife that exposes QJS, context
155+
https://gist.github.com/simonw/36506994222a56d1556b3452ca663dbe
156+
157+
refresh:
158+
curl 'https://cdn.jsdelivr.net/npm/[email protected]/dist/index.global.js' > quickjs.js
159+
-->
160+
161+
<script src="peersky://static/js/vendor/quickjs-emscripten/quickjs.js"></script>
162+
141163
<script src="peersky://static/elves/hello-world.js" type="module"></script>
142164
<script src="peersky://static/elves/qr-code.js" type="module"></script>
143165
<script src="peersky://static/elves/goodbye-world.js" type="module"></script>
166+
<script src="peersky://static/elves/js-repl.js" type="module"></script>

src/pages/static/elves/js-repl.js

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
import $elf from 'peersky://static/elves/elf.js'
2+
3+
// expects
4+
// <script src="peersky://static/js/vendor/quickjs-emscripten/quickjs.js"></script>
5+
6+
const { getQuickJS } = self.QJS
7+
8+
async function main() {
9+
const QuickJS = await getQuickJS()
10+
const vm = QuickJS.newContext()
11+
12+
const world = vm.newString("world")
13+
vm.setProp(vm.global, "NAME", world)
14+
world.dispose()
15+
16+
const result = vm.evalCode(`"Hello " + NAME + "!"`)
17+
if (result.error) {
18+
console.log("Execution failed:", vm.dump(result.error))
19+
result.error.dispose()
20+
} else {
21+
console.log("Success:", vm.dump(result.value))
22+
result.value.dispose()
23+
}
24+
25+
vm.dispose()
26+
}
27+
28+
main()
29+
30+
const data = {
31+
input: `
32+
/*
33+
* Congratulations, you've found java's crypt!
34+
*
35+
* this is a text based adventure game that is won by code.
36+
*
37+
*/
38+
39+
// a function is a block of code that can be re-used
40+
function hello() {
41+
return 'world'
42+
}
43+
44+
// say hello from wherever in the world
45+
hello()
46+
47+
// values
48+
const a = 2;
49+
const b = 3;
50+
const c = 4;
51+
52+
const target = {
53+
score: 0
54+
}
55+
56+
function score(x, count) {
57+
if(count) {
58+
x.score += count
59+
}
60+
61+
return x.score
62+
}
63+
64+
function formula(x, y, z) {
65+
return x * y + z
66+
}
67+
68+
const views = {
69+
north: (target) => {
70+
return 'Forest blocks all directions. Except a beach to the SOUTH. And the WAFFLES above.'
71+
},
72+
south: (target) => {
73+
return 'Ocean blocks all directions. Except a meadow to the NORTH. And the WAFFLES above.'
74+
},
75+
waffles: (target) => {
76+
return 'You win. WAFFLES. (total score: ' + score(target) + ')'
77+
}
78+
}
79+
80+
const commands = {
81+
north: (target) => {
82+
},
83+
south: (target) => {
84+
},
85+
waffles: (target) => {
86+
score(target, 1)
87+
}
88+
}
89+
90+
function turn(command) {
91+
if(commands[command]) {
92+
history(command)
93+
commands[command](target)
94+
}
95+
}
96+
97+
let activeView = 'north'
98+
function view(newView) {
99+
if(newView) {
100+
turn(newView)
101+
activeView = newView
102+
}
103+
return activeView
104+
}
105+
106+
function print() {
107+
return views[view()] ? views[view()](target) : error('Invalid View')
108+
}
109+
110+
function error(message) {
111+
return message
112+
}
113+
114+
let past = []
115+
function history(now) {
116+
if(now) {
117+
past.push(now)
118+
}
119+
120+
return past
121+
}
122+
123+
function map() {
124+
return {
125+
key: hello(),
126+
value: formula(a,b,c),
127+
history: history(),
128+
view: view(),
129+
print: print(),
130+
score: score(target),
131+
actions: ['north', 'south', 'waffles'],
132+
import: {
133+
meta: {
134+
url: "${import.meta.url}"
135+
}
136+
}
137+
}
138+
}
139+
140+
function render(tag, x) {
141+
return "<"+tag+">"+x+"</"+tag+">"
142+
}
143+
144+
map()
145+
view('south')
146+
view('north')
147+
map()
148+
view('waffles')
149+
view('waffles')
150+
view('waffles')
151+
view('waffles')
152+
view('waffles')
153+
view('waffles')
154+
map()
155+
view('waffles')
156+
map()
157+
render('static-code', JSON.stringify(map(), '', 2))
158+
`,
159+
output: null
160+
}
161+
162+
const $ = $elf('js-repl', data)
163+
export default $
164+
debugger
165+
166+
window.Module = {
167+
print: function (msg) { log(msg) }
168+
}
169+
function log(text) {
170+
$.teach(text, mergeOutput)
171+
}
172+
173+
export async function runJs(program) {
174+
$.teach({ output: null })
175+
const QuickJS = await getQuickJS()
176+
const vm = QuickJS.newContext()
177+
178+
const result = vm.evalCode(program)
179+
if (result.error) {
180+
const error = vm.dump(result.error)
181+
result.error.dispose()
182+
vm.dispose()
183+
return error
184+
} else {
185+
const data = vm.dump(result.value)
186+
result.value.dispose()
187+
vm.dispose()
188+
return data
189+
}
190+
}
191+
192+
async function run() {
193+
const { input } = $.learn()
194+
const output = await runJs(input)
195+
$.teach({ output })
196+
}
197+
198+
$.when('click', '[data-run]', run)
199+
$.when('click', '[data-edit]', () => $.teach({ output: null }))
200+
201+
$.draw(render, { beforeUpdate, afterUpdate })
202+
203+
function render(target) {
204+
const { input, output } = $.learn()
205+
return `
206+
<div class="action-bar">
207+
<button style="float: right; margin-left: 1rem;" data-run class="standard-button">Run</button>
208+
<button style="float: right;" data-edit class="standard-button -outlined hide-full">Edit</button>
209+
<div class="title">Elf Tunnel A</div>
210+
</div>
211+
<div class="input ${output?'invisible':'visible'}">
212+
<textarea
213+
name="input"
214+
data-bind="input"
215+
placeholder="Say it, don't spray it."
216+
value="${escapeHyperText(input)}"
217+
></textarea>
218+
</div>
219+
<div class="output ${output?'visible':'invisible'}">
220+
<div class="textarea">${output}</div>
221+
</div>
222+
`
223+
}
224+
225+
function beforeUpdate(target) {
226+
{ // convert a query string to new post
227+
const q = target.getAttribute('q')
228+
if(!target.initialized) {
229+
target.initialized = true
230+
if(q) {
231+
const input = decodeURIComponent(q)
232+
$.teach({ input })
233+
}
234+
}
235+
}
236+
237+
238+
}
239+
240+
function afterUpdate(target) {
241+
242+
}
243+
244+
function mergeOutput(state, payload) {
245+
return {
246+
...state,
247+
output: [...state.output, payload]
248+
}
249+
}
250+
251+
function escapeHyperText(text = '') {
252+
if(!text) return ''
253+
return text.replace(/[&<>'"]/g,
254+
actor => ({
255+
'&': '&amp;',
256+
'<': '&lt;',
257+
'>': '&gt;',
258+
"'": '&#39;',
259+
'"': '&quot;'
260+
}[actor])
261+
)
262+
}
263+
264+
$.when('input', '[data-bind]', (event) => {
265+
$.teach({[event.target.name]: event.target.value })
266+
})
267+
268+
$.style(`
269+
& {
270+
display: grid;
271+
grid-template-rows: auto 1fr;
272+
grid-template-columns: 1fr;
273+
height: 100%;
274+
overflow: hidden;
275+
}
276+
277+
& .action-bar {
278+
background: rgba(0,0,0,1);
279+
padding: .5rem;
280+
display: block;
281+
}
282+
283+
& .title {
284+
color: rgba(255,255,255,.85);
285+
font-weight: bold;
286+
font-size: 1.5rem;
287+
}
288+
289+
& .input textarea {
290+
border: none;
291+
height: 100%;
292+
width: 100%;
293+
resize: none;
294+
background: rgba(0,0,0,.85);
295+
color: rgba(255,255,255,.85);
296+
padding: .5rem;
297+
border-radius: 0;
298+
}
299+
300+
& .output {
301+
height: 100%;
302+
overflow: auto;
303+
padding: .5rem;
304+
}
305+
306+
& .output .textarea {
307+
white-space: preserve;
308+
}
309+
310+
& .invisible {
311+
display: none;
312+
}
313+
314+
@media (min-width: 36rem) {
315+
& {
316+
display: grid;
317+
grid-template-rows: auto 1fr;
318+
grid-template-columns: 1fr 1fr;
319+
}
320+
321+
& .action-bar {
322+
grid-column: -1 / 1;
323+
}
324+
325+
& .invisible {
326+
display: block;
327+
}
328+
329+
& .hide-full {
330+
display: none;
331+
}
332+
}
333+
`)
334+
335+
$elf($)

0 commit comments

Comments
 (0)