原文: How to Build a Code Editor with React that Compiles and Executes in 40+ Languages

オンラインのコード実行プラットフォームを利用すれば、あなたのお気に入りのプログラミング言語でコードを書き、そのコードを同じプラットフォーム上で実行することができます。

自分で作成したプログラム (たとえば、JavaScript で作成されたバイナリ検索プログラム) の出力を確認できると理想的です。

今日は、40 以上の異なるプログラミング言語でコードをコンパイルおよび実行できる CodeRush と呼ばれるオンラインコード実行プラットフォームを構築していきます。

構築するもの

Screenshot-2022-05-18-at-9.05.14-PM

Source Code | Live Demo

以下の機能を備えた多彩なコードエディターを構築します。

  • VS Code にも使われているコードエディター (Monaco Editor)。
  • 40 を超えるプログラミング言語をサポートし、標準入出力を使用して Web アプリ上のコードをコンパイルできる。
  • 利用可能なテーマのリストからエディターのテーマを変更できる。
  • 実行されたコードに関する情報 (コードにかかった時間、使用されたメモリ、ステータスなど) を取得できる。

技術スタック

このプロジェクトでは、以下の技術スタックを使用します。

  • React.js – フロントエンド用
  • TailwindCSS – スタイル用
  • Judge0 – コードのコンパイルおよび実行用
  • RapidAPI – Judge0 コードの迅速なデプロイ
  • Monaco Editor – プロジェクトを支えるコードエディター

プロジェクトの構造

プロジェクトの構造は非常にシンプルで理解しやすくなっています。

  • Components: すべてのコンポーネント又は再利用可能なコードスニペットがここにあります。(例: CodeEditorWindow や Landing)
  • hooks: ここにすべてのカスタムフックがあります。(キーボードイベントを使用してコードをコンパイルするためのキープレスフックを使用する予定です。)
  • lib: すべてのライブラリ関数はここにあります。(ここでテーマを定義する関数を作成します。)
  • constants: ドロップダウンの languageOptionscustomStyles などのすべての定数がここに入力されます。
  • utils: コードの保守に役立つ一般的なユーティリティー関数がここにあります。

アプリケーションの流れ

コードを深く掘り下げる前に、まずアプリケーションの流れと、ゼロからコードを書いていく方法を把握しましょう。

  • ユーザーは Web アプリケーションにアクセスし、好みのプログラミング言語 (デフォルトは JavaScript) を選択できます。
  • ユーザーがコードの作成を完了すると、コードをコンパイルし、出力ウィンドウで出力、結果を確認できます。
  • コード出力ウィンドウには、コードスニペットの成功または失敗が表示されます。すべての情報がコード出力ウィンドウで確認できます。
  • ユーザーはコードスニペットにカスタム入力を追加でき、Judge0 (オンラインコンパイラー) はユーザーが指定したカスタム入力を考慮します。
  • ユーザーは、実行されたコードに関する関連情報を確認できます。(例: コードのコンパイルと実行に 5 ms かかり、2024 kb のメモリが使用され、ランタイムステータスは成功)

フォルダー構造とアプリケーションの流れについて少し理解できたところで、コードを詳しく見て、すべてがどのように機能するかをみてみましょう。

コードエディターコンポーネントを構築する方法

Screenshot-2022-05-18-at-9.26.48-PM

コードエディターコンポーネントは主に、使用およびカスタマイズできる NPM パッケージ、Monaco Editor で構成されます。

// CodeEditorWindow.js

import React, { useState } from "react";

import Editor from "@monaco-editor/react";

const CodeEditorWindow = ({ onChange, language, code, theme }) => {
  const [value, setValue] = useState(code || "");

  const handleEditorChange = (value) => {
    setValue(value);
    onChange("code", value);
  };

  return (
    <div className="overlay rounded-md overflow-hidden w-full h-full shadow-4xl">
      <Editor
        height="85vh"
        width={`100%`}
        language={language || "javascript"}
        value={value}
        theme={theme}
        defaultValue="// some comment"
        onChange={handleEditorChange}
      />
    </div>
  );
};
export default CodeEditorWindow;

Editor コンポーネントは @monaco-editor/react パッケージから取得されたもので、指定された高さ 85vh でコードエディターを起動できます。

Editor コンポーネントはいくつかの props を受け取ります:

  • language: 構文ハイライトやインテリセンスのために必要な、言語を指定する必須項目。
  • theme: コードスニペットの色と背景 (チュートリアルの後半で構成します)。
  • value: コードエディターに入力される実際のコード値。
  • onChange: これは、コードエディターの値が変更されたときにトリガーされます。後で Judge0 API を呼び出してコンパイルできるように、変更された値を状態に保存する必要があります。

エディターは、その親コンポーネントである Landing.js から、onChangelanguagecodetheme の props を受け取ります。コードエディター内の値が変更されるたびに、親コンポーネントの Landing 内にある onChange ハンドラーが呼び出されます。

ランディングコンポーネントを構築する

ランディングコンポーネントは主に 3 つの部分で構成されています。

  • languagetheme のドロップダウンコンポーネントを備えた Actions Bar
  • Code Editor Window コンポーネント
  • Output and Custom Input コンポーネント
// Landing.js

import React, { useEffect, useState } from "react";
import CodeEditorWindow from "./CodeEditorWindow";
import axios from "axios";
import { classnames } from "../utils/general";
import { languageOptions } from "../constants/languageOptions";

import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";

import { defineTheme } from "../lib/defineTheme";
import useKeyPress from "../hooks/useKeyPress";
import Footer from "./Footer";
import OutputWindow from "./OutputWindow";
import CustomInput from "./CustomInput";
import OutputDetails from "./OutputDetails";
import ThemeDropdown from "./ThemeDropdown";
import LanguagesDropdown from "./LanguagesDropdown";

const javascriptDefault = `// some comment`;

const Landing = () => {
  const [code, setCode] = useState(javascriptDefault);
  const [customInput, setCustomInput] = useState("");
  const [outputDetails, setOutputDetails] = useState(null);
  const [processing, setProcessing] = useState(null);
  const [theme, setTheme] = useState("cobalt");
  const [language, setLanguage] = useState(languageOptions[0]);

  const enterPress = useKeyPress("Enter");
  const ctrlPress = useKeyPress("Control");

  const onSelectChange = (sl) => {
    console.log("selected Option...", sl);
    setLanguage(sl);
  };

  useEffect(() => {
    if (enterPress && ctrlPress) {
      console.log("enterPress", enterPress);
      console.log("ctrlPress", ctrlPress);
      handleCompile();
    }
  }, [ctrlPress, enterPress]);
  const onChange = (action, data) => {
    switch (action) {
      case "code": {
        setCode(data);
        break;
      }
      default: {
        console.warn("case not handled!", action, data);
      }
    }
  };
  const handleCompile = () => {
    // We will come to the implementation later in the code
  };

  const checkStatus = async (token) => {
    // We will come to the implementation later in the code
  };

  function handleThemeChange(th) {
    // We will come to the implementation later in the code
  }
  useEffect(() => {
    defineTheme("oceanic-next").then((_) =>
      setTheme({ value: "oceanic-next", label: "Oceanic Next" })
    );
  }, []);

  const showSuccessToast = (msg) => {
    toast.success(msg || `Compiled Successfully!`, {
      position: "top-right",
      autoClose: 1000,
      hideProgressBar: false,
      closeOnClick: true,
      pauseOnHover: true,
      draggable: true,
      progress: undefined,
    });
  };
  const showErrorToast = (msg) => {
    toast.error(msg || `Something went wrong! Please try again.`, {
      position: "top-right",
      autoClose: 1000,
      hideProgressBar: false,
      closeOnClick: true,
      pauseOnHover: true,
      draggable: true,
      progress: undefined,
    });
  };

  return (
    <>
      <ToastContainer
        position="top-right"
        autoClose={2000}
        hideProgressBar={false}
        newestOnTop={false}
        closeOnClick
        rtl={false}
        pauseOnFocusLoss
        draggable
        pauseOnHover
      />
      <div className="h-4 w-full bg-gradient-to-r from-pink-500 via-red-500 to-yellow-500"></div>
      <div className="flex flex-row">
        <div className="px-4 py-2">
          <LanguagesDropdown onSelectChange={onSelectChange} />
        </div>
        <div className="px-4 py-2">
          <ThemeDropdown handleThemeChange={handleThemeChange} theme={theme} />
        </div>
      </div>
      <div className="flex flex-row space-x-4 items-start px-4 py-4">
        <div className="flex flex-col w-full h-full justify-start items-end">
          <CodeEditorWindow
            code={code}
            onChange={onChange}
            language={language?.value}
            theme={theme.value}
          />
        </div>

        <div className="right-container flex flex-shrink-0 w-[30%] flex-col">
          <OutputWindow outputDetails={outputDetails} />
          <div className="flex flex-col items-end">
            <CustomInput
              customInput={customInput}
              setCustomInput={setCustomInput}
            />
            <button
              onClick={handleCompile}
              disabled={!code}
              className={classnames(
                "mt-4 border-2 border-black z-10 rounded-md shadow-[5px_5px_0px_0px_rgba(0,0,0)] px-4 py-2 hover:shadow transition duration-200 bg-white flex-shrink-0",
                !code ? "opacity-50" : ""
              )}
            >
              {processing ? "Processing..." : "Compile and Execute"}
            </button>
          </div>
          {outputDetails && <OutputDetails outputDetails={outputDetails} />}
        </div>
      </div>
      <Footer />
    </>
  );
};
export default Landing;

ランディングページの基本構造をさらに詳しくみてみましょう。

CodeEditorWindow コンポーネント

上記に説明したように、CodeEditorWindow コンポーネントは (変更され続ける) コードと、コード内の変更を追跡する onChange メソッドを考慮します。

// onChange method implementation

 const onChange = (action, data) => {
    switch (action) {
      case "code": {
        setCode(data);
        break;
      }
      default: {
        console.warn("case not handled!", action, data);
      }
    }
  };

code の状態を設定し、変更を追跡するだけでいいのです。

CodeEditorWindow コンポーネントは、構文の強調表示とインテリセンスが必要な現在選択されている言語である language の props も考慮します。

Monaco Editor によって受け入れられた language props を追跡し、コンパイルも処理する languageOptions 配列を作成しました。(judge0 API によって受け入れられる languageId を追跡します)

// constants/languageOptions.js

export const languageOptions = [
  {
    id: 63,
    name: "JavaScript (Node.js 12.14.0)",
    label: "JavaScript (Node.js 12.14.0)",
    value: "javascript",
  },
  {
    id: 45,
    name: "Assembly (NASM 2.14.02)",
    label: "Assembly (NASM 2.14.02)",
    value: "assembly",
  },
    ...
    ...
    ...
    ...
    ...
    ...
    
  {
    id: 84,
    name: "Visual Basic.Net (vbnc 0.0.0.5943)",
    label: "Visual Basic.Net (vbnc 0.0.0.5943)",
    value: "vbnet",
  },
];

すべての languageOptions オブジェクトには、idnamelabel、および value キーが含まれています。languageOptions 配列全体を取得してドロップダウン内に配置し、オプションとして指定できます。

ドロップダウンの状態が変化するたびに、onSelectChange メソッドは選択された id を追跡し、状態を適切に変更します。

LanguageDropdown コンポーネント

Screenshot-2022-05-19-at-10.42.43-PM
// LanguageDropdown.js

import React from "react";
import Select from "react-select";
import { customStyles } from "../constants/customStyles";
import { languageOptions } from "../constants/languageOptions";

const LanguagesDropdown = ({ onSelectChange }) => {
  return (
    <Select
      placeholder={`Filter By Category`}
      options={languageOptions}
      styles={customStyles}
      defaultValue={languageOptions[0]}
      onChange={(selectedOption) => onSelectChange(selectedOption)}
    />
  );
};

export default LanguagesDropdown;

ドロップダウンの場合は、ドロップダウンとその変更ハンドラーを処理する react-select パッケージを使用します。

React select は、defaultValueoptions を主要引数として受け取ります。options は、関連するすべてのドロップダウン値を自動的に表示する配列 (ここでは languageOptions を渡します) です。

defaultValue という props は、コンポーネントに提供されるデフォルト値です。ここではデフォルト言語として JavaScript を選択します。(これは言語の配列内で最初の言語です。)

ユーザーが言語を変更するたびに、onSelectChange コールバックを使用して言語を変更します。

const onSelectChange = (sl) => {
    setLanguage(sl);
};

ThemeDropdown コンポーネント

Screenshot-2022-05-19-at-10.42.43-PM-1-1

ThemeDropdown コンポーネントは、実は (UI および React-select パッケージを含む) LanguageDropdown コンポーネントに非常に似ています。

// ThemeDropdown.js

import React from "react";
import Select from "react-select";
import monacoThemes from "monaco-themes/themes/themelist";
import { customStyles } from "../constants/customStyles";

const ThemeDropdown = ({ handleThemeChange, theme }) => {
  return (
    <Select
      placeholder={`Select Theme`}
      // options={languageOptions}
      options={Object.entries(monacoThemes).map(([themeId, themeName]) => ({
        label: themeName,
        value: themeId,
        key: themeId,
      }))}
      value={theme}
      styles={customStyles}
      onChange={handleThemeChange}
    />
  );
};

export default ThemeDropdown;

ここでは、Monaco Editor 用にネット上で入手可能で、多種のきれいなデザインテーマを使用することができる、MonacoThemes というパッケージを利用します。

自由に利用できるテーマのリストがこちらです。

// lib/defineTheme.js

import { loader } from "@monaco-editor/react";

const monacoThemes = {
  active4d: "Active4D",
  "all-hallows-eve": "All Hallows Eve",
  amy: "Amy",
  "birds-of-paradise": "Birds of Paradise",
  blackboard: "Blackboard",
  "brilliance-black": "Brilliance Black",
  "brilliance-dull": "Brilliance Dull",
  "chrome-devtools": "Chrome DevTools",
  "clouds-midnight": "Clouds Midnight",
  clouds: "Clouds",
  cobalt: "Cobalt",
  dawn: "Dawn",
  dreamweaver: "Dreamweaver",
  eiffel: "Eiffel",
  "espresso-libre": "Espresso Libre",
  github: "GitHub",
  idle: "IDLE",
  katzenmilch: "Katzenmilch",
  "kuroir-theme": "Kuroir Theme",
  lazy: "LAZY",
  "magicwb--amiga-": "MagicWB (Amiga)",
  "merbivore-soft": "Merbivore Soft",
  merbivore: "Merbivore",
  "monokai-bright": "Monokai Bright",
  monokai: "Monokai",
  "night-owl": "Night Owl",
  "oceanic-next": "Oceanic Next",
  "pastels-on-dark": "Pastels on Dark",
  "slush-and-poppies": "Slush and Poppies",
  "solarized-dark": "Solarized-dark",
  "solarized-light": "Solarized-light",
  spacecadet: "SpaceCadet",
  sunburst: "Sunburst",
  "textmate--mac-classic-": "Textmate (Mac Classic)",
  "tomorrow-night-blue": "Tomorrow-Night-Blue",
  "tomorrow-night-bright": "Tomorrow-Night-Bright",
  "tomorrow-night-eighties": "Tomorrow-Night-Eighties",
  "tomorrow-night": "Tomorrow-Night",
  tomorrow: "Tomorrow",
  twilight: "Twilight",
  "upstream-sunburst": "Upstream Sunburst",
  "vibrant-ink": "Vibrant Ink",
  "xcode-default": "Xcode_default",
  zenburnesque: "Zenburnesque",
  iplastic: "iPlastic",
  idlefingers: "idleFingers",
  krtheme: "krTheme",
  monoindustrial: "monoindustrial",
};

const defineTheme = (theme) => {
  return new Promise((res) => {
    Promise.all([
      loader.init(),
      import(`monaco-themes/themes/${monacoThemes[theme]}.json`),
    ]).then(([monaco, themeData]) => {
      monaco.editor.defineTheme(theme, themeData);
      res();
    });
  });
};

export { defineTheme };

monaco-themes というパッケージは、コードエディターのデザインの方向性を定義するために使用できる多数のテーマを提供します。

defineTheme 関数は、ユーザーが選択するであろうさまざまなテーマを扱います。defineTheme 関数は、monaco.editor.defineTheme(theme,themeData) アクションを使用して実際に Monaco Editor のテーマを設定する Promise を返します。このコード行は、Monaco Editor コードウィンドウ内のテーマを実際に変更する役割を担っています。

defineTheme 関数は、先ほど ThemeDropdown.js コンポーネントで見た onChange コールバックを利用して呼び出されます。

// Landing.js - handleThemeChange() function

function handleThemeChange(th) {
    const theme = th;
    console.log("theme...", theme);

    if (["light", "vs-dark"].includes(theme.value)) {
      setTheme(theme);
    } else {
      defineTheme(theme.value).then((_) => setTheme(theme));
    }
  }
  

handleThemeChange() 関数は、テーマが lightdark かをチェックします。これらのテーマはデフォルトで MonacoEditor コンポーネントで利用できるため、defineTheme() メソッドを呼び出す必要はありません。

そうでない場合は、defineTheme() コンポーネントを呼び出して、選択したテーマの状態を設定します。

Screenshot-2022-05-19-at-10.46.34-PM

Judge0 を使用してコードをコンパイルする方法

アプリケーションの根幹部である、さまざまな言語でコードをコンパイルする方法を見ていきましょう。

コードをコンパイルするには、Judge0 を使用します。Judge0 は、対話可能でかつシンプルな、オープンソースのコード実行システムです。

いくつかの引数 (ソースコード、言語 ID) を使用して単純な API 呼び出しを実行し、応答として出力を取得することができます。

Judge0 をセットアップして、次のステップに進みましょう。

  • Judge0 に移動し、ベーシックプランを選択します
  • Judge0 は実際は RapidAPI でホスティングされています。ベーシックプランに登録してください。
  • 登録すると、コード実行システムへの API 呼び出しを行うために必要な RAPIDAPI_HOSTRAPIDAPI_KEY をコピーできます。

ダッシュボードは以下のようなものになります。

Untitled-design

API 呼び出しには X-RapidAPI-Host 引数と X-RapidAPI-Key 引数が必要です。後ほど使用できるように、次のように .env ファイルに保存します。

REACT_APP_RAPID_API_HOST = YOUR_HOST_URL
REACT_APP_RAPID_API_KEY = YOUR_SECRET_KEY
REACT_APP_RAPID_API_URL = YOUR_SUBMISSIONS_URL

React では、環境変数に REACT_APP というプレフィックスを最初に含めることが重要です。

SUBMISSIONS_URL は、使用する URL です。これは基本的に host とそれに続く /submission ルートで構成されます。

例えばこの場合では、https://judge0-ce.p.rapidapi.com/submissionssubmissions URL になります。

変数を正しく設定したら、compilation (コンパイル) ロジックを処理できるようになります。

コンパイルフローとロジック

コンパイルの流れは以下の通りです。

  • Compile and Execute ボタンをクリックすると、handleCompile() 関数が呼び出されます。
  • handleCompile() 関数は、languageIdsource_codestdin (この場合は customInput) を本体引数として、submissions URL 上で Judge0 RapidAPI のバックエンドにリクエストします。
  • the optionshostsecret をヘッダーとして受け取ります。
  • base64_encodedfields は、渡すことができるオプションの引数です。
  • submission POST リクエストはリクエストをサーバーに登録し、プロセスを作成します。post リクエストの応答は、後で実行のステータスを確認するために使用する token です。(処理中、承認済み、時間制限超過、実行時間の例外など、さまざまなステータスがあります。)
  • 結果が返されると、結果が成功か失敗かを条件付きで確認し、結果を出力画面に表示します。

実際にコードを見てみて、handleCompile() メソッドを理解しましょう。

const handleCompile = () => {
    setProcessing(true);
    const formData = {
      language_id: language.id,
      // encode source code in base64
      source_code: btoa(code),
      stdin: btoa(customInput),
    };
    const options = {
      method: "POST",
      url: process.env.REACT_APP_RAPID_API_URL,
      params: { base64_encoded: "true", fields: "*" },
      headers: {
        "content-type": "application/json",
        "Content-Type": "application/json",
        "X-RapidAPI-Host": process.env.REACT_APP_RAPID_API_HOST,
        "X-RapidAPI-Key": process.env.REACT_APP_RAPID_API_KEY,
      },
      data: formData,
    };

    axios
      .request(options)
      .then(function (response) {
        console.log("res.data", response.data);
        const token = response.data.token;
        checkStatus(token);
      })
      .catch((err) => {
        let error = err.response ? err.response.data : err;
        setProcessing(false);
        console.log(error);
      });
  };

上記の通り、handleCompile() メソッドは languageIdsource_codestdin を受け取ります。特に source_codestdin の前に btoa が使用されていることに注目してください。これは、API に渡すパラメータで base64_encoded: true を使用しているのにあわせて、文字列を Base64 エンコードするためのものです。

応答が成功でトークンを入手したら、checkStatus() メソッドを呼び出して /submissions/${token} ルートをポーリングします。

const checkStatus = async (token) => {
    const options = {
      method: "GET",
      url: process.env.REACT_APP_RAPID_API_URL + "/" + token,
      params: { base64_encoded: "true", fields: "*" },
      headers: {
        "X-RapidAPI-Host": process.env.REACT_APP_RAPID_API_HOST,
        "X-RapidAPI-Key": process.env.REACT_APP_RAPID_API_KEY,
      },
    };
    try {
      let response = await axios.request(options);
      let statusId = response.data.status?.id;

      // Processed - we have a result
      if (statusId === 1 || statusId === 2) {
        // still processing
        setTimeout(() => {
          checkStatus(token)
        }, 2000)
        return
      } else {
        setProcessing(false)
        setOutputDetails(response.data)
        showSuccessToast(`Compiled Successfully!`)
        console.log('response.data', response.data)
        return
      }
    } catch (err) {
      console.log("err", err);
      setProcessing(false);
      showErrorToast();
    }
  };

以前に送信したコードの結果を取得するには、応答として受け取った token を使用して submissions API をポーリングする必要があります。

上記のように、エンドポイントに対して GET リクエストを行います。レスポンスがあると、statusId === 1 || statusId === 2 をチェックしていますが、これはどういう意味なのでしょうか。

私たちが API に提出したコードに関連する合計 14 のステータスがあります。それらは以下の通りです:

export const statuses = [
  {
    id: 1,
    description: "In Queue",
  },
  {
    id: 2,
    description: "Processing",
  },
  {
    id: 3,
    description: "Accepted",
  },
  {
    id: 4,
    description: "Wrong Answer",
  },
  {
    id: 5,
    description: "Time Limit Exceeded",
  },
  {
    id: 6,
    description: "Compilation Error",
  },
  {
    id: 7,
    description: "Runtime Error (SIGSEGV)",
  },
  {
    id: 8,
    description: "Runtime Error (SIGXFSZ)",
  },
  {
    id: 9,
    description: "Runtime Error (SIGFPE)",
  },
  {
    id: 10,
    description: "Runtime Error (SIGABRT)",
  },
  {
    id: 11,
    description: "Runtime Error (NZEC)",
  },
  {
    id: 12,
    description: "Runtime Error (Other)",
  },
  {
    id: 13,
    description: "Internal Error",
  },
  {
    id: 14,
    description: "Exec Format Error",
  },
];

したがって、statusId ===1 または statusId ===2 の場合、コードがまだ処理中であり、API を再度呼び出して結果が得られるかどうかを確認する必要があることを意味します。

このため、if 文の条件に checkStatus() 関数を再度呼び出す setTimeout() があり、内部で API を再度呼び出してステータスを確認します。

ステータスが 2 または 3 以外の場合は、コードの実行が完了し、結果が得られたことを意味します。successfully compiled (コンパイル成功) か、Time Limit Exceeded (タイムリミット超過) か、あるいは Runtime Exception (ランタイム例外の発生) のいずれかになります。statusId は各シナリオを表し、これらを再現することができます。

例えば、while(true) では time limit exceeded のエラーが発生します。

Screenshot-2022-05-20-at-1.33.08-AM

または、構文に誤りがあると、コンパイルエラーが発生します。

Screenshot-2022-05-20-at-1.34.42-AM

どの場合においても、何らかの結果を得ることになります。そして、この結果を outputDetails の状態に保存します。これは、画面の右側 (出力ウィンドウ) に表示するものがあることを保証するためです。

出力ウィンドウコンポーネント

Screenshot-2022-05-20-at-1.37.39-AM
import React from "react";

const OutputWindow = ({ outputDetails }) => {
  const getOutput = () => {
    let statusId = outputDetails?.status?.id;

    if (statusId === 6) {
      // compilation error
      return (
        <pre className="px-2 py-1 font-normal text-xs text-red-500">
          {atob(outputDetails?.compile_output)}
        </pre>
      );
    } else if (statusId === 3) {
      return (
        <pre className="px-2 py-1 font-normal text-xs text-green-500">
          {atob(outputDetails.stdout) !== null
            ? `${atob(outputDetails.stdout)}`
            : null}
        </pre>
      );
    } else if (statusId === 5) {
      return (
        <pre className="px-2 py-1 font-normal text-xs text-red-500">
          {`Time Limit Exceeded`}
        </pre>
      );
    } else {
      return (
        <pre className="px-2 py-1 font-normal text-xs text-red-500">
          {atob(outputDetails?.stderr)}
        </pre>
      );
    }
  };
  return (
    <>
      <h1 className="font-bold text-xl bg-clip-text text-transparent bg-gradient-to-r from-slate-900 to-slate-700 mb-2">
        Output
      </h1>
      <div className="w-full h-56 bg-[#1e293b] rounded-md text-white font-normal text-sm overflow-y-auto">
        {outputDetails ? <>{getOutput()}</> : null}
      </div>
    </>
  );
};

export default OutputWindow;

これは、成功または失敗のシナリオのみを表示する、シンプルなコンポーネントです。

getOutput() メソッドは、テキストの色がどのように見えるか、および何を表示するかを決定します。

  • statusId6 の場合: コンパイルエラーが発生しています。この場合、API は compile_output を返し、それを使用してエラーを表示できます。
  • statusId3 の場合: これは Accepted という成功のシナリオです。この場合、API は stdout (標準出力) を返します。これは、API に提供したコードから返されたデータを表示するために使用されます。
  • statusId5 の場合: 時間制限超過エラーが発生しています。コード内に無限ループ条件があるか、またはコード実行の標準の 5 秒時間を超えていることを表示するのみです。
  • その他すべてのステータスについては、標準的な stderr オブジェクトを取得し、エラーを表示するために使用します。
  • atob() メソッドが使用されている点に注意してください。これは、出力を Base64 文字列として受け取るためです。デコードするために atob() メソッドを使用します。

以下は、JavaScript での Binary Search プログラムの成功シナリオです。

Screenshot-2022-05-20-at-1.42.55-AM

出力詳細コンポーネント

Screenshot-2022-05-20-at-1.44.01-AM

OutputDetails コンポーネントは、最初にコンパイルしたコードスニペットに関連する詳細を表示するためのシンプルなマッパーです。データはすでに outputDetails 状態変数に設定されています。

import React from "react";

const OutputDetails = ({ outputDetails }) => {
  return (
    <div className="metrics-container mt-4 flex flex-col space-y-3">
      <p className="text-sm">
        Status:{" "}
        <span className="font-semibold px-2 py-1 rounded-md bg-gray-100">
          {outputDetails?.status?.description}
        </span>
      </p>
      <p className="text-sm">
        Memory:{" "}
        <span className="font-semibold px-2 py-1 rounded-md bg-gray-100">
          {outputDetails?.memory}
        </span>
      </p>
      <p className="text-sm">
        Time:{" "}
        <span className="font-semibold px-2 py-1 rounded-md bg-gray-100">
          {outputDetails?.time}
        </span>
      </p>
    </div>
  );
};

export default OutputDetails;

timememorystatus.description はすべて API 応答から受信された後、outputDetails に保存され表示されます。

キーボードイベント

最後は、ctrl+enter を使って、コードをコンパイルする機能です。これには、Web アプリケーションでさまざまなキーボードイベントのイベントリスナーの役割をする、カスタムフックを作成します (カスタムフックは非常に便利で、コードもきれいに保てます)。

// useKeyPress.js

import React, { useState } from "react";

const useKeyPress = function (targetKey) {
  const [keyPressed, setKeyPressed] = useState(false);

  function downHandler({ key }) {
    if (key === targetKey) {
      setKeyPressed(true);
    }
  }

  const upHandler = ({ key }) => {
    if (key === targetKey) {
      setKeyPressed(false);
    }
  };

  React.useEffect(() => {
    document.addEventListener("keydown", downHandler);
    document.addEventListener("keyup", upHandler);

    return () => {
      document.removeEventListener("keydown", downHandler);
      document.removeEventListener("keyup", upHandler);
    };
  });

  return keyPressed;
};

export default useKeyPress;
// Landing.js

...
...
...
const Landing = () => {
    ...
    ...
      const enterPress = useKeyPress("Enter");
      const ctrlPress = useKeyPress("Control");
   ...
   ...
}

ここでは、純粋な JavaScript の Event Listeners を使用して、目的の target キーのイベントを待ち、応答します。

この Hook は、keydownkeyup のイベントを待ちます。EnterControl のターゲットキーを使用してフックを初期化します。

targetKey === key かどうかを確認し、それに応じて keyPressed を設定しているため、keyPressed で返されたブール値 (true または false) を使用することができます。

これで、useEffect フックでこれらのイベントにリスナーを付け、両方が同時に押されたことを確認できます。

useEffect(() => {
    if (enterPress && ctrlPress) {
      console.log("enterPress", enterPress);
      console.log("ctrlPress", ctrlPress);
      handleCompile();
    }
  }, [ctrlPress, enterPress]);

これにより、ユーザーが control キーと enter キーを連続して押すか、同時に押すたびに、handleCompile() メソッドが呼び出されます。

注意事項

楽しいプロジェクトではありますが、Judge0 の基本プランには、1 日 100 回までのリクエスト制限があります。

その対策法として、独自のサーバーやドロップレットを (Digital Ocean 上で) 起動し、オープンソースプロジェクトを独自にホストすることができます (これについての素晴らしいドキュメントも用意されています)。

まとめ

最終結果は以下になります。

  • 40 以上の言語でコンパイルできるコードエディター
  • コードエディターのデザインを変更するためのテーマ切り替え
  • RapidAPI 上での API の相互作用とホスティング
  • カスタムフックを使用した React でキーボードイベントの使用
  • 楽しい時間! ;)

最後に、プロジェクトをさらに掘り下げたい方は、以下の機能を追加してみてはどうでしょうか。

  • ログインおよび登録モジュール: コードを自分の個人ダッシュボードに保存できるようにします。
  • インターネット上で他の人とコードをシェアする方法
  • プロフィールページとカスタマイズ
  • Socket プログラミングと操作変換を使用した、単一のコードスニペットでのペアプログラミング
  • お気に入りのコードスニペットをブックマークする
  • CodePen のような、保存されたコードスニペットのカスタムダッシュボード

私にとって、このアプリケーションをゼロからコーディングすることはとても楽しく、TailwindCSS は私のお気に入りであり、アプリケーションのスタイリングには欠かせないリソースです。

もしこの記事が役に立ったのなら、GitHub Repository に⭐️をつけていただけると嬉しいです。
質問があれば、TwitterWebsite からお気軽にご連絡ください。喜んでサポートさせていただきます。

Source Code | Live Demo