diff --git a/app/[docs_id]/markdown.tsx b/app/[docs_id]/markdown.tsx index 303b66e..cacf58f 100644 --- a/app/[docs_id]/markdown.tsx +++ b/app/[docs_id]/markdown.tsx @@ -3,8 +3,8 @@ import remarkGfm from "remark-gfm"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { PythonEmbeddedTerminal } from "../terminal/python/embedded"; import { Heading } from "./section"; -import { EditorComponent } from "../terminal/editor"; -import { ExecFile } from "../terminal/exec"; +import { AceLang, EditorComponent } from "../terminal/editor"; +import { ExecFile, ExecLang } from "../terminal/exec"; export function StyledMarkdown({ content }: { content: string }) { return ( @@ -34,6 +34,11 @@ const components: Components = { strong: ({ node, ...props }) => ( ), + table: ({ node, ...props }) => ( +
+ + + ), hr: ({ node, ...props }) =>
, pre: ({ node, ...props }) => props.children, code: ({ node, className, ref, style, ...props }) => { @@ -52,21 +57,59 @@ const components: Components = { hello, world! --------------------------- */ - return ( -
- -
- ); + let execLang: ExecLang | undefined = undefined; + switch (match[1]) { + case "python": + execLang = "python"; + break; + case "cpp": + case "c++": + execLang = "cpp"; + break; + default: + console.warn(`Unsupported language for exec: ${match[1]}`); + break; + } + if (execLang) { + return ( +
+ +
+ ); + } } else if (match[3]) { // ファイル名指定がある場合、ファイルエディター + let aceLang: AceLang | undefined = undefined; + switch (match[1]) { + case "python": + aceLang = "python"; + break; + case "cpp": + case "c++": + aceLang = "c_cpp"; + break; + case "json": + aceLang = "json"; + break; + case "csv": + aceLang = "csv"; + break; + case "text": + case "txt": + aceLang = "text"; + break; + default: + console.warn(`Unsupported language for editor: ${match[1]}`); + break; + } return (
void; addFile: (filename: string) => void; diff --git a/app/layout.tsx b/app/layout.tsx index 1165301..fdf20a1 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,6 +5,7 @@ import { Sidebar } from "./sidebar"; import { ReactNode } from "react"; import { PyodideProvider } from "./terminal/python/pyodide"; import { FileProvider } from "./terminal/file"; +import { WandboxProvider } from "./terminal/wandbox/wandbox"; export const metadata: Metadata = { title: "Create Next App", @@ -22,7 +23,9 @@ export default function RootLayout({
- {children} + + {children} +
diff --git a/app/page.tsx b/app/page.tsx index c69f396..5166a77 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,79 @@ +import Link from "next/link"; +import { pagesList } from "./pagesList"; + export default function Home() { - return
- This is root page -
; + return ( +
+

my.code(); へようこそ

+

+ my.code(); + はプログラミング言語のチュートリアルを提供するウェブサイトです。 +

+
+ {pagesList.map((group) => { + return ( +
+
+

{group.lang}

+

{group.description}

+
+ + はじめる + +
+
+
+ ); + })} +
+

主な特徴

+ {/* TODO: デザインがダサい */} +
    +
  • + 豊富なチュートリアル +

    + my.code(); + ではさまざまなプログラミング言語やフレームワークのチュートリアルを提供しています。 + 初心者向けの基礎から上級者向けの応用まで、幅広いレベルに対応したチュートリアルが揃っています。 + {/* ほんまか? */} +

    +
  • +
  • + すぐに動かせる実行環境 +

    + my.code(); + ではブラウザ上でコードを実行できる環境を整備しており、環境構築の手間なくすぐにコードを実行することができます。 + チュートリアル内のサンプルコードはそのまま実行するだけでなく、自由に編集して試すことも可能です。 +

    +
  • +
  • + AIアシスタントによるサポート +

    + my.code(); ではAIアシスタントが学習をサポートします。 + チュートリアルを読んでいてわからないことがあれば、AIアシスタントに質問してみてください。 + さらに、実行したサンプルコードの解説やエラーの原因調査、改善提案まで、AIアシスタントがあなたの学習を強力に支援します。 +

    +
  • +
  • + 実践的な練習問題 +

    + 各チュートリアルには練習問題が含まれており、学んだ内容を実際に試すことができます。 + 練習問題は段階的に難易度が上がるように設計されており、理解度を深めるのに役立ちます。 + 書いたコードはその場ですぐにAIアシスタントがレビューし、フィードバックを提供します。 +

    +
  • +
+
+ ); } diff --git a/app/pagesList.ts b/app/pagesList.ts new file mode 100644 index 0000000..2a8a150 --- /dev/null +++ b/app/pagesList.ts @@ -0,0 +1,32 @@ +// docs_id = `${group.id}-${page.id}` +export const pagesList = [ + { + id: "python", + lang: "Python", + // TODO: これをいい感じの文章に変える↓ + description: "Pythonの基礎から応用までを学べるチュートリアル", + pages: [ + { id: 1, title: "環境構築と基本思想" }, + { id: 2, title: "基本構文とデータ型" }, + { id: 3, title: "リスト、タプル、辞書、セット" }, + { id: 4, title: "制御構文と関数" }, + { id: 5, title: "モジュールとパッケージ" }, + { id: 6, title: "オブジェクト指向プログラミング" }, + { + id: 7, + title: "ファイルの入出力とコンテキストマネージャ", + }, + { id: 8, title: "例外処理" }, + { id: 9, title: "ジェネレータとデコレータ" }, + ], + }, + { + id: "cpp", + lang: "C++", + description: "C++の基本から高度な機能までを学べるチュートリアル", + pages: [ + { id: 2, title: "型システムとメモリ" }, + { id: 3, title: "関数と参照" }, + ], + }, +] as const; diff --git a/app/sidebar.tsx b/app/sidebar.tsx index 21b7730..3d2d81d 100644 --- a/app/sidebar.tsx +++ b/app/sidebar.tsx @@ -1,65 +1,60 @@ "use client"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import useSWR, { Fetcher } from 'swr' +import useSWR, { Fetcher } from "swr"; import { splitMarkdown } from "./[docs_id]/splitMarkdown"; +import { pagesList } from "./pagesList"; -const fetcher: Fetcher = (url) => fetch(url).then((r) => r.text()) +const fetcher: Fetcher = (url) => + fetch(url).then((r) => r.text()); export function Sidebar() { const pathname = usePathname(); const docs_id = pathname.replace(/^\//, ""); - const { data, error, isLoading } = useSWR( - `/docs/${docs_id}.md`, - fetcher - ) + const { data, error, isLoading } = useSWR(`/docs/${docs_id}.md`, fetcher); - const pages = [ - { id: "python-1", title: "1. 環境構築と基本思想" }, - { id: "python-2", title: "2. 基本構文とデータ型" }, - { id: "python-3", title: "3. リスト、タプル、辞書、セット" }, - { id: "python-4", title: "4. 制御構文と関数" }, - { id: "python-5", title: "5. モジュールとパッケージ" }, - { id: "python-6", title: "6. オブジェクト指向プログラミング" }, - { id: "python-7", title: "7. ファイルの入出力とコンテキストマネージャ" }, - { id: "python-8", title: "8. 例外処理" }, - { id: "python-9", title: "9. ジェネレータとデコレータ" }, - ]; + if (error) console.error("Sidebar fetch error:", error); - if (error) console.error("Sidebar fetch error:", error) - - - - - const splitmdcontent = splitMarkdown(data ?? "") + const splitmdcontent = splitMarkdown(data ?? ""); return ( -
+
{/* todo: 背景色ほんとにこれでいい? */} -

+

{/* サイドバーが常時表示されている場合のみ */} Navbar Title

- -
    - {pages.map((page) => ( -
  1. - {page.title} - {page.id === docs_id && !isLoading &&( -
      - {splitmdcontent - .slice(1) - .map((section, idx) => ( -
    • - {section.title} -
    • - ))} -
    - )} +
      + {pagesList.map((group) => ( +
    • +
      + {group.lang} +
        + {group.pages.map((page) => ( +
      • + + {page.id}. + {page.title} + + {`${group.id}-${page.id}` === docs_id && !isLoading && ( +
          + {splitmdcontent.slice(1).map((section, idx) => ( +
        • + {section.title} +
        • + ))} +
        + )} +
      • + ))} +
      +
    • ))} -
+
); } - diff --git a/app/terminal/editor.tsx b/app/terminal/editor.tsx index fdbe9bb..57d8c38 100644 --- a/app/terminal/editor.tsx +++ b/app/terminal/editor.tsx @@ -9,6 +9,7 @@ import "ace-builds/src-min-noconflict/theme-twilight"; import "ace-builds/src-min-noconflict/ext-language_tools"; import "ace-builds/src-min-noconflict/ext-searchbox"; import "ace-builds/src-min-noconflict/mode-python"; +import "ace-builds/src-min-noconflict/mode-c_cpp"; import "ace-builds/src-min-noconflict/mode-json"; import "ace-builds/src-min-noconflict/mode-csv"; import "ace-builds/src-min-noconflict/mode-text"; @@ -17,8 +18,11 @@ import { useSectionCode } from "../[docs_id]/section"; import clsx from "clsx"; // snippetを有効化するにはsnippetもimportする必要がある: import "ace-builds/src-min-noconflict/snippets/python"; +// mode-xxxx.js のファイル名と、AceEditorの mode プロパティの値が対応する +export type AceLang = "python" | "c_cpp" | "json" | "csv" | "text"; + interface EditorProps { - language?: string; + language?: AceLang; tabSize: number; filename: string; initContent: string; diff --git a/app/terminal/exec.tsx b/app/terminal/exec.tsx index 82ba51b..c6b3ee7 100644 --- a/app/terminal/exec.tsx +++ b/app/terminal/exec.tsx @@ -1,21 +1,34 @@ "use client"; -import chalk from "chalk"; import { usePyodide } from "./python/pyodide"; -import { clearTerminal, getRows, useTerminal } from "./terminal"; +import { + clearTerminal, + getRows, + hideCursor, + systemMessageColor, + useTerminal, +} from "./terminal"; import { useSectionCode } from "../[docs_id]/section"; +import { useWandbox } from "./wandbox/wandbox"; +import { ReplOutput, writeOutput } from "./repl"; +import { useState } from "react"; + +export type ExecLang = "python" | "cpp"; interface ExecProps { - filename: string; - language: string; + /* + * Pythonの場合はメインファイル1つのみを指定する。 + * C++の場合はソースコード(.cpp)とヘッダー(.h)を全部指定し、ExecFile内で拡張子を元にソースコードと追加コードを分ける。 + */ + filenames: string[]; + language: ExecLang; content: string; } export function ExecFile(props: ExecProps) { const { terminalRef, terminalInstanceRef, termReady } = useTerminal({ getRows: (cols: number) => getRows(props.content, cols) + 1, onReady: () => { - // カーソル非表示 - terminalInstanceRef.current!.write("\x1b[?25l"); + hideCursor(terminalInstanceRef.current!); for (const line of props.content.split("\n")) { terminalInstanceRef.current!.writeln(line); } @@ -24,51 +37,77 @@ export function ExecFile(props: ExecProps) { const sectionContext = useSectionCode(); const pyodide = usePyodide(); + const wandbox = useWandbox(); + // 表示するコマンドライン文字列 let commandline: string; - let exec: () => Promise | void; + // trueの間 (初期化しています...) と表示される let runtimeInitializing: boolean; + // 初期化処理が必要な場合の関数 + let beforeExec: (() => Promise) | null = null; + // 実行中です... と表示される + const [isExecuting, setIsExecuting] = useState(false); + // 実際に実行する関数 + let exec: (() => Promise) | null = null; switch (props.language) { case "python": - commandline = `python ${props.filename}`; + if (props.filenames.length !== 1) { + throw new Error("Pythonの実行にはファイル名が1つ必要です"); + } + commandline = `python ${props.filenames[0]}`; runtimeInitializing = pyodide.initializing; - exec = async () => { - if (!pyodide.ready) { - clearTerminal(terminalInstanceRef.current!); - terminalInstanceRef.current!.write( - chalk.dim.bold.italic("(初期化しています...しばらくお待ちください)") - ); - await pyodide.init(); - } - clearTerminal(terminalInstanceRef.current!); - const outputs = await pyodide.runFile(props.filename); - for (const output of outputs) { - // 出力内容に応じて色を変える - const message = String(output.message).replace(/\n/g, "\r\n"); - switch (output.type) { - case "error": - terminalInstanceRef.current!.writeln(chalk.red(message)); - break; - default: - terminalInstanceRef.current!.writeln(message); - break; - } - } - sectionContext?.setExecResult(props.filename, outputs); - }; + beforeExec = pyodide.ready ? null : pyodide.init; + exec = () => pyodide.runFile(props.filenames[0]); + break; + case "cpp": + if (!props.filenames || props.filenames.length === 0) { + throw new Error("C++の実行には filenames プロパティが必要です"); + } + commandline = wandbox.cppOptions + ? `${wandbox.cppOptions.commandline} ${props.filenames.join(" ")} && ./a.out` + : ""; + runtimeInitializing = false; + const namesSource = props.filenames!.filter((name) => + name.endsWith(".cpp") + ); + const namesAdditional = props.filenames!.filter( + (name) => !name.endsWith(".cpp") + ); + exec = () => wandbox.runFiles("C++", namesSource, namesAdditional); break; default: + props.language satisfies never; commandline = `エラー: 非対応の言語 ${props.language}`; runtimeInitializing = false; - exec = () => undefined; break; } + + const onClick = async () => { + if (exec) { + if (beforeExec) { + clearTerminal(terminalInstanceRef.current!); + terminalInstanceRef.current!.write( + systemMessageColor("(初期化しています...しばらくお待ちください)") + ); + await beforeExec(); + } + clearTerminal(terminalInstanceRef.current!); + terminalInstanceRef.current!.write(systemMessageColor("実行中です...")); + setIsExecuting(true); + const outputs = await exec(); + setIsExecuting(false); + clearTerminal(terminalInstanceRef.current!); + writeOutput(terminalInstanceRef.current!, outputs, false); + // TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる + sectionContext?.setExecResult(props.filenames.join(","), outputs); + } + }; return (