Skip to content

Commit 91ba55c

Browse files
committed
ファイルを実行できるようになった
1 parent c5e812e commit 91ba55c

12 files changed

Lines changed: 625 additions & 406 deletions

File tree

app/[docs_id]/markdown.tsx

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import Markdown, { Components } from "react-markdown";
22
import remarkGfm from "remark-gfm";
33
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
4-
import { PythonEmbeddedTerminal, PythonExecFile } from "../terminal/python/embedded";
4+
import { PythonEmbeddedTerminal } from "../terminal/python/embedded";
55
import { Heading } from "./section";
66
import { EditorComponent } from "../terminal/editor";
7+
import { ExecFile } from "../terminal/exec";
78

89
export function StyledMarkdown({ content }: { content: string }) {
910
return (
@@ -36,9 +37,11 @@ const components: Components = {
3637
hr: ({ node, ...props }) => <hr className="border-primary my-4" {...props} />,
3738
pre: ({ node, ...props }) => props.children,
3839
code: ({ node, className, ref, style, ...props }) => {
39-
const match = /^language-(\w+)(-repl|-exec)?\:?(.+)?$/.exec(className || "");
40+
const match = /^language-(\w+)(-repl|-exec)?\:?(.+)?$/.exec(
41+
className || ""
42+
);
4043
if (match) {
41-
if(match[2] === "-exec" && match[3]) {
44+
if (match[2] === "-exec" && match[3]) {
4245
/*
4346
```python-exec:main.py
4447
hello, world!
@@ -49,18 +52,16 @@ const components: Components = {
4952
hello, world!
5053
---------------------------
5154
*/
52-
switch(match[1]){
53-
case "python":
54-
return (
55-
<div className="border border-primary m-2 rounded-lg">
56-
<PythonExecFile filename={match[3]} content={String(props.children).replace(/\n$/, "")} />
57-
</div>
58-
);
59-
default:
60-
console.warn(`Unsupported language for exec: ${match[1]}`);
61-
break;
62-
}
63-
}else if (match[3]) {
55+
return (
56+
<div className="border border-primary m-2 rounded-lg">
57+
<ExecFile
58+
language={match[1]}
59+
filename={match[3]}
60+
content={String(props.children).replace(/\n$/, "")}
61+
/>
62+
</div>
63+
);
64+
} else if (match[3]) {
6465
// ファイル名指定がある場合、ファイルエディター
6566
// 現状はPythonのみ対応
6667
switch (match[1]) {
@@ -79,7 +80,7 @@ const components: Components = {
7980
console.warn(`Unsupported language for editor: ${match[1]}`);
8081
break;
8182
}
82-
}else if (match[2] === "-repl") {
83+
} else if (match[2] === "-repl") {
8384
// repl付きの言語指定
8485
// 現状はPythonのみ対応
8586
switch (match[1]) {

app/layout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ export default function RootLayout({
2121
<input id="drawer-toggle" type="checkbox" className="drawer-toggle" />
2222
<div className="drawer-content flex flex-col">
2323
<Navbar />
24-
<PyodideProvider>
25-
<FileProvider>{children}</FileProvider>
26-
</PyodideProvider>
24+
<FileProvider>
25+
<PyodideProvider>{children}</PyodideProvider>
26+
</FileProvider>
2727
</div>
2828
<div className="drawer-side shadow-md">
2929
<label

app/terminal/editor.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.embedded-editor > .ace_gutter {
2+
border-bottom-left-radius: 0.5rem;
3+
}
4+
.embedded-editor > .ace_scroller {
5+
border-bottom-right-radius: 0.5rem;
6+
border-top-right-radius: 0.5rem;
7+
}
8+
.embedded-editor > .ace_editor {
9+
border-bottom-left-radius: 0.5rem;
10+
border-bottom-right-radius: 0.5rem;
11+
border-top-right-radius: 0.5rem;
12+
}

app/terminal/editor.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
import { useFile } from "./file";
44
import AceEditor from "react-ace";
5+
import "./editor.css";
56
// テーマは色分けが今のTerminal側のハイライト(highlight.js)の実装に近いものを適当に選んだ
67
import "ace-builds/src-min-noconflict/theme-tomorrow";
78
import "ace-builds/src-min-noconflict/theme-twilight";
89
import "ace-builds/src-min-noconflict/ext-language_tools";
910
import "ace-builds/src-min-noconflict/ext-searchbox";
1011
import "ace-builds/src-min-noconflict/mode-python";
12+
import { useEffect } from "react";
1113
// snippetを有効化するにはsnippetもimportする必要がある: import "ace-builds/src-min-noconflict/snippets/python";
1214

1315
interface EditorProps {
@@ -19,10 +21,15 @@ interface EditorProps {
1921
export function EditorComponent(props: EditorProps) {
2022
const { files, writeFile } = useFile();
2123
const code = files[props.filename] || props.initContent;
24+
useEffect(() => {
25+
if (!files[props.filename]) {
26+
writeFile(props.filename, props.initContent);
27+
}
28+
}, [files, props.filename, props.initContent, writeFile]);
2229

2330
return (
24-
<div>
25-
<div className="font-mono text-sm mt-2 ml-4 ">{props.filename}</div>
31+
<div className="embedded-editor">
32+
<div className="font-mono text-sm mt-2 mb-1 ml-4 ">{props.filename}</div>
2633
<AceEditor
2734
name={`ace-editor-${props.filename}`}
2835
mode={props.language}

app/terminal/exec.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"use client";
2+
3+
import chalk from "chalk";
4+
import { usePyodide } from "./python/pyodide";
5+
import { clearTerminal, getRows, useTerminal } from "./terminal";
6+
7+
interface ExecProps {
8+
filename: string;
9+
language: string;
10+
content: string;
11+
}
12+
export function ExecFile(props: ExecProps) {
13+
const { terminalRef, terminalInstanceRef, termReady } = useTerminal({
14+
getRows: (cols: number) => getRows(props.content, cols),
15+
onReady: () => {
16+
// カーソル非表示
17+
terminalInstanceRef.current!.write("\x1b[?25l");
18+
for (const line of props.content.split("\n")) {
19+
terminalInstanceRef.current!.writeln(line);
20+
}
21+
},
22+
});
23+
24+
const pyodide = usePyodide();
25+
26+
let commandline: string;
27+
let exec: () => Promise<void> | void;
28+
let runtimeInitializing: boolean;
29+
switch (props.language) {
30+
case "python":
31+
commandline = `python ${props.filename}`;
32+
runtimeInitializing = pyodide.initializing;
33+
exec = async () => {
34+
if (!pyodide.ready) {
35+
clearTerminal(terminalInstanceRef.current!);
36+
terminalInstanceRef.current!.write(
37+
chalk.dim.bold.italic("(初期化しています...しばらくお待ちください)")
38+
);
39+
await pyodide.init();
40+
}
41+
clearTerminal(terminalInstanceRef.current!);
42+
const outputs = await pyodide.runFile(props.filename);
43+
for (const output of outputs) {
44+
// 出力内容に応じて色を変える
45+
const message = String(output.message).replace(/\n/g, "\r\n");
46+
switch (output.type) {
47+
case "error":
48+
terminalInstanceRef.current!.writeln(chalk.red(message));
49+
break;
50+
default:
51+
terminalInstanceRef.current!.writeln(message);
52+
break;
53+
}
54+
}
55+
};
56+
break;
57+
default:
58+
commandline = `エラー: 非対応の言語 ${props.language}`;
59+
runtimeInitializing = false;
60+
exec = () => undefined;
61+
break;
62+
}
63+
return (
64+
<div className="relative">
65+
<div>
66+
<button
67+
className="btn btn-soft btn-primary rounded-tl-lg rounded-none"
68+
onClick={exec}
69+
disabled={!termReady || runtimeInitializing}
70+
>
71+
▶ 実行
72+
</button>
73+
<code className="text-sm ml-4">{commandline}</code>
74+
</div>
75+
<div className="bg-base-300 p-4 pt-2 rounded-b-lg">
76+
<div ref={terminalRef} />
77+
</div>
78+
{runtimeInitializing && (
79+
<div className="absolute z-10 inset-0 cursor-wait" />
80+
)}
81+
</div>
82+
);
83+
}

app/terminal/python/embedded.tsx

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,15 @@
11
"use client";
22

33
import { useMemo } from "react";
4-
import { TerminalComponent, TerminalOutput } from "../terminal";
4+
import { ReplTerminal, ReplOutput } from "../repl";
55
import { usePyodide } from "./pyodide";
6-
import { useFile } from "../file";
7-
8-
interface ExecProps {
9-
filename: string;
10-
content: string;
11-
}
12-
export function PythonExecFile(props: ExecProps){
13-
const {files} = useFile();
14-
15-
return <>
16-
<div>
17-
<button className="btn btn-soft btn-primary">
18-
▶ 実行
19-
</button>
20-
<code className="text-sm">
21-
python {props.filename}
22-
</code>
23-
</div>
24-
</>
25-
}
266

277
export function PythonEmbeddedTerminal({ content }: { content: string }) {
288
const initCommands = useMemo(() => splitContents(content), [content]);
299
const { init, initializing, ready, runPython, checkSyntax, mutex } = usePyodide();
3010

3111
return (
32-
<TerminalComponent
12+
<ReplTerminal
3313
initRuntime={init}
3414
runtimeInitializing={initializing}
3515
runtimeReady={ready}
@@ -47,8 +27,8 @@ export function PythonEmbeddedTerminal({ content }: { content: string }) {
4727

4828
function splitContents(
4929
contents: string
50-
): { command: string; output: TerminalOutput[] }[] {
51-
const initCommands: { command: string; output: TerminalOutput[] }[] = [];
30+
): { command: string; output: ReplOutput[] }[] {
31+
const initCommands: { command: string; output: ReplOutput[] }[] = [];
5232
for (const line of contents.split("\n")) {
5333
if (line.startsWith(">>> ")) {
5434
// Remove the prompt from the command

app/terminal/python/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
"use client";
22

33
import { EditorComponent } from "../editor";
4-
import { TerminalComponent } from "../terminal";
4+
import { ExecFile } from "../exec";
5+
import { ReplTerminal } from "../repl";
56
import { usePyodide } from "./pyodide";
67

78
export default function PythonPage() {
89
const { init, ready, initializing, runPython, checkSyntax, mutex } =
910
usePyodide();
1011
return (
1112
<div className="p-4 flex flex-col gap-4">
12-
<TerminalComponent
13+
<ReplTerminal
1314
initRuntime={init}
1415
runtimeInitializing={initializing}
1516
runtimeReady={ready}
@@ -28,6 +29,7 @@ export default function PythonPage() {
2829
filename="main.py"
2930
initContent="print('hello, world!')"
3031
/>
32+
<ExecFile filename="main.py" language="python" content="" />
3133
</div>
3234
);
3335
}

0 commit comments

Comments
 (0)