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.
Files changed (117) hide show
  1. checksums.yaml +7 -0
  2. data/CITATION.cff +17 -0
  3. data/DESIGN.md +783 -0
  4. data/LICENSE +21 -0
  5. data/MANUAL.md +6265 -0
  6. data/README.md +267 -0
  7. data/bin/rcas +9 -0
  8. data/bin/rcas-app +9 -0
  9. data/bin/rcas-chat +9 -0
  10. data/lib/rcas/algebraic.rb +481 -0
  11. data/lib/rcas/analysis.rb +966 -0
  12. data/lib/rcas/app/launcher.rb +203 -0
  13. data/lib/rcas/app/public/app.css +402 -0
  14. data/lib/rcas/app/public/app.js +449 -0
  15. data/lib/rcas/app/public/index.html +46 -0
  16. data/lib/rcas/app/server.rb +220 -0
  17. data/lib/rcas/app/window.rb +94 -0
  18. data/lib/rcas/app/worksheet.rb +290 -0
  19. data/lib/rcas/app.rb +168 -0
  20. data/lib/rcas/background.rb +758 -0
  21. data/lib/rcas/chat/assistant.rb +199 -0
  22. data/lib/rcas/chat/picker.rb +164 -0
  23. data/lib/rcas/chat/repl.rb +583 -0
  24. data/lib/rcas/chat/session.rb +137 -0
  25. data/lib/rcas/chat/settings.rb +71 -0
  26. data/lib/rcas/chat/style.rb +30 -0
  27. data/lib/rcas/chat/tool.rb +53 -0
  28. data/lib/rcas/chat/ui.rb +316 -0
  29. data/lib/rcas/chat/usage.rb +62 -0
  30. data/lib/rcas/chat/workspace.rb +132 -0
  31. data/lib/rcas/chat.rb +54 -0
  32. data/lib/rcas/coefficients.rb +170 -0
  33. data/lib/rcas/combinatorics.rb +274 -0
  34. data/lib/rcas/complex_parts.rb +160 -0
  35. data/lib/rcas/constants.rb +129 -0
  36. data/lib/rcas/core_ext.rb +35 -0
  37. data/lib/rcas/decide.rb +501 -0
  38. data/lib/rcas/decompositions.rb +241 -0
  39. data/lib/rcas/differentiate.rb +144 -0
  40. data/lib/rcas/discussion.rb +558 -0
  41. data/lib/rcas/distributions.rb +980 -0
  42. data/lib/rcas/dixon.rb +95 -0
  43. data/lib/rcas/docs.rb +321 -0
  44. data/lib/rcas/domains.rb +728 -0
  45. data/lib/rcas/expand.rb +174 -0
  46. data/lib/rcas/expression.rb +613 -0
  47. data/lib/rcas/factor.rb +605 -0
  48. data/lib/rcas/finite_field.rb +577 -0
  49. data/lib/rcas/fourier.rb +118 -0
  50. data/lib/rcas/fps.rb +678 -0
  51. data/lib/rcas/fraction.rb +126 -0
  52. data/lib/rcas/functions.rb +1136 -0
  53. data/lib/rcas/gcd.rb +112 -0
  54. data/lib/rcas/geometry.rb +266 -0
  55. data/lib/rcas/groebner.rb +162 -0
  56. data/lib/rcas/hold.rb +277 -0
  57. data/lib/rcas/hypothesis.rb +364 -0
  58. data/lib/rcas/inequalities.rb +689 -0
  59. data/lib/rcas/integral_functions.rb +260 -0
  60. data/lib/rcas/integrate.rb +1589 -0
  61. data/lib/rcas/integrate_substitutions.rb +434 -0
  62. data/lib/rcas/interpolate.rb +40 -0
  63. data/lib/rcas/irb.rb +146 -0
  64. data/lib/rcas/laplace.rb +159 -0
  65. data/lib/rcas/latex.rb +556 -0
  66. data/lib/rcas/lattice.rb +172 -0
  67. data/lib/rcas/linear_algebra.rb +117 -0
  68. data/lib/rcas/linear_program.rb +416 -0
  69. data/lib/rcas/lint.rb +79 -0
  70. data/lib/rcas/matrix.rb +531 -0
  71. data/lib/rcas/matrix_multiply.rb +202 -0
  72. data/lib/rcas/multimodular.rb +286 -0
  73. data/lib/rcas/named_polynomials.rb +274 -0
  74. data/lib/rcas/number_theory.rb +443 -0
  75. data/lib/rcas/numerics.rb +825 -0
  76. data/lib/rcas/ode.rb +488 -0
  77. data/lib/rcas/openmath/objects.rb +364 -0
  78. data/lib/rcas/openmath/phrasebook.rb +551 -0
  79. data/lib/rcas/openmath/popcorn.rb +518 -0
  80. data/lib/rcas/openmath/xml.rb +309 -0
  81. data/lib/rcas/openmath.rb +49 -0
  82. data/lib/rcas/petkovsek.rb +165 -0
  83. data/lib/rcas/piecewise.rb +488 -0
  84. data/lib/rcas/plot.rb +763 -0
  85. data/lib/rcas/plot3d.rb +419 -0
  86. data/lib/rcas/poly_matrix.rb +318 -0
  87. data/lib/rcas/poly_recurrence.rb +117 -0
  88. data/lib/rcas/polynomial.rb +466 -0
  89. data/lib/rcas/precision.rb +925 -0
  90. data/lib/rcas/printer.rb +150 -0
  91. data/lib/rcas/product.rb +155 -0
  92. data/lib/rcas/q_difference.rb +296 -0
  93. data/lib/rcas/q_functions.rb +158 -0
  94. data/lib/rcas/q_summation.rb +308 -0
  95. data/lib/rcas/q_zeilberger.rb +199 -0
  96. data/lib/rcas/random.rb +506 -0
  97. data/lib/rcas/rational_function.rb +186 -0
  98. data/lib/rcas/recurrence.rb +323 -0
  99. data/lib/rcas/render.rb +431 -0
  100. data/lib/rcas/results.rb +192 -0
  101. data/lib/rcas/scalar.rb +219 -0
  102. data/lib/rcas/series.rb +726 -0
  103. data/lib/rcas/simplify.rb +649 -0
  104. data/lib/rcas/solve.rb +2002 -0
  105. data/lib/rcas/special.rb +163 -0
  106. data/lib/rcas/statistics.rb +175 -0
  107. data/lib/rcas/steps.rb +835 -0
  108. data/lib/rcas/summation.rb +532 -0
  109. data/lib/rcas/trig.rb +264 -0
  110. data/lib/rcas/van_hoeij.rb +241 -0
  111. data/lib/rcas/vector.rb +175 -0
  112. data/lib/rcas/vector_calculus.rb +411 -0
  113. data/lib/rcas/version.rb +5 -0
  114. data/lib/rcas/zeilberger.rb +358 -0
  115. data/lib/rcas.rb +91 -0
  116. data/package.json +8 -0
  117. 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>