-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnotebook.js
277 lines (241 loc) · 7.44 KB
/
notebook.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
const lang = require('./lang.js')
const CodeMirror = require('/node_modules/codemirror/lib/codemirror.js')
const EXAMPLES = {
en: ` white black
fade slow
blue
swap
red yellow fade
yellow green fade
glue
green cyan fade
glue
cyan blue fade
glue
blue purple fade
glue
copy
slow
swap
copy
copy reverse glue`,
he: ` לבן שחור
מעבר לאט
כחול
החלף
אדום צהוב מעבר
צהוב ירוק מעבר
הדבק
ירוק תכלת מעבר
הדבק
תכלת כחול מעבר
הדבק
כחול סגול מעבר
הדבק
שכפל
לאט
החלף
שכפל
שכפל הפוך הדבק`,
}
var exampleButton = document.getElementById('example')
var langSelector = document.getElementById('lang')
var hasBackend = true // until proven otherwise
// Set by initEditor() at the end...
var editor, doc
var dictionary = document.getElementById('dictionary')
const showDictionary = () => {
const words = Object.keys(lang.wordsByLanguage[langSelector.value])
//words.sort() — actually no, lang.js definition order is logical.
dictionary.replaceChildren(
...words.map(w => {
let el = document.createElement('span')
el.innerText = ' ' + w + ' ' // spaces help when copy-pasted
return el
})
)
}
// HACK: Executing word-by-word *as part of CodeMirror parsing*,
// so that we retain the execution state after each word, and also highlight errors.
const editorConfig = () => {
const dictionary = lang.wordsByLanguage[langSelector.value]
return {
mode: { // CM passes this into `modeOptions` 2nd arg below
name: 'animation-stack-language',
initialState: lang.initialState(dictionary, []),
},
direction: lang.isRightToLeft(langSelector.value) ? 'rtl' : 'ltr',
}
}
CodeMirror.defineMode('animation-stack-language',
(cmConfig, modeOptions) => {
return {
// Keeping evaluation state inside CodeMirror mode is convenient
// and even gives us partial caching on edits for free.
// TODO: once I support function defitions, will need
// invalidation when another definition is changed.
startState: () => (
{ langState: modeOptions.initialState }
),
// TODO optimize (CodeMirror by default copies arrays, no need as we're immutable)
//copyState: (modeState) =>
token: (stream, modeState) => {
if (stream.eatSpace()) {
return ''
}
let m = stream.match(/^\S+/)
if (m.length > 0) {
const [word] = m
modeState.langState = lang.evalSmallStep(modeState.langState, word)
if (modeState.langState.error) {
if (modeState.langState.error === 'NameError') {
return 'word unknown'
} else {
return 'word error'
}
} else {
return 'word good'
}
}
}
}
}
)
var result = document.getElementById('result')
const cssChannel = (val) => Math.round(lang.clipChannel(val)).toString()
const renderColor = (color) => {
const { red, green, blue } = color
let el = document.createElement('span')
el.className = 'moment_color'
el.textContent = '█' // U+2588 FULL BLOCK
el.style.color = `rgb(${cssChannel(red)}, ${cssChannel(green)}, ${cssChannel(blue)})`
return el
}
const renderAnim = (anim) => {
let el = document.createElement('div')
el.className = 'animation'
for (let time = 0; time <= anim.duration; time += 0.2) {
try {
el.append(renderColor(anim.color(time)))
} catch (err) {
el.append('✗')
}
}
return el
}
const renderStack = (stack) => {
let el = document.createElement('div')
el.className = 'stack'
el.append(...stack.slice().reverse().map(renderAnim))
return el
}
const renderEvalPosition = (className, textContent, tooltip) => {
let el = document.createElement('span')
el.classList.add('eval-position')
el.classList.add(className)
el.textContent = textContent
el.title = tooltip
return el
}
let bookmark = null
const showResult = () => {
// Find close word boundary to use as eval position.
// Going left then right means that when in middle of word, we're evaluating after it.
// TODO use findWordAt?
let pos = editor.getCursor('head')
// Do allow evaluation at very start of document.
if (pos.line !== 0 || pos.ch !== 0) {
pos = editor.findPosH(pos, -1, 'word')
pos = editor.findPosH(pos, 1, 'word')
}
if (bookmark !== null) {
bookmark.clear()
}
window.charPos = doc.indexFromPos(pos)
const token = editor.getTokenAt(pos, true)
if (token.state.langState.error) {
if (token.state.langState.error === 'NameError') {
let widget = renderEvalPosition(
'cm-unknown', '📖', // U+1F4D6 OPEN BOOK
token.state.langState.errorMessage)
bookmark = doc.setBookmark(pos, { widget })
} else {
let widget = renderEvalPosition(
'cm-error', '💥', // U+1F4A5 COLLISION SYMBOL, double-width
token.state.langState.errorMessage)
bookmark = doc.setBookmark(pos, { widget })
}
} else {
let widget = renderEvalPosition('cm-good', '👀') // U+1F440 EYES
bookmark = doc.setBookmark(pos, { widget })
}
const stack = token.state.langState.stack
result.querySelector('.stack').replaceWith(renderStack(stack))
}
// Also send whole code to backend.
// NOT sensitive to cursor position, so you can use UI to probe execution process without disturbing the room lighting.
const sendToBackend = () => {
if (hasBackend) {
// KLUDGE: Async, not awaiting.
const codeForBackend = { __lang__: langSelector.value, main: doc.getValue() }
fetch('/api/code', { method: 'PUT', body: JSON.stringify(codeForBackend) })
console.log('sent to backend', codeForBackend)
}
}
var sourceTextArea = document.getElementById('source')
console.log(sourceTextArea.value)
const initEditor = () => {
editor = CodeMirror.fromTextArea(
sourceTextArea,
{
autofocus: true,
viewportMargin: Infinity, // https://codemirror.net/demo/resize.html
theme: 'xq-dark',
...editorConfig(),
}
)
doc = editor.getDoc()
editor.setCursor({ line: Infinity, ch: Infinity }) // end of doc
editor.on('change', showResult)
editor.on('cursorActivity', showResult)
editor.on('change', sendToBackend)
langSelector.onchange = () => {
for (const [name, value] of Object.entries(editorConfig())) {
editor.setOption(name, value)
}
showResult()
sendToBackend()
showDictionary()
}
exampleButton.onclick = () => {
doc.setValue(EXAMPLES[langSelector.value])
editor.setCursor({ line: Infinity, ch: Infinity }) // end of doc
showResult()
sendToBackend()
}
showDictionary()
showResult()
}
// Try loading last executed code from server.
// Can fail if no backend running, just static netlify.
// Wait either way before setting up editor to avoid race conditions.
fetch('/api/code', { mode: 'no-cors' })
.then((response) => {
if (response.status !== 200) {
throw new Error(`${response.status} ${response.statusText}`)
}
return response.json().then((result) => {
console.log('Got initial code.', result)
langSelector.value = result.__lang__
sourceTextArea.value = result.main
})
})
// Network error, HTTP errors (404), JSON.parse error
.catch((err) => {
console.error('Error getting initial code.', err)
hasBackend = false
langSelector.value = lang.userLanguage()
})
// Separated here so errors evaluating (e.g. stack underflow)
// don't make us give up `hasBackend = false`.
.then(initEditor)