Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0e4c270
PythonのREPLが表示できた
na-trium-144 Aug 9, 2025
aa4b9b2
複数行入力に対応
na-trium-144 Aug 9, 2025
8e369f9
シンタックスハイライト実装、コピペ対応など
na-trium-144 Aug 9, 2025
4ed4cd4
テーマオプション設定
na-trium-144 Aug 9, 2025
91d4ae6
テーマ設定
na-trium-144 Aug 10, 2025
c793a6f
ターミナルをドキュメント内に埋め込む実装
na-trium-144 Aug 10, 2025
918a81a
ターミナルをクリックしたときに初期化
na-trium-144 Aug 10, 2025
b437d18
フォント変更、リサイズ処理
na-trium-144 Aug 10, 2025
67af70c
letterSpacingを修正
na-trium-144 Aug 10, 2025
6d8a470
より正確な行数カウント
na-trium-144 Aug 10, 2025
0279492
markdownからファイル名をパース、言語の後ろに-replがついているときのみreplを起動する
na-trium-144 Aug 10, 2025
5740498
Merge remote-tracking branch 'origin/main' into terminal
na-trium-144 Aug 11, 2025
0f8dc51
Merge remote-tracking branch 'origin/main' into terminal
na-trium-144 Aug 12, 2025
404de61
python→python-repl
na-trium-144 Aug 12, 2025
92a6bf0
python実行の排他制御
na-trium-144 Aug 12, 2025
c5e812e
ファイルエディターの実装
na-trium-144 Aug 12, 2025
91ba55c
ファイルを実行できるようになった
na-trium-144 Aug 12, 2025
8fffda0
section.tsxで各セクションのユーザーコードを取得できるようにした
na-trium-144 Aug 13, 2025
5517ec6
lintエラー修正
na-trium-144 Aug 13, 2025
8a64757
1章の記述修正、ファイルエディターの改善など
na-trium-144 Aug 15, 2025
243807e
フォントが初期化されるまでターミナルを初期化しない
na-trium-144 Aug 16, 2025
f8b9a58
練習問題追加 5章まで
na-trium-144 Aug 16, 2025
434209d
7章まで練習問題追加、テキストファイルを読み込んで表示する機能を追加
na-trium-144 Aug 18, 2025
61fd739
9章まで練習問題追加
na-trium-144 Aug 18, 2025
a75d2e0
readme
na-trium-144 Aug 18, 2025
026bc5d
Merge remote-tracking branch 'origin/main' into terminal
na-trium-144 Aug 18, 2025
d00c06b
不要なconsole.log削除
na-trium-144 Aug 18, 2025
ea09c9d
操作可能なコードブロックとそうでないものの区別をわかりやすく
na-trium-144 Aug 18, 2025
f111788
エディタに変更が反映されないことがあるのを修正
na-trium-144 Aug 18, 2025
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
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,56 @@ npm run lint
```
でコードをチェックします。出てくるwarningやerrorはできるだけ直しましょう。

## markdown仕様

````
```言語名-repl
>>> コマンド
実行結果例
```
````

でターミナルを埋め込む。
* ターミナル表示部は app/terminal/terminal.tsx
* コマンド入力処理は app/terminal/repl.tsx
* 各言語の実行環境は app/terminal/言語名/ 内に書く。
* 実行結果はSectionContextにも送られ、section.tsxからアクセスできる

````
```言語名:ファイル名
ファイルの内容
```
````

でテキストエディターを埋め込む。
* app/terminal/editor.tsx
* editor.tsx内で `import "ace-builds/src-min-noconflict/mode-言語名";` を追加すればその言語に対応した色付けがされる。
* importできる言語の一覧は https://github.com/ajaxorg/ace-builds/tree/master/src-noconflict
* 編集した内容は app/terminal/file.tsx のFileContextで管理される。
* 編集中のコードはFileContextに即時送られる
* FileContextが書き換えられたら即時すべてのエディターに反映される
* 編集したファイルの一覧はSectionContextにも送られ、section.tsxからアクセスできる

````
```言語名-readonly:ファイル名
ファイルの内容
```
````

で同様にテキストエディターを埋め込むが、編集不可になる

````
```言語名-exec:ファイル名
実行結果例
```
````

で実行ボタンを表示する
* 実行ボタンを押した際にFileContextからファイルを読み、実行し、結果を表示する
* app/terminal/exec.tsx に各言語ごとの実装を書く (それぞれ app/terminal/言語名/ 内に定義した関数を呼び出す)
* 実行結果はSectionContextにも送られ、section.tsxからアクセスできる


## 技術スタック・ドキュメント・メモ

- [Next.js](https://nextjs.org/docs)
Expand Down
77 changes: 63 additions & 14 deletions app/[docs_id]/markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import Markdown, { Components } from "react-markdown";
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";

export function StyledMarkdown({ content }: { content: string }) {
return (
Expand Down Expand Up @@ -31,48 +34,94 @@ const components: Components = {
strong: ({ node, ...props }) => (
<strong className="text-primary" {...props} />
),
hr: ({ node, ...props }) => <hr className="border-primary my-4" {...props} />,
pre: ({ node, ...props }) => props.children,
code: ({ node, className, ref, style, ...props }) => {
const match = /language-(\w+)/.exec(className || "");
const match = /^language-(\w+)(-repl|-exec|-readonly)?\:?(.+)?$/.exec(
className || ""
);
if (match) {
// block
if (match[2] === "-exec" && match[3]) {
/*
```python-exec:main.py
hello, world!
```
---------------------------
[▶ 実行] `python main.py`
hello, world!
---------------------------
*/
return (
<div className="border border-primary border-2 shadow-md m-2 rounded-lg">
<ExecFile
language={match[1]}
filename={match[3]}
content={String(props.children || "").replace(/\n$/, "")}
/>
</div>
);
} else if (match[3]) {
// ファイル名指定がある場合、ファイルエディター
return (
<div className="border border-primary border-2 shadow-md m-2 rounded-lg">
<EditorComponent
language={match[1]}
tabSize={4}
filename={match[3]}
readonly={match[2] === "-readonly"}
initContent={String(props.children || "").replace(/\n$/, "")}
/>
</div>
);
} else if (match[2] === "-repl") {
// repl付きの言語指定
// 現状はPythonのみ対応
switch (match[1]) {
case "python":
return (
<div className="bg-base-300 border border-primary border-2 shadow-md m-2 p-4 pr-1 rounded-lg">
<PythonEmbeddedTerminal
content={String(props.children || "").replace(/\n$/, "")}
/>
</div>
);
default:
console.warn(`Unsupported language for repl: ${match[1]}`);
break;
}
}
return (
<SyntaxHighlighter
language={match[1]}
PreTag="div"
className="px-4! py-4! m-0!"
className="border border-base-300 mx-2 my-2 rounded-lg text-sm! m-2! p-4!"
// style={todo dark theme?}
{...props}
>
{String(props.children).replace(/\n$/, "")}
{String(props.children || "").replace(/\n$/, "")}
</SyntaxHighlighter>
);
} else if (String(props.children).includes("\n")) {
// 言語指定なしコードブロック
return (
<SyntaxHighlighter
PreTag="div"
className="px-4! py-4! m-0!"
className="border border-base-300 mx-2 my-2 rounded-lg text-sm! m-2! p-4!"
// style={todo dark theme?}
{...props}
>
{String(props.children).replace(/\n$/, "")}
{String(props.children || "").replace(/\n$/, "")}
</SyntaxHighlighter>
);
} else {
// inline
return (
<code
className="bg-base-200 border border-base-300 p-1 rounded font-mono "
className="bg-base-200 border border-base-300 px-1 py-0.5 rounded text-sm "
{...props}
/>
);
}
},
pre: ({ node, ...props }) => (
<pre
className="bg-base-200 border border-primary mx-2 my-2 rounded-lg font-mono text-sm overflow-x-auto"
{...props}
/>
),
hr: ({ node, ...props }) => <hr className="border-primary my-4" {...props} />,
};
6 changes: 6 additions & 0 deletions app/[docs_id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { MarkdownSection, splitMarkdown } from "./splitMarkdown";
import { Section } from "./section";
import * as pyodideLock from "pyodide/pyodide-lock.json";

export default async function Page({
params,
Expand All @@ -30,6 +31,11 @@ export default async function Page({
notFound();
}

mdContent = mdContent.replaceAll(
"{process.env.PYODIDE_PYTHON_VERSION}",
String(pyodideLock.info.python)
);

const splitMdContent: MarkdownSection[] = await splitMarkdown(mdContent);

return (
Expand Down
80 changes: 73 additions & 7 deletions app/[docs_id]/section.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,88 @@
"use client";

import { ReactNode } from "react";
import {
createContext,
ReactNode,
useCallback,
useContext,
useState,
} from "react";
import { type MarkdownSection } from "./splitMarkdown";
import { StyledMarkdown } from "./markdown";
import { ChatForm } from "./chatForm";
import { ReplCommand, ReplOutput } from "../terminal/repl";
import { useFile } from "../terminal/file";

// セクション内に埋め込まれているターミナルとファイルエディターの内容をSection側から取得できるよう、
// Contextに保存する
interface ISectionCodeContext {
addReplOutput: (command: string, output: ReplOutput[]) => void;
addFile: (filename: string) => void;
setExecResult: (filename: string, output: ReplOutput[]) => void;
}
const SectionCodeContext = createContext<ISectionCodeContext | null>(null);
export const useSectionCode = () => useContext(SectionCodeContext);

// 1つのセクションのタイトルと内容を表示する。内容はMarkdownとしてレンダリングする
export function Section({ section }: { section: MarkdownSection }) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [replOutputs, setReplOutputs] = useState<ReplCommand[]>([]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [execResults, setExecResults] = useState<Record<string, ReplOutput[]>>(
{}
);
const [filenames, setFilenames] = useState<string[]>([]);
const { files } = useFile();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const fileContents: { name: string; content: string }[] = filenames.map(
(name) => ({ name, content: files[name] || "" })
);
const addReplOutput = useCallback(
(command: string, output: ReplOutput[]) =>
setReplOutputs((outs) => [...outs, { command, output }]),
[]
);
const addFile = useCallback(
(filename: string) =>
setFilenames((filenames) =>
filenames.includes(filename) ? filenames : [...filenames, filename]
),
[]
);
const setExecResult = useCallback(
(filename: string, output: ReplOutput[]) =>
setExecResults((results) => {
results[filename] = output;
return results;
}),
[]
);

// replOutputs: section内にあるターミナルにユーザーが入力したコマンドとその実行結果
// fileContents: section内にあるファイルエディターの内容
// execResults: section内にあるファイルの実行結果
// console.log(section.title, replOutputs, fileContents, execResults);

return (
<div>
<Heading level={section.level}>{section.title}</Heading>
<StyledMarkdown content={section.content} />
<ChatForm documentContent={section.content} />
</div>
<SectionCodeContext.Provider
value={{ addReplOutput, addFile, setExecResult }}
>
<div>
<Heading level={section.level}>{section.title}</Heading>
<StyledMarkdown content={section.content} />
<ChatForm documentContent={section.content} />
</div>
</SectionCodeContext.Provider>
);
}

export function Heading({ level, children }: { level: number; children: ReactNode }) {
export function Heading({
level,
children,
}: {
level: number;
children: ReactNode;
}) {
switch (level) {
case 1:
return <h1 className="text-2xl font-bold my-4">{children}</h1>;
Expand Down
18 changes: 18 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,20 @@
@import "tailwindcss";
@plugin "daisyui";

/* inconsolata-latin-wght-normal */
@font-face {
font-family: "Inconsolata Variable";
font-style: normal;
font-display: swap;
font-weight: 200 900;
src: url(https://cdn.jsdelivr.net/fontsource/fonts/inconsolata:vf@latest/latin-wght-normal.woff2)
format("woff2-variations");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
U+2215, U+FEFF, U+FFFD;
}

@theme {
--font-mono: "Inconsolata Variable", monospace;
}
6 changes: 5 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import "./globals.css";
import { Navbar } from "./navbar";
import { Sidebar } from "./sidebar";
import { ReactNode } from "react";
import { PyodideProvider } from "./terminal/python/pyodide";
import { FileProvider } from "./terminal/file";

export const metadata: Metadata = {
title: "Create Next App",
Expand All @@ -19,7 +21,9 @@ export default function RootLayout({
<input id="drawer-toggle" type="checkbox" className="drawer-toggle" />
<div className="drawer-content flex flex-col">
<Navbar />
{children}
<FileProvider>
<PyodideProvider>{children}</PyodideProvider>
</FileProvider>
</div>
<div className="drawer-side shadow-md">
<label
Expand Down
11 changes: 11 additions & 0 deletions app/terminal/editor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.embedded-editor > .ace_gutter {
border-bottom-left-radius: 0.5rem;
}
.embedded-editor > .ace_scroller {
border-bottom-right-radius: 0.5rem;
border-top-right-radius: 0.5rem;
}
.embedded-editor > .ace_editor {
border-bottom-left-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
}
Loading