Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts


/public/typescript/
4 changes: 3 additions & 1 deletion app/[docs_id]/markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Markdown, { Components } from "react-markdown";
import remarkGfm from "remark-gfm";
import SyntaxHighlighter from "react-syntax-highlighter";
import { EditorComponent, getAceLang } from "../terminal/editor";
import { ExecFile } from "../terminal/exec";
import { useChangeTheme } from "./themeToggle";
Expand All @@ -11,6 +10,9 @@ import {
import { ReactNode } from "react";
import { getRuntimeLang } from "@/terminal/runtime";
import { ReplTerminal } from "@/terminal/repl";
import dynamic from "next/dynamic";
// SyntaxHighlighterはファイルサイズがでかいので & HydrationErrorを起こすので、SSRを無効化する
const SyntaxHighlighter = dynamic(() => import("react-syntax-highlighter"), { ssr: false });

export function StyledMarkdown({ content }: { content: string }) {
return (
Expand Down
5 changes: 3 additions & 2 deletions app/terminal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、そ

### ファイル実行用

* runFiles: `(filenames: string[]) => Promise<ReplOutput[]>`
* 指定されたファイルを実行します。ファイルの中身はEmbedContextから取得されます。
* runFiles: `(filenames: string[], files: Record<string, string>) => Promise<ReplOutput[]>`
* 指定されたファイルを実行します。
* EmbedContextから取得したfilesを呼び出し側で引数に渡します
* 呼び出し側でmutexのロックはせず、必要であればrunFiles()内でロックします。
* getCommandlineStr: `(filenames: string[]) => string`
* 指定されたファイルを実行するためのコマンドライン引数文字列を返します。表示用です。
Expand Down
20 changes: 16 additions & 4 deletions app/terminal/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const AceEditor = dynamic(
await import("ace-builds/src-min-noconflict/mode-ruby");
await import("ace-builds/src-min-noconflict/mode-c_cpp");
await import("ace-builds/src-min-noconflict/mode-javascript");
await import("ace-builds/src-min-noconflict/mode-typescript");
await import("ace-builds/src-min-noconflict/mode-json");
await import("ace-builds/src-min-noconflict/mode-csv");
await import("ace-builds/src-min-noconflict/mode-text");
Expand All @@ -30,7 +31,15 @@ import { langConstants } from "./runtime";
// snippetを有効化するにはsnippetもimportする必要がある: import "ace-builds/src-min-noconflict/snippets/python";

// mode-xxxx.js のファイル名と、AceEditorの mode プロパティの値が対応する
export type AceLang = "python" | "ruby" | "c_cpp" | "javascript" | "json" | "csv" | "text";
export type AceLang =
| "python"
| "ruby"
| "c_cpp"
| "javascript"
| "typescript"
| "json"
| "csv"
| "text";
export function getAceLang(lang: string | undefined): AceLang {
// Markdownで指定される可能性のある言語名からAceLangを取得
switch (lang) {
Expand All @@ -46,6 +55,9 @@ export function getAceLang(lang: string | undefined): AceLang {
case "javascript":
case "js":
return "javascript";
case "typescript":
case "ts":
return "typescript";
case "json":
return "json";
case "csv":
Expand Down Expand Up @@ -73,7 +85,7 @@ export function EditorComponent(props: EditorProps) {
const code = files[props.filename] || props.initContent;
useEffect(() => {
if (!files[props.filename]) {
writeFile(props.filename, props.initContent);
writeFile({ [props.filename]: props.initContent });
}
}, [files, props.filename, props.initContent, writeFile]);

Expand All @@ -92,7 +104,7 @@ export function EditorComponent(props: EditorProps) {
// codeの内容が変更された場合のみ表示する
(props.readonly || code == props.initContent) && "invisible"
)}
onClick={() => writeFile(props.filename, props.initContent)}
onClick={() => writeFile({ [props.filename]: props.initContent })}
>
{/*<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->*/}
<svg
Expand Down Expand Up @@ -131,7 +143,7 @@ export function EditorComponent(props: EditorProps) {
enableLiveAutocompletion={true}
enableSnippets={false}
value={code}
onChange={(code: string) => writeFile(props.filename, code)}
onChange={(code: string) => writeFile({ [props.filename]: code })}
/>
</div>
);
Expand Down
42 changes: 28 additions & 14 deletions app/terminal/embedContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,21 @@ type Filename = string;
type TerminalId = string;

interface IEmbedContext {
files: Record<Filename, string>;
writeFile: (name: Filename, content: string) => void;
files: Readonly<Record<Filename, string>>;
// ファイルを書き込む。更新後のページ内の全ファイル内容を返す
// 返り値を使うことで再レンダリングを待たずに最新の内容を取得できる
writeFile: (
updatedFiles: Readonly<Record<Filename, string>>
) => Promise<Readonly<Record<Filename, string>>>;

replOutputs: Record<TerminalId, ReplCommand[]>;
replOutputs: Readonly<Record<TerminalId, ReplCommand[]>>;
addReplOutput: (
terminalId: TerminalId,
command: string,
output: ReplOutput[]
) => void;

execResults: Record<Filename, ReplOutput[]>;
execResults: Readonly<Record<Filename, ReplOutput[]>>;
setExecResult: (filename: Filename, output: ReplOutput[]) => void;
}
const EmbedContext = createContext<IEmbedContext>(null!);
Expand Down Expand Up @@ -72,16 +76,26 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) {
}, [pathname, currentPathname]);

const writeFile = useCallback(
(name: Filename, content: string) => {
setFiles((files) => {
if (files[pathname]?.[name] !== content) {
files = { ...files };
files[pathname] = { ...(files[pathname] ?? {}) };
files[pathname][name] = content;
return files;
} else {
return files;
}
(updatedFiles: Record<Filename, string>) => {
return new Promise<Record<Filename, string>>((resolve) => {
setFiles((files) => {
let changed = false;
const newFiles = { ...files };
newFiles[pathname] = { ...(newFiles[pathname] ?? {}) };
for (const [name, content] of Object.entries(updatedFiles)) {
if (newFiles[pathname][name] !== content) {
changed = true;
newFiles[pathname][name] = content;
}
}
if (changed) {
resolve(newFiles[pathname]);
return newFiles;
} else {
resolve(files[pathname] || {});
return files;
}
});
});
},
[pathname]
Expand Down
6 changes: 4 additions & 2 deletions app/terminal/exec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function ExecFile(props: ExecProps) {
}
},
});
const { setExecResult } = useEmbedContext();
const { files, setExecResult } = useEmbedContext();

const { ready, runFiles, getCommandlineStr } = useRuntime(props.language);

Expand All @@ -45,13 +45,14 @@ export function ExecFile(props: ExecProps) {
(async () => {
clearTerminal(terminalInstanceRef.current!);
terminalInstanceRef.current!.write(systemMessageColor("実行中です..."));
const outputs = await runFiles(props.filenames);
const outputs = await runFiles(props.filenames, files);
clearTerminal(terminalInstanceRef.current!);
writeOutput(
terminalInstanceRef.current!,
outputs,
false,
undefined,
null, // ファイル実行で"return"メッセージが返ってくることはないはずなので、Prismを渡す必要はない
props.language
);
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる
Expand All @@ -67,6 +68,7 @@ export function ExecFile(props: ExecProps) {
setExecResult,
terminalInstanceRef,
props.language,
files,
]);

return (
Expand Down
21 changes: 16 additions & 5 deletions app/terminal/highlight.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import Prism from "prismjs";
import chalk from "chalk";
import { RuntimeLang } from "./runtime";
// 言語定義をインポート
import "prismjs/components/prism-python";
import "prismjs/components/prism-ruby";
import "prismjs/components/prism-javascript";

export async function importPrism() {
if (typeof window !== "undefined") {
const Prism = await import("prismjs");
// 言語定義をインポート
await import("prismjs/components/prism-python");
await import("prismjs/components/prism-ruby");
await import("prismjs/components/prism-javascript");
return Prism;
} else {
return null!;
}
}

type PrismLang = "python" | "ruby" | "javascript";

Expand All @@ -17,6 +25,7 @@ function getPrismLanguage(language: RuntimeLang): PrismLang {
case "javascript":
return "javascript";
case "cpp":
case "typescript":
throw new Error(
`highlight for ${language} is disabled because it should not support REPL`
);
Expand Down Expand Up @@ -74,6 +83,7 @@ const prismToAnsi: Record<string, (text: string) => string> = {
* @returns {string} ANSIで色付けされた文字列
*/
export function highlightCodeToAnsi(
Prism: typeof import("prismjs"),
code: string,
language: RuntimeLang
): string {
Expand Down Expand Up @@ -129,3 +139,4 @@ export function highlightCodeToAnsi(
""
);
}

49 changes: 33 additions & 16 deletions app/terminal/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";
import { Heading } from "@/[docs_id]/markdown";
import "mocha/mocha.js";
import "mocha/mocha.css";
import { Fragment, useEffect, useRef, useState } from "react";
import { useWandbox } from "./wandbox/runtime";
Expand All @@ -13,6 +12,7 @@ import { useJSEval } from "./worker/jsEval";
import { ReplTerminal } from "./repl";
import { EditorComponent, getAceLang } from "./editor";
import { ExecFile } from "./exec";
import { useTypeScript } from "./typescript/runtime";

export default function RuntimeTestPage() {
return (
Expand Down Expand Up @@ -69,8 +69,17 @@ const sampleConfig: Record<RuntimeLang, SampleConfig> = {
javascript: {
repl: true,
replInitContent: '> console.log("Hello, World!");\nHello, World!',
editor: false,
exec: false,
editor: {
"main.js": 'console.log("Hello, World!");',
},
exec: ["main.js"],
},
typescript: {
repl: false,
editor: {
"main.ts": 'function greet(name: string): void {\n console.log("Hello, " + name + "!");\n}\n\ngreet("World");',
},
exec: ["main.ts"],
},
cpp: {
repl: false,
Expand Down Expand Up @@ -122,13 +131,15 @@ function RuntimeSample({
function MochaTest() {
const pyodide = usePyodide();
const ruby = useRuby();
const javascript = useJSEval();
const jsEval = useJSEval();
const typescript = useTypeScript(jsEval);
const wandboxCpp = useWandbox("cpp");
const runtimeRef = useRef<Record<RuntimeLang, RuntimeContext>>(null!);
runtimeRef.current = {
python: pyodide,
ruby: ruby,
javascript: javascript,
javascript: jsEval,
typescript: typescript,
cpp: wandboxCpp,
};

Expand All @@ -139,21 +150,27 @@ function MochaTest() {
const [mochaState, setMochaState] = useState<"idle" | "running" | "finished">(
"idle"
);
const { writeFile } = useEmbedContext();
const { files } = useEmbedContext();
const filesRef = useRef(files);
filesRef.current = files;

const runTest = () => {
setMochaState("running");
const runTest = async () => {
if(typeof window !== "undefined") {
setMochaState("running");

await import("mocha/mocha.js");

mocha.setup("bdd");
mocha.setup("bdd");

for (const lang of Object.keys(runtimeRef.current) as RuntimeLang[]) {
defineTests(lang, runtimeRef, writeFile);
}
for (const lang of Object.keys(runtimeRef.current) as RuntimeLang[]) {
defineTests(lang, runtimeRef, filesRef);
}

const runner = mocha.run();
runner.on("end", () => {
setMochaState("finished");
});
const runner = mocha.run();
runner.on("end", () => {
setMochaState("finished");
});
}
};

return (
Expand Down
Loading