rcas 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CITATION.cff +17 -0
- data/DESIGN.md +783 -0
- data/LICENSE +21 -0
- data/MANUAL.md +6265 -0
- data/README.md +267 -0
- data/bin/rcas +9 -0
- data/bin/rcas-app +9 -0
- data/bin/rcas-chat +9 -0
- data/lib/rcas/algebraic.rb +481 -0
- data/lib/rcas/analysis.rb +966 -0
- data/lib/rcas/app/launcher.rb +203 -0
- data/lib/rcas/app/public/app.css +402 -0
- data/lib/rcas/app/public/app.js +449 -0
- data/lib/rcas/app/public/index.html +46 -0
- data/lib/rcas/app/server.rb +220 -0
- data/lib/rcas/app/window.rb +94 -0
- data/lib/rcas/app/worksheet.rb +290 -0
- data/lib/rcas/app.rb +168 -0
- data/lib/rcas/background.rb +758 -0
- data/lib/rcas/chat/assistant.rb +199 -0
- data/lib/rcas/chat/picker.rb +164 -0
- data/lib/rcas/chat/repl.rb +583 -0
- data/lib/rcas/chat/session.rb +137 -0
- data/lib/rcas/chat/settings.rb +71 -0
- data/lib/rcas/chat/style.rb +30 -0
- data/lib/rcas/chat/tool.rb +53 -0
- data/lib/rcas/chat/ui.rb +316 -0
- data/lib/rcas/chat/usage.rb +62 -0
- data/lib/rcas/chat/workspace.rb +132 -0
- data/lib/rcas/chat.rb +54 -0
- data/lib/rcas/coefficients.rb +170 -0
- data/lib/rcas/combinatorics.rb +274 -0
- data/lib/rcas/complex_parts.rb +160 -0
- data/lib/rcas/constants.rb +129 -0
- data/lib/rcas/core_ext.rb +35 -0
- data/lib/rcas/decide.rb +501 -0
- data/lib/rcas/decompositions.rb +241 -0
- data/lib/rcas/differentiate.rb +144 -0
- data/lib/rcas/discussion.rb +558 -0
- data/lib/rcas/distributions.rb +980 -0
- data/lib/rcas/dixon.rb +95 -0
- data/lib/rcas/docs.rb +321 -0
- data/lib/rcas/domains.rb +728 -0
- data/lib/rcas/expand.rb +174 -0
- data/lib/rcas/expression.rb +613 -0
- data/lib/rcas/factor.rb +605 -0
- data/lib/rcas/finite_field.rb +577 -0
- data/lib/rcas/fourier.rb +118 -0
- data/lib/rcas/fps.rb +678 -0
- data/lib/rcas/fraction.rb +126 -0
- data/lib/rcas/functions.rb +1136 -0
- data/lib/rcas/gcd.rb +112 -0
- data/lib/rcas/geometry.rb +266 -0
- data/lib/rcas/groebner.rb +162 -0
- data/lib/rcas/hold.rb +277 -0
- data/lib/rcas/hypothesis.rb +364 -0
- data/lib/rcas/inequalities.rb +689 -0
- data/lib/rcas/integral_functions.rb +260 -0
- data/lib/rcas/integrate.rb +1589 -0
- data/lib/rcas/integrate_substitutions.rb +434 -0
- data/lib/rcas/interpolate.rb +40 -0
- data/lib/rcas/irb.rb +146 -0
- data/lib/rcas/laplace.rb +159 -0
- data/lib/rcas/latex.rb +556 -0
- data/lib/rcas/lattice.rb +172 -0
- data/lib/rcas/linear_algebra.rb +117 -0
- data/lib/rcas/linear_program.rb +416 -0
- data/lib/rcas/lint.rb +79 -0
- data/lib/rcas/matrix.rb +531 -0
- data/lib/rcas/matrix_multiply.rb +202 -0
- data/lib/rcas/multimodular.rb +286 -0
- data/lib/rcas/named_polynomials.rb +274 -0
- data/lib/rcas/number_theory.rb +443 -0
- data/lib/rcas/numerics.rb +825 -0
- data/lib/rcas/ode.rb +488 -0
- data/lib/rcas/openmath/objects.rb +364 -0
- data/lib/rcas/openmath/phrasebook.rb +551 -0
- data/lib/rcas/openmath/popcorn.rb +518 -0
- data/lib/rcas/openmath/xml.rb +309 -0
- data/lib/rcas/openmath.rb +49 -0
- data/lib/rcas/petkovsek.rb +165 -0
- data/lib/rcas/piecewise.rb +488 -0
- data/lib/rcas/plot.rb +763 -0
- data/lib/rcas/plot3d.rb +419 -0
- data/lib/rcas/poly_matrix.rb +318 -0
- data/lib/rcas/poly_recurrence.rb +117 -0
- data/lib/rcas/polynomial.rb +466 -0
- data/lib/rcas/precision.rb +925 -0
- data/lib/rcas/printer.rb +150 -0
- data/lib/rcas/product.rb +155 -0
- data/lib/rcas/q_difference.rb +296 -0
- data/lib/rcas/q_functions.rb +158 -0
- data/lib/rcas/q_summation.rb +308 -0
- data/lib/rcas/q_zeilberger.rb +199 -0
- data/lib/rcas/random.rb +506 -0
- data/lib/rcas/rational_function.rb +186 -0
- data/lib/rcas/recurrence.rb +323 -0
- data/lib/rcas/render.rb +431 -0
- data/lib/rcas/results.rb +192 -0
- data/lib/rcas/scalar.rb +219 -0
- data/lib/rcas/series.rb +726 -0
- data/lib/rcas/simplify.rb +649 -0
- data/lib/rcas/solve.rb +2002 -0
- data/lib/rcas/special.rb +163 -0
- data/lib/rcas/statistics.rb +175 -0
- data/lib/rcas/steps.rb +835 -0
- data/lib/rcas/summation.rb +532 -0
- data/lib/rcas/trig.rb +264 -0
- data/lib/rcas/van_hoeij.rb +241 -0
- data/lib/rcas/vector.rb +175 -0
- data/lib/rcas/vector_calculus.rb +411 -0
- data/lib/rcas/version.rb +5 -0
- data/lib/rcas/zeilberger.rb +358 -0
- data/lib/rcas.rb +91 -0
- data/package.json +8 -0
- metadata +206 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
// The worksheet. Everything mathematical happens in Ruby; this file takes
|
|
2
|
+
// a line of input to /api/eval and draws the cell that comes back.
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const token = new URLSearchParams(location.search).get("token") || "";
|
|
6
|
+
const worksheet = document.getElementById("worksheet");
|
|
7
|
+
const greeting = document.getElementById("greeting");
|
|
8
|
+
const input = document.getElementById("input");
|
|
9
|
+
const promptLabel = document.getElementById("prompt");
|
|
10
|
+
const completions = document.getElementById("completions");
|
|
11
|
+
const modes = document.getElementById("modes");
|
|
12
|
+
|
|
13
|
+
const history = [];
|
|
14
|
+
let historyIndex = 0;
|
|
15
|
+
let draft = "";
|
|
16
|
+
let state = { mode: "typeset", theme: "auto", numbered: true, line: 1, version: "", modes: [] };
|
|
17
|
+
|
|
18
|
+
// ---- talking to Ruby -------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
async function api(path, body) {
|
|
21
|
+
const response = await fetch(path, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: { "Content-Type": "application/json", "X-RCAS-Token": token },
|
|
24
|
+
body: JSON.stringify(body || {})
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
|
|
27
|
+
return response.json();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---- small DOM helpers -----------------------------------------------------
|
|
31
|
+
|
|
32
|
+
function el(tag, className, text) {
|
|
33
|
+
const node = document.createElement(tag);
|
|
34
|
+
if (className) node.className = className;
|
|
35
|
+
if (text !== undefined && text !== null) node.textContent = text;
|
|
36
|
+
return node;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function line(tag, className) {
|
|
40
|
+
const row = el("div", "line" + (className ? " " + className : ""));
|
|
41
|
+
row.append(el("span", "tag", tag || ""));
|
|
42
|
+
const body = el("div", "body");
|
|
43
|
+
row.append(body);
|
|
44
|
+
return { row, body };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// KaTeX renders into the node; without the package the source is shown
|
|
48
|
+
// instead, which is still the honest answer.
|
|
49
|
+
function typeset(latex) {
|
|
50
|
+
const box = el("div", "tex");
|
|
51
|
+
if (window.katex && !window.katexMissing) {
|
|
52
|
+
try {
|
|
53
|
+
window.katex.render(latex, box, { displayMode: true, throwOnError: false, strict: "ignore" });
|
|
54
|
+
return box;
|
|
55
|
+
} catch (e) {
|
|
56
|
+
// fall through to the source
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return el("code", "source", latex);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function label(kind, n) {
|
|
63
|
+
if (n === undefined || n === null) return "";
|
|
64
|
+
return state.numbered ? `${kind}[${n}]` : kind;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---- drawing a cell --------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
function draw(cell, source) {
|
|
70
|
+
if (cell.kind === "clear") {
|
|
71
|
+
worksheet.replaceChildren();
|
|
72
|
+
if (cell.text) append(informational(cell));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (cell.kind === "exit") {
|
|
76
|
+
window.close();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const node = el("section", "cell");
|
|
80
|
+
if (source !== undefined) {
|
|
81
|
+
const { row, body } = line(label("In", cell.n), "in");
|
|
82
|
+
body.textContent = source;
|
|
83
|
+
body.title = "click to edit this line again";
|
|
84
|
+
body.addEventListener("click", () => {
|
|
85
|
+
input.value = source;
|
|
86
|
+
autosize();
|
|
87
|
+
input.focus();
|
|
88
|
+
});
|
|
89
|
+
node.append(row);
|
|
90
|
+
}
|
|
91
|
+
for (const part of output(cell)) node.append(part);
|
|
92
|
+
append(node);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function append(node) {
|
|
96
|
+
if (greeting && greeting.isConnected) greeting.remove();
|
|
97
|
+
worksheet.append(node);
|
|
98
|
+
worksheet.scrollTop = worksheet.scrollHeight;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The Out row (or whatever the command produced) for one cell.
|
|
102
|
+
function output(cell) {
|
|
103
|
+
switch (cell.kind) {
|
|
104
|
+
case "result": return [result(cell)];
|
|
105
|
+
case "error": return [failure(cell)];
|
|
106
|
+
case "help": return [commandTable(cell.commands)];
|
|
107
|
+
case "doc": return [documentation(cell)];
|
|
108
|
+
case "vars": return [rowTable(cell)];
|
|
109
|
+
default: return cell.text ? [informational(cell)] : [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// `x + 1/3` is x + 0 by the time rcas sees it: the server says so.
|
|
114
|
+
function warnings(cell, body) {
|
|
115
|
+
for (const text of cell.warnings || []) body.append(el("div", "warning", `warning: ${text}`));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function result(cell) {
|
|
119
|
+
const { row, body } = line(label("Out", cell.n));
|
|
120
|
+
warnings(cell, body);
|
|
121
|
+
if (cell.stdout) body.append(el("pre", "stdout", cell.stdout));
|
|
122
|
+
|
|
123
|
+
// A plot is a picture here, the way it is braille art in a terminal;
|
|
124
|
+
// only /output text asks for the characters.
|
|
125
|
+
const drawn = cell.svg && state.mode !== "text";
|
|
126
|
+
const showText = !drawn && (state.mode !== "typeset" || !cell.latex);
|
|
127
|
+
if (showText && cell.text) body.append(el("pre", "text", cell.text));
|
|
128
|
+
if (drawn) {
|
|
129
|
+
const plot = el("div", "plot");
|
|
130
|
+
plot.innerHTML = cell.svg; // Plot#to_svg builds this string itself
|
|
131
|
+
body.append(plot);
|
|
132
|
+
}
|
|
133
|
+
if (cell.latex && (state.mode === "typeset" || state.mode === "both")) body.append(typeset(cell.latex));
|
|
134
|
+
if (cell.latex && state.mode === "latex") body.append(el("code", "source", cell.latex));
|
|
135
|
+
if (cell.note) body.append(el("div", "note", cell.note));
|
|
136
|
+
if (cell.seconds > 0.25) body.append(el("div", "took", `${cell.seconds.toFixed(2)} s`));
|
|
137
|
+
return row;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function failure(cell) {
|
|
141
|
+
const { row, body } = line(label("Out", cell.n));
|
|
142
|
+
warnings(cell, body);
|
|
143
|
+
body.append(el("pre", "text error", cell.text));
|
|
144
|
+
if (cell.hint && cell.hint.length) {
|
|
145
|
+
const hint = el("div", "hint");
|
|
146
|
+
for (const text of cell.hint) hint.append(el("span", null, text));
|
|
147
|
+
body.append(hint);
|
|
148
|
+
}
|
|
149
|
+
return row;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function informational(cell) {
|
|
153
|
+
const { row, body } = line("");
|
|
154
|
+
body.append(el("pre", cell.kind === "error" ? "text error" : "stdout", cell.text));
|
|
155
|
+
return row;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function commandTable(commands) {
|
|
159
|
+
const { row, body } = line("");
|
|
160
|
+
const table = el("table", "rows");
|
|
161
|
+
for (const [name, description] of Object.entries(commands || {})) {
|
|
162
|
+
const tr = el("tr");
|
|
163
|
+
tr.append(el("td", "name", name));
|
|
164
|
+
tr.append(el("td", null, description));
|
|
165
|
+
table.append(tr);
|
|
166
|
+
}
|
|
167
|
+
body.append(table);
|
|
168
|
+
return row;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function rowTable(cell) {
|
|
172
|
+
const { row, body } = line("");
|
|
173
|
+
const table = el("table", "rows");
|
|
174
|
+
for (const entry of cell.rows || []) {
|
|
175
|
+
const tr = el("tr");
|
|
176
|
+
tr.append(el("td", "name", entry.name));
|
|
177
|
+
const value = el("td", "value");
|
|
178
|
+
if (entry.latex && (state.mode === "typeset" || state.mode === "both")) value.append(typeset(entry.latex));
|
|
179
|
+
else value.textContent = entry.text;
|
|
180
|
+
tr.append(value);
|
|
181
|
+
table.append(tr);
|
|
182
|
+
}
|
|
183
|
+
body.append(table);
|
|
184
|
+
return row;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// /help NAME: the signature, the comment block, and the background and
|
|
188
|
+
// reading that rcas keeps for the name.
|
|
189
|
+
function documentation(cell) {
|
|
190
|
+
const { row, body } = line("");
|
|
191
|
+
const doc = el("div", "doc");
|
|
192
|
+
doc.append(el("h2", null, cell.signature));
|
|
193
|
+
for (const text of cell.lines || []) doc.append(el("p", null, text));
|
|
194
|
+
if (cell.also) doc.append(field("also", cell.also));
|
|
195
|
+
for (const entry of cell.background || []) doc.append(field(entry.label, entry.text));
|
|
196
|
+
for (const source of cell.sources || []) doc.append(field("source", source));
|
|
197
|
+
if (cell.reading && cell.reading.length) {
|
|
198
|
+
const links = el("div", "field");
|
|
199
|
+
links.append(el("b", null, "read: "));
|
|
200
|
+
cell.reading.forEach((href, i) => {
|
|
201
|
+
if (i) links.append(document.createTextNode(", "));
|
|
202
|
+
const a = el("a", null, href.replace(/^https?:\/\/(en\.)?wikipedia\.org\/wiki\//, ""));
|
|
203
|
+
a.href = href;
|
|
204
|
+
a.target = "_blank";
|
|
205
|
+
a.rel = "noopener noreferrer";
|
|
206
|
+
links.append(a);
|
|
207
|
+
});
|
|
208
|
+
doc.append(links);
|
|
209
|
+
}
|
|
210
|
+
if (cell.sections && cell.sections.length) doc.append(field("manual", cell.sections.join("; ")));
|
|
211
|
+
body.append(doc);
|
|
212
|
+
return row;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function field(name, text) {
|
|
216
|
+
const node = el("div", "field");
|
|
217
|
+
node.append(el("b", null, `${name}: `));
|
|
218
|
+
node.append(document.createTextNode(text));
|
|
219
|
+
return node;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---- the session -----------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
// What "auto" actually came to in this window: plots are drawn in Ruby,
|
|
225
|
+
// which cannot see a media query.
|
|
226
|
+
function resolvedTheme() {
|
|
227
|
+
if (state.theme !== "auto") return state.theme;
|
|
228
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function applyState(next) {
|
|
232
|
+
state = Object.assign(state, next || {});
|
|
233
|
+
document.getElementById("version").textContent = state.version || "";
|
|
234
|
+
if (state.theme === "auto") document.documentElement.removeAttribute("data-theme");
|
|
235
|
+
else document.documentElement.setAttribute("data-theme", state.theme);
|
|
236
|
+
promptLabel.textContent = state.numbered ? `In[${state.line}]` : "In";
|
|
237
|
+
if (state.modes && modes.children.length !== state.modes.length) drawModes(state.modes);
|
|
238
|
+
for (const button of modes.children) button.setAttribute("aria-pressed", String(button.dataset.mode === state.mode));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// `typed` is the text as it was when Enter was pressed. Enter first asks
|
|
242
|
+
// the server whether the line is finished, and what is typed while that
|
|
243
|
+
// answer is on its way belongs to the next line, not to this one: reading
|
|
244
|
+
// input.value here submitted "1 + 12" for "1 + 1" followed by a quick "2"
|
|
245
|
+
// (a review, 23 Sept 2026). Text added after the submitted line stays in
|
|
246
|
+
// the field; an edit anywhere else leaves the field as the reader made it.
|
|
247
|
+
async function submit(typed = input.value) {
|
|
248
|
+
const source = typed.replace(/\s+$/, "");
|
|
249
|
+
if (!source.trim()) return;
|
|
250
|
+
const now = input.value;
|
|
251
|
+
input.value = now === typed ? "" : now.startsWith(typed) ? now.slice(typed.length).replace(/^\n/, "") : now;
|
|
252
|
+
autosize();
|
|
253
|
+
hideCompletions();
|
|
254
|
+
history.push(source);
|
|
255
|
+
historyIndex = history.length;
|
|
256
|
+
draft = "";
|
|
257
|
+
try {
|
|
258
|
+
const cell = await api("/api/eval", { source, theme: resolvedTheme() });
|
|
259
|
+
draw(cell, cell.kind === "result" || cell.kind === "error" ? source : undefined);
|
|
260
|
+
if (cell.state) applyState(cell.state);
|
|
261
|
+
else applyState(await api("/api/state", {}));
|
|
262
|
+
} catch (e) {
|
|
263
|
+
draw({ kind: "error", text: `the session is gone: ${e.message}` }, source);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---- input behaviour -------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
function autosize() {
|
|
270
|
+
input.style.height = "auto";
|
|
271
|
+
input.style.height = `${input.scrollHeight}px`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function caretLine() {
|
|
275
|
+
const before = input.value.slice(0, input.selectionStart);
|
|
276
|
+
return { first: !before.includes("\n"), last: !input.value.slice(input.selectionEnd).includes("\n") };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function recall(step) {
|
|
280
|
+
if (historyIndex === history.length) draft = input.value;
|
|
281
|
+
historyIndex = Math.min(Math.max(historyIndex + step, 0), history.length);
|
|
282
|
+
input.value = historyIndex === history.length ? draft : history[historyIndex];
|
|
283
|
+
autosize();
|
|
284
|
+
const end = input.value.length;
|
|
285
|
+
input.setSelectionRange(end, end);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function complete() {
|
|
289
|
+
const before = input.value.slice(0, input.selectionStart);
|
|
290
|
+
const match = before.match(/[\p{L}_][\p{L}\p{N}_]*$/u);
|
|
291
|
+
if (!match) return;
|
|
292
|
+
const { candidates } = await api("/api/complete", { prefix: match[0] });
|
|
293
|
+
if (!candidates || !candidates.length) return hideCompletions();
|
|
294
|
+
const shared = candidates.reduce((a, b) => {
|
|
295
|
+
let i = 0;
|
|
296
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i += 1;
|
|
297
|
+
return a.slice(0, i);
|
|
298
|
+
});
|
|
299
|
+
if (shared.length > match[0].length) insertCompletion(match[0], shared);
|
|
300
|
+
if (candidates.length === 1) return hideCompletions();
|
|
301
|
+
showCompletions(candidates, match[0]);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function insertCompletion(prefix, word) {
|
|
305
|
+
const start = input.selectionStart - prefix.length;
|
|
306
|
+
input.setRangeText(word, start, input.selectionStart, "end");
|
|
307
|
+
autosize();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function showCompletions(candidates, prefix) {
|
|
311
|
+
completions.replaceChildren();
|
|
312
|
+
for (const word of candidates.slice(0, 40)) {
|
|
313
|
+
const button = el("button", null, word);
|
|
314
|
+
button.type = "button";
|
|
315
|
+
button.addEventListener("click", () => {
|
|
316
|
+
const current = input.value.slice(0, input.selectionStart).match(/[\p{L}_][\p{L}\p{N}_]*$/u);
|
|
317
|
+
insertCompletion(current ? current[0] : prefix, word);
|
|
318
|
+
hideCompletions();
|
|
319
|
+
input.focus();
|
|
320
|
+
});
|
|
321
|
+
completions.append(button);
|
|
322
|
+
}
|
|
323
|
+
completions.hidden = false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function hideCompletions() {
|
|
327
|
+
completions.hidden = true;
|
|
328
|
+
completions.replaceChildren();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
input.addEventListener("input", () => {
|
|
332
|
+
autosize();
|
|
333
|
+
if (!completions.hidden) hideCompletions();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Enter never types anything: whether the line is finished has to be asked
|
|
337
|
+
// of Ruby, and preventDefault after an await comes too late to stop the
|
|
338
|
+
// browser inserting the newline first. So the default is cancelled at once
|
|
339
|
+
// and the newline, if the line turns out to be unfinished, is put in here.
|
|
340
|
+
//
|
|
341
|
+
// Only the latest Enter counts: a second one while the first is still
|
|
342
|
+
// being checked makes the first answer stale, and it is dropped.
|
|
343
|
+
let enterCount = 0;
|
|
344
|
+
|
|
345
|
+
async function enter() {
|
|
346
|
+
const source = input.value;
|
|
347
|
+
const mine = ++enterCount;
|
|
348
|
+
if (source.trim() && !source.trim().startsWith("/")) {
|
|
349
|
+
try {
|
|
350
|
+
const { incomplete } = await api("/api/incomplete", { source });
|
|
351
|
+
if (mine !== enterCount) return;
|
|
352
|
+
// unfinished: the newline Enter stood for, unless the reader has
|
|
353
|
+
// typed on in the meantime, which says the line was not done anyway
|
|
354
|
+
if (incomplete) return input.value === source ? newline() : undefined;
|
|
355
|
+
} catch (e) {
|
|
356
|
+
if (mine !== enterCount) return;
|
|
357
|
+
// the server is gone; submit and let that be reported
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
submit(source);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// A line break at the caret, as typing one would have done. setRangeText
|
|
364
|
+
// fires no input event, so the two things that listener does are done here.
|
|
365
|
+
function newline() {
|
|
366
|
+
input.setRangeText("\n", input.selectionStart, input.selectionEnd, "end");
|
|
367
|
+
autosize();
|
|
368
|
+
hideCompletions();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
input.addEventListener("keydown", (event) => {
|
|
372
|
+
// Mid-composition (an IME entering α, or Japanese, or Chinese) Enter and
|
|
373
|
+
// the arrows belong to the IME, not to us.
|
|
374
|
+
if (event.isComposing || event.keyCode === 229) return;
|
|
375
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
376
|
+
event.preventDefault();
|
|
377
|
+
enter();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (event.key === "Tab") {
|
|
381
|
+
event.preventDefault();
|
|
382
|
+
complete();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (event.key === "Enter" && event.shiftKey) {
|
|
386
|
+
event.preventDefault();
|
|
387
|
+
newline();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (event.key === "Escape") return hideCompletions();
|
|
391
|
+
if (event.key === "ArrowUp" && caretLine().first && history.length) {
|
|
392
|
+
event.preventDefault();
|
|
393
|
+
recall(-1);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (event.key === "ArrowDown" && caretLine().last && historyIndex < history.length) {
|
|
397
|
+
event.preventDefault();
|
|
398
|
+
recall(1);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Typing anywhere goes to the input; the worksheet itself is only scrolled.
|
|
403
|
+
document.addEventListener("keydown", (event) => {
|
|
404
|
+
if (event.target === input || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
405
|
+
if (event.key.length === 1 || event.key === "Backspace") input.focus();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// ---- chrome ----------------------------------------------------------------
|
|
409
|
+
|
|
410
|
+
// The buttons are named by Ruby, so the window and /output agree on what
|
|
411
|
+
// each mode is called.
|
|
412
|
+
function drawModes(list) {
|
|
413
|
+
modes.replaceChildren();
|
|
414
|
+
for (const mode of list) {
|
|
415
|
+
const button = el("button", null, mode);
|
|
416
|
+
button.type = "button";
|
|
417
|
+
button.dataset.mode = mode;
|
|
418
|
+
button.addEventListener("click", async () => {
|
|
419
|
+
const cell = await api("/api/eval", { source: `/output ${mode}`, theme: resolvedTheme() });
|
|
420
|
+
if (cell.state) applyState(cell.state);
|
|
421
|
+
});
|
|
422
|
+
modes.append(button);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
document.getElementById("help-button").addEventListener("click", () => {
|
|
427
|
+
input.value = "/help";
|
|
428
|
+
submit();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
for (const button of document.querySelectorAll(".examples button")) {
|
|
432
|
+
button.addEventListener("click", () => {
|
|
433
|
+
input.value = button.textContent;
|
|
434
|
+
autosize();
|
|
435
|
+
input.focus();
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// A reloaded window comes back with the session it had: the values never
|
|
440
|
+
// left Ruby, only the page was thrown away.
|
|
441
|
+
async function restore() {
|
|
442
|
+
applyState(await api("/api/state", {}));
|
|
443
|
+
const { cells } = await api("/api/cells", {});
|
|
444
|
+
for (const cell of cells || []) draw(cell, cell.input);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
restore().catch(() => {});
|
|
448
|
+
autosize();
|
|
449
|
+
input.focus();
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
|
+
<title>rcas</title>
|
|
7
|
+
<link rel="stylesheet" href="/katex/katex.min.css">
|
|
8
|
+
<link rel="stylesheet" href="/app.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<header class="titlebar">
|
|
12
|
+
<img src="/api/logo" alt="" class="logo" onerror="this.remove()">
|
|
13
|
+
<h1>rcas</h1>
|
|
14
|
+
<span class="version" id="version"></span>
|
|
15
|
+
<div class="spacer"></div>
|
|
16
|
+
<nav class="modes" id="modes" aria-label="output mode"></nav>
|
|
17
|
+
<button type="button" class="ghost" id="help-button" title="/help">?</button>
|
|
18
|
+
</header>
|
|
19
|
+
|
|
20
|
+
<main class="worksheet" id="worksheet" tabindex="-1">
|
|
21
|
+
<section class="greeting" id="greeting">
|
|
22
|
+
<p>Symbols are the indeterminates and Ruby operators build the expressions.</p>
|
|
23
|
+
<ul class="examples">
|
|
24
|
+
<li><button type="button">e = (x + 1) * (1 - x)</button></li>
|
|
25
|
+
<li><button type="button">e.expand</button></li>
|
|
26
|
+
<li><button type="button">integrate(1/(x**2 + 1), x)</button></li>
|
|
27
|
+
<li><button type="button">ZZ[x].(x**6 - 1).factor</button></li>
|
|
28
|
+
<li><button type="button">plot(sin(x), x: -PI..PI)</button></li>
|
|
29
|
+
</ul>
|
|
30
|
+
<p class="dim">Enter evaluates, Shift-Enter starts a line, Tab completes, <code>/help</code> explains.</p>
|
|
31
|
+
</section>
|
|
32
|
+
</main>
|
|
33
|
+
|
|
34
|
+
<footer class="composer">
|
|
35
|
+
<div class="completions" id="completions" hidden></div>
|
|
36
|
+
<div class="prompt-row">
|
|
37
|
+
<label class="prompt" id="prompt" for="input">In[1]</label>
|
|
38
|
+
<textarea id="input" rows="1" spellcheck="false" autocapitalize="off" autocomplete="off"
|
|
39
|
+
autocorrect="off" placeholder="an expression, or /help"></textarea>
|
|
40
|
+
</div>
|
|
41
|
+
</footer>
|
|
42
|
+
|
|
43
|
+
<script src="/katex/katex.min.js" onerror="window.katexMissing = true"></script>
|
|
44
|
+
<script src="/app.js"></script>
|
|
45
|
+
</body>
|
|
46
|
+
</html>
|