@basthon/console-react 0.1.1 → 0.1.2

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 (41) hide show
  1. package/lib/components/app.d.ts +15 -0
  2. package/lib/components/app.js +44 -0
  3. package/lib/components/basthon.svg +62 -0
  4. package/lib/components/button.d.ts +7 -0
  5. package/lib/components/button.js +9 -0
  6. package/lib/components/editor.d.ts +11 -0
  7. package/lib/components/editor.js +60 -0
  8. package/lib/components/footer.d.ts +7 -0
  9. package/lib/components/footer.js +8 -0
  10. package/lib/components/graphics.d.ts +6 -0
  11. package/lib/components/graphics.js +51 -0
  12. package/lib/components/header.d.ts +9 -0
  13. package/lib/components/header.js +15 -0
  14. package/lib/components/main-container.d.ts +14 -0
  15. package/lib/components/main-container.js +25 -0
  16. package/lib/components/styles.module.scss +394 -0
  17. package/lib/components/terminal.d.ts +5 -0
  18. package/lib/components/terminal.js +41 -0
  19. package/lib/gui/ace.d.ts +4 -0
  20. package/lib/gui/ace.js +17 -0
  21. package/lib/gui/editor.d.ts +35 -0
  22. package/lib/gui/editor.js +113 -0
  23. package/lib/gui/graphics.d.ts +44 -0
  24. package/lib/gui/graphics.js +185 -0
  25. package/lib/gui/gui.d.ts +87 -0
  26. package/lib/gui/gui.js +188 -0
  27. package/lib/gui/terminal.d.ts +67 -0
  28. package/lib/gui/terminal.js +242 -0
  29. package/lib/hooks/async-effect.d.ts +7 -0
  30. package/lib/hooks/async-effect.js +17 -0
  31. package/lib/hooks/basthon.d.ts +16 -0
  32. package/lib/hooks/basthon.js +55 -0
  33. package/lib/hooks/dispatch.d.ts +4 -0
  34. package/lib/hooks/dispatch.js +4 -0
  35. package/lib/hooks/gui.d.ts +3 -0
  36. package/lib/hooks/gui.js +3 -0
  37. package/lib/index.d.ts +2 -0
  38. package/lib/index.js +2 -0
  39. package/lib/state-manager.d.ts +38 -0
  40. package/lib/state-manager.js +101 -0
  41. package/package.json +1 -1
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Fix Matplotlib icons using Fontawesome not loaded globaly.
3
+ */
4
+ const fixMatplotlibIcons = async (element) => {
5
+ if (element.getAttribute("id")?.startsWith("matplotlib_")) {
6
+ const fa = await import("@fortawesome/fontawesome-svg-core");
7
+ const fas = await import("@fortawesome/free-solid-svg-icons");
8
+ fa.library.add(fas.faHome, fas.faArrowLeft, fas.faArrowRight, fas.faArrows, fas.faSearchPlus);
9
+ for (const button of element.querySelectorAll(".matplotlib-toolbar-button")) {
10
+ button.classList.remove("fa");
11
+ const faClass = Array.from(button.classList).find((c) => c.startsWith("fa-"));
12
+ if (faClass != null) {
13
+ const icon = fa.parse.icon(faClass);
14
+ const node = fa.icon(icon)?.node?.[0];
15
+ if (node != null)
16
+ button.appendChild(node);
17
+ }
18
+ }
19
+ }
20
+ };
21
+ /**
22
+ * Dedicated class to manage action not managed by react.
23
+ */
24
+ export default class Graphics {
25
+ constructor(element, kernel, onContentUpdate) {
26
+ // array of destroy callbacks
27
+ this._destroyCb = [];
28
+ this._elem = element;
29
+ this._onContentUpdate = onContentUpdate;
30
+ const callback = this.display.bind(this);
31
+ kernel.addEventListener("eval.display", callback);
32
+ // remember to remove listeners on destroy
33
+ this._destroyCb.push(() => kernel.removeEventListener("eval.display", callback));
34
+ }
35
+ /**
36
+ * Set the dynamic IFrame handler called on each new dynamic iframe.
37
+ */
38
+ setDynamicIFrameHandler(handler) {
39
+ this._handleDynamicIFrame = handler;
40
+ }
41
+ /**
42
+ * Same as that.innerHTML = html but with script execution support.
43
+ */
44
+ setInnerHTML(html, runScripts = false) {
45
+ // FIXME: sanitize input html
46
+ if (this._elem == null)
47
+ return;
48
+ this._elem.innerHTML = html;
49
+ if (!runScripts)
50
+ return;
51
+ for (const oldScript of this._elem.querySelectorAll("script")) {
52
+ const newScript = document.createElement("script");
53
+ for (const attr of oldScript.attributes)
54
+ newScript.setAttribute(attr.name, attr.value);
55
+ newScript.appendChild(document.createTextNode(oldScript.innerHTML));
56
+ oldScript.parentNode?.replaceChild(newScript, oldScript);
57
+ }
58
+ }
59
+ /**
60
+ * CSS transform the first child element to fit the view.
61
+ */
62
+ fitContent() {
63
+ for (const child of this._elem.children) {
64
+ if (!(child instanceof HTMLElement))
65
+ continue;
66
+ if (["DIV", "IFRAME"].includes(child.tagName)) {
67
+ const ratio = Math.min((this._elem.offsetWidth - 5) / child.scrollWidth, (this._elem.offsetHeight - 5) / child.scrollHeight, 1);
68
+ child.style.transform = `scale(${ratio})`;
69
+ }
70
+ // p5 specific case
71
+ if (child.tagName === "DIV") {
72
+ const child2 = child.firstElementChild;
73
+ if (child2.tagName === "CANVAS" &&
74
+ child2.classList.contains("p5Canvas")) {
75
+ child.style.position = "relative";
76
+ child.style.width = "99%";
77
+ child.style.height = "99%";
78
+ child.style.transform = "unset";
79
+ child2.style.position = "absolute";
80
+ child2.style.top = "50%";
81
+ child2.style.left = "50%";
82
+ child2.style.transform = "translate(-50%, -50%)";
83
+ child2.style.maxWidth = "100%";
84
+ child2.style.maxHeight = "100%";
85
+ child2.style.width = "unset";
86
+ child2.style.height = "unset";
87
+ }
88
+ else if (child2.tagName === "VIDEO") {
89
+ child.style.height = "100%";
90
+ child2.style.width = "100%";
91
+ child2.style.height = "100%";
92
+ }
93
+ }
94
+ }
95
+ }
96
+ /**
97
+ * To be called when resized.
98
+ */
99
+ onResize() {
100
+ this.fitContent();
101
+ }
102
+ /**
103
+ * Display an element from Basthon data.
104
+ */
105
+ async display(data) {
106
+ this.clean();
107
+ switch (data.display_type) {
108
+ case "dom-node":
109
+ const elem = data.content;
110
+ await fixMatplotlibIcons(elem);
111
+ this._elem?.appendChild(elem);
112
+ break;
113
+ case "multiple":
114
+ /* typically dispached by display() */
115
+ for (let mime of [
116
+ "image/svg+xml",
117
+ "image/png",
118
+ "text/latex",
119
+ "text/html",
120
+ "text/dynamic-iframe",
121
+ "text/plain",
122
+ ]) {
123
+ if (mime in data.content) {
124
+ let content = data.content[mime];
125
+ switch (mime) {
126
+ case "image/png":
127
+ content = `<img src="data:image/png;base64,${content}">`;
128
+ break;
129
+ case "text/dynamic-iframe":
130
+ const dataURL = `data:text/html;charset=utf-8,${encodeURIComponent(content)}`;
131
+ content = `<iframe allowFullscreen class="basthon-dynamic-iframe" frameborder="0" src="${dataURL}"></iframe>`;
132
+ break;
133
+ case "text/latex":
134
+ if (window.MathJax != null)
135
+ break;
136
+ // dynamically loading MathJax
137
+ console.log("Loading MathJax (LaTeX expression needs it).");
138
+ await new Promise((resolve, reject) => {
139
+ const script = document.createElement("script");
140
+ script.type = "text/javascript";
141
+ script.onload = resolve;
142
+ script.onerror = reject;
143
+ script.src =
144
+ "https://cdn.jsdelivr.net/npm/mathjax@3.0.5/es5/tex-mml-chtml.js";
145
+ document.getElementsByTagName("head")[0].appendChild(script);
146
+ });
147
+ break;
148
+ default:
149
+ break;
150
+ }
151
+ this.setInnerHTML(content, true);
152
+ if (mime === "text/latex")
153
+ window.MathJax?.typeset?.();
154
+ if (mime === "text/dynamic-iframe") {
155
+ const elem = this._elem.firstElementChild;
156
+ const o = new ResizeObserver(() => this.fitContent());
157
+ o.observe(elem);
158
+ this._handleDynamicIFrame?.(elem);
159
+ }
160
+ break;
161
+ }
162
+ }
163
+ break;
164
+ default:
165
+ console.error(`Not supported node type '${data.display_type}' in eval.display result processing.`);
166
+ }
167
+ // update size
168
+ this.fitContent();
169
+ this._onContentUpdate();
170
+ }
171
+ /**
172
+ * Clean graphics.
173
+ */
174
+ clean() {
175
+ this._elem.textContent = "";
176
+ }
177
+ /**
178
+ * Destroy all created by graphics.
179
+ */
180
+ destroy() {
181
+ this._destroyCb.forEach((cb) => cb());
182
+ this._destroyCb = [];
183
+ this.clean();
184
+ }
185
+ }
@@ -0,0 +1,87 @@
1
+ import "notie/dist/notie.css";
2
+ import "@fontsource/open-sans";
3
+ import { GUIBase, GUIOptions } from "@basthon/gui-base";
4
+ import Editor from "./editor";
5
+ import Terminal from "./terminal";
6
+ import Graphics from "./graphics";
7
+ export default class GUI extends GUIBase {
8
+ private _editor?;
9
+ private _term?;
10
+ private _graphics?;
11
+ constructor(options: GUIOptions);
12
+ /**
13
+ * Set editor.
14
+ */
15
+ setEditor(editor: Editor): void;
16
+ /**
17
+ * Set terminal.
18
+ */
19
+ setTerminal(term: Terminal): void;
20
+ /**
21
+ * Set graphics.
22
+ */
23
+ setGraphics(graphics: Graphics): void;
24
+ /**
25
+ * get editor.
26
+ */
27
+ get editor(): Editor | undefined;
28
+ /**
29
+ * get terminal.
30
+ */
31
+ terminal(): Terminal | undefined;
32
+ /**
33
+ * get graphics.
34
+ */
35
+ get graphics(): Graphics | undefined;
36
+ /**
37
+ * Get editor's content.
38
+ */
39
+ content(): string;
40
+ /**
41
+ * Set editor's content.
42
+ */
43
+ setContent(content: string): void;
44
+ /**
45
+ * Notify the user with an error.
46
+ */
47
+ error(title: string, message: string): void;
48
+ /**
49
+ * Notify the user.
50
+ */
51
+ info(title: string, message: string): void;
52
+ /**
53
+ * Ask the user to confirm or cancel.
54
+ */
55
+ confirm(title: string, message: string, text: string, callback: () => void, textCancel: string, callbackCancel: () => void): void;
56
+ /**
57
+ * Ask the user to select a choice.
58
+ */
59
+ select(title: string, message: string, choices: {
60
+ text: string;
61
+ handler: () => void;
62
+ }[], textCancel: string, callbackCancel: () => void): void;
63
+ /**
64
+ * Rolling back to previous version
65
+ */
66
+ rollback(): Promise<void>;
67
+ protected setupUI(options: any): Promise<void>;
68
+ /**
69
+ * Run the editor script in the terminal.
70
+ */
71
+ runScript(): Promise<void>;
72
+ /**
73
+ * RAZ function.
74
+ */
75
+ restart(): void;
76
+ /**
77
+ * Open *.py file by asking user what to do:
78
+ * load in editor or put on (emulated) local filesystem.
79
+ */
80
+ openModuleFile(file: File): Promise<void>;
81
+ /**
82
+ * Opening file: If it has kernel extension, loading it in the editor
83
+ * or put on (emulated) local filesystem (user is asked to),
84
+ * otherwise, loading it in the local filesystem.
85
+ */
86
+ openFile(): Promise<void>;
87
+ }
package/lib/gui/gui.js ADDED
@@ -0,0 +1,188 @@
1
+ // notie
2
+ import * as notie from "notie";
3
+ import "notie/dist/notie.css";
4
+ // open-sans
5
+ import "@fontsource/open-sans";
6
+ // basthon
7
+ import { GUIBase } from "@basthon/gui-base";
8
+ export default class GUI extends GUIBase {
9
+ constructor(options) {
10
+ super({ ...options, uiName: "console" });
11
+ this._urlKey = "script";
12
+ this._contentFilename = "script";
13
+ /* when kernel is available, set contentFilename extension */
14
+ this.kernelLoader.kernelAvailable().then((kernel) => {
15
+ const exts = kernel.moduleExts();
16
+ if (exts.length)
17
+ this._contentFilename += "." + exts[0];
18
+ });
19
+ }
20
+ /**
21
+ * Set editor.
22
+ */
23
+ setEditor(editor) {
24
+ this._editor = editor;
25
+ }
26
+ /**
27
+ * Set terminal.
28
+ */
29
+ setTerminal(term) {
30
+ this._term = term;
31
+ }
32
+ /**
33
+ * Set graphics.
34
+ */
35
+ setGraphics(graphics) {
36
+ graphics.setDynamicIFrameHandler(this.handleDynamicIFrame.bind(this));
37
+ this._graphics = graphics;
38
+ }
39
+ /**
40
+ * get editor.
41
+ */
42
+ get editor() {
43
+ return this._editor;
44
+ }
45
+ /**
46
+ * get terminal.
47
+ */
48
+ terminal() {
49
+ return this._term;
50
+ }
51
+ /**
52
+ * get graphics.
53
+ */
54
+ get graphics() {
55
+ return this._graphics;
56
+ }
57
+ /**
58
+ * Get editor's content.
59
+ */
60
+ content() {
61
+ return this._editor?.getContent();
62
+ }
63
+ /**
64
+ * Set editor's content.
65
+ */
66
+ setContent(content) {
67
+ this._editor?.setContent(content);
68
+ }
69
+ /**
70
+ * Notify the user with an error.
71
+ */
72
+ error(title, message) {
73
+ notie.alert({
74
+ type: "error",
75
+ text: message,
76
+ stay: false,
77
+ time: 3,
78
+ position: "top",
79
+ });
80
+ }
81
+ /**
82
+ * Notify the user.
83
+ */
84
+ info(title, message) {
85
+ notie.alert({
86
+ type: "success",
87
+ text: message,
88
+ stay: false,
89
+ time: 3,
90
+ position: "top",
91
+ });
92
+ }
93
+ /**
94
+ * Ask the user to confirm or cancel.
95
+ */
96
+ confirm(title, message, text, callback, textCancel, callbackCancel) {
97
+ notie.confirm({
98
+ text: message,
99
+ submitText: text,
100
+ cancelText: textCancel,
101
+ position: "top",
102
+ submitCallback: callback,
103
+ cancelCallback: callbackCancel,
104
+ });
105
+ }
106
+ /**
107
+ * Ask the user to select a choice.
108
+ */
109
+ select(title, message, choices, textCancel, callbackCancel) {
110
+ let count = 0;
111
+ notie.select({
112
+ text: message,
113
+ cancelText: textCancel,
114
+ position: "top",
115
+ choices: choices.map((c) => ({
116
+ text: c.text,
117
+ type: count++ % 2 ? "error" : "success",
118
+ handler: c.handler || (() => { }),
119
+ })),
120
+ cancelCallback: callbackCancel || (() => { }),
121
+ });
122
+ }
123
+ /**
124
+ * Rolling back to previous version
125
+ */
126
+ async rollback() {
127
+ const content = await this.selectCheckpoint();
128
+ if (content != null)
129
+ this.setContent(content);
130
+ }
131
+ async setupUI(options) {
132
+ await super.setupUI(options);
133
+ /* binding ctrl+s */
134
+ window.addEventListener("keydown", (event) => {
135
+ if ((event.ctrlKey || event.metaKey) &&
136
+ String.fromCharCode(event.which).toLowerCase() === "s") {
137
+ event.preventDefault();
138
+ this.download();
139
+ }
140
+ });
141
+ /* backup before closing */
142
+ window.onbeforeunload = () => {
143
+ this.backup().catch(this.notifyError.bind(this));
144
+ };
145
+ }
146
+ /**
147
+ * Run the editor script in the terminal.
148
+ */
149
+ async runScript() {
150
+ await this.loaded();
151
+ const src = this.content();
152
+ this._term?.launch(src, false);
153
+ this._term?.focus();
154
+ }
155
+ /**
156
+ * RAZ function.
157
+ */
158
+ restart() {
159
+ this.kernelRestart();
160
+ this._term?.clear();
161
+ this._graphics?.clean();
162
+ }
163
+ /**
164
+ * Open *.py file by asking user what to do:
165
+ * load in editor or put on (emulated) local filesystem.
166
+ */
167
+ async openModuleFile(file) {
168
+ this.confirm("", `Que faire de ${file.name} ?`, "Charger dans l'éditeur", () => {
169
+ this.open(file);
170
+ }, "Installer le module", () => {
171
+ this.putFSRessource(file);
172
+ });
173
+ }
174
+ /**
175
+ * Opening file: If it has kernel extension, loading it in the editor
176
+ * or put on (emulated) local filesystem (user is asked to),
177
+ * otherwise, loading it in the local filesystem.
178
+ */
179
+ async openFile() {
180
+ await this.kernelLoader.kernelAvailable();
181
+ const callback = this.openModuleFile.bind(this);
182
+ const callbacks = {};
183
+ this.kernelSafe?.moduleExts().forEach((ext) => {
184
+ callbacks[ext] = callback;
185
+ });
186
+ return await this._openFile(callbacks);
187
+ }
188
+ }
@@ -0,0 +1,67 @@
1
+ import "jquery.terminal";
2
+ import "jquery.terminal/css/jquery.terminal.css";
3
+ import { KernelBase } from "@basthon/kernel-base";
4
+ /**
5
+ * Wrapper class arround JQTerminal.
6
+ */
7
+ export default class Terminal {
8
+ private _kernel;
9
+ private _elem;
10
+ private _term;
11
+ private _prompt_tail?;
12
+ private _buffer;
13
+ private _inputMode;
14
+ private _inputResolve?;
15
+ private _destroyCb;
16
+ constructor(element: HTMLElement, kernel: KernelBase);
17
+ /**
18
+ * Mimic CPython's REPL banner.
19
+ */
20
+ banner(): string;
21
+ /**
22
+ * Reset the multiline input buffer.
23
+ */
24
+ reset_buffer(): void;
25
+ private _interpreter;
26
+ /**
27
+ * Force waiting for input to end.
28
+ */
29
+ private _stopWaitInput;
30
+ /**
31
+ * Print a string in the terminal.
32
+ */
33
+ echo(message: string): void;
34
+ /**
35
+ * Print error in terminal (in red).
36
+ */
37
+ error(message: string): JQueryTerminal<HTMLElement>;
38
+ /**
39
+ * Pause the terminal execution (usefull to wait for event or promise).
40
+ */
41
+ pause(): void;
42
+ /**
43
+ * Resume the terminal execution (see `pause`).
44
+ */
45
+ wakeup(): void;
46
+ /**
47
+ * Recover the terminal as it was at start.
48
+ */
49
+ clear(): void;
50
+ /**
51
+ * Launch code in terminal.
52
+ */
53
+ launch(code: string, interactive?: boolean): void;
54
+ /**
55
+ * Give focus to terminal.
56
+ */
57
+ focus(): void;
58
+ /**
59
+ * Show script in terminal with content as dropdown
60
+ * (should improve readability).
61
+ */
62
+ showScript(code: string): void;
63
+ /**
64
+ * Destroy all created by terminal.
65
+ */
66
+ destroy(): void;
67
+ }