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
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,59 @@ npm run lint
* GOOGLE_CLIENT_IDとGOOGLE_CLIENT_SECRETにGoogle OAuthのクライアントIDとシークレット https://www.better-auth.com/docs/authentication/google
* GITHUB_CLIENT_IDとGITHUB_CLIENT_SECRETにGitHub OAuthのクライアントIDとシークレット https://www.better-auth.com/docs/authentication/github

## ベースとなるドキュメントの作り方

- web版の ~~Gemini2.5Pro~~ Gemini3Pro を用いる。
- 以下のプロンプトで章立てを考えさせる
> `n`章前後から構成される`言語名`のチュートリアルを書こうと思います。章立てを考えてください。`言語名`以外の言語でのプログラミングはある程度やったことがある人を対象にします。
>
- nを8, 10, 12, 15 など変えて何回か出力させ、それを統合していい感じの章立てを決める
- 実際にドキュメントを書かせる
> 以下の内容で`言語名`チュートリアルの第`n`章を書いてください。他の言語でのプログラミングは経験がある人を対象にします。
> タイトルにはレベル1の見出し(#), それ以降の見出しにはレベル2以下(##)を使用してください。
REPLで動作可能なコード例はスクリプトではなくREPLの実行例として書いてください。
> コード例はREPLの実行例では \`\`\``言語名`-repl 、ソースファイルの場合は \`\`\``言語名`:ファイル名`.拡張子` ではじまるコードブロックで示してください。ファイル名は被らないようにしてください。
> また、ファイルの場合は \`\`\``言語名`-exec:ファイル名`.拡張子` のコードブロック内に実行結果例を記載してください。
> また、最後には この章のまとめ セクションと、練習問題を2つほど書いてください。練習問題はこの章で学んだ内容を活用してコードを書かせるものにしてください。
>
> 全体の構成
> `1. hoge`
> `2. fuga`
> `3. piyo`
> `4. ...`
>
> `第n章: 第n章のタイトル`
> `第n章内の見出し・内容の概要…`
>
- Gemini出力の調整
- Canvasを使われた場合はやり直す。(Canvasはファイル名付きコードブロックで壊れる)
- 箇条書きの最後に `<!-- end list -->` と出力される場合がある。消す
- 太字がなぜか `**キーワード**` の代わりに `\*\*キーワード\*\*` となっている場合がある。 `\*\*` → `**` の置き換えで対応
- 見出しの前に `-----` (水平線)が入る場合がある。my.code();は水平線の表示に対応しているが、消す方向で統一
- `言語名-repl` にはページ内で一意なIDを追加する (例: `言語名-repl:1`)
- REPLの出力部分に書かれたコメントは消えるので修正する
- ダメな例
````
```js-repl:1
> console.log("Hello")
Hello // 文字列を表示する
```
````
- 以下のようにすればok
````
```js-repl:1
> console.log("Hello") // 文字列を表示する
Hello

> // 文字列を表示する
> console.log("Hello")
Hello
```
````
- 練習問題の見出しは「この章のまとめ」の直下のレベル3見出しで、 `### 練習問題n` または `### 練習問題n: タイトル` とする
- 練習問題のファイル名は不都合がなければ `practice(章番号)_(問題番号).拡張子` で統一。空でもよいのでファイルコードブロックとexecコードブロックを置く
- 1章にはたぶん練習問題要らない。

## markdown仕様

````
Expand Down
2 changes: 1 addition & 1 deletion app/[docs_id]/markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ function CodeComponent({
} else if (match[2] === "-repl") {
// repl付きの言語指定
if (!match[3]) {
console.warn(
console.error(
`${match[1]}-repl without terminal id! content: ${String(props.children).slice(0, 20)}...`
);
}
Expand Down
6 changes: 5 additions & 1 deletion app/[docs_id]/styledSyntaxHighlighter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type MarkdownLang =
| "sh"
| "json"
| "csv"
| "html"
| "text"
| "txt";

Expand All @@ -45,6 +46,7 @@ export type SyntaxHighlighterLang =
| "javascript"
| "typescript"
| "bash"
| "html"
| "json";
export function getSyntaxHighlighterLang(
lang: MarkdownLang | undefined
Expand All @@ -70,14 +72,16 @@ export function getSyntaxHighlighterLang(
return "bash";
case "json":
return "json";
case "html":
return "html";
case "csv": // not supported
case "text":
case "txt":
case undefined:
return undefined;
default:
lang satisfies never;
console.warn(`Language not listed in MarkdownLang: ${lang}`);
console.error(`getSyntaxHighlighterLang() does not handle language ${lang}`);
return undefined;
}
}
Expand Down
17 changes: 17 additions & 0 deletions app/pagesList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@ export const pagesList = [
{ id: 12, title: "メタプログラミング入門" },
],
},
{
id: "javascript",
lang: "JavaScript",
description: "hoge",
pages: [
{ id: 1, title: "JavaScriptへようこそ" },
{ id: 2, title: "基本構文とデータ型" },
{ id: 3, title: "制御構文" },
{ id: 4, title: "関数とクロージャ" },
{ id: 5, title: "'this'の正体" },
{ id: 6, title: "オブジェクトとプロトタイプ" },
{ id: 7, title: "クラス構文" },
{ id: 8, title: "配列とイテレーション" },
{ id: 9, title: "非同期処理①: Promise" },
{ id: 10, title: "非同期処理②: Async/Await" },
],
},
{
id: "cpp",
lang: "C++",
Expand Down
3 changes: 2 additions & 1 deletion app/terminal/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,13 @@ export function getAceLang(lang: MarkdownLang | undefined): AceLang {
case "bash":
case "text":
case "txt":
case "html":
case undefined:
console.warn(`Ace editor mode not implemented for language: ${lang}`);
return "text";
default:
lang satisfies never;
console.warn(`Language not listed in MarkdownLang: ${lang}`);
console.error(`getAceLang() does not handle language ${lang}`);
return "text";
}
}
Expand Down
4 changes: 3 additions & 1 deletion app/terminal/runtime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,13 @@ export function getRuntimeLang(
case "csv":
case "text":
case "txt":
case "html":
case undefined:
// unsupported languages
return undefined;
default:
lang satisfies never;
console.warn(`Language not listed in MarkdownLang: ${lang}`);
console.error(`getRuntimeLang() does not handle language ${lang}`);
return undefined;
}
}
Expand Down Expand Up @@ -152,6 +153,7 @@ export function langConstants(lang: RuntimeLang | AceLang): LangConstants {
return {
tabSize: 2,
prompt: "> ",
promptMore: "... ",
};
case "c_cpp":
case "cpp":
Expand Down
16 changes: 14 additions & 2 deletions app/terminal/worker/jsEval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function useJSEval() {
return {
...context,
splitReplExamples,
// getCommandlineStr,
getCommandlineStr,
};
}

Expand All @@ -24,15 +24,27 @@ function splitReplExamples(content: string): ReplCommand[] {
if (line.startsWith("> ")) {
// Remove the prompt from the command
initCommands.push({ command: line.slice(2), output: [] });
} else if (line.startsWith("... ")) {
if (initCommands.length > 0) {
initCommands[initCommands.length - 1].command += "\n" + line.slice(4);
}
} else {
// Lines without prompt are output from the previous command
// and the last output is return value
if (initCommands.length > 0) {
initCommands[initCommands.length - 1].output.forEach(
(out) => (out.type = "stdout")
);
initCommands[initCommands.length - 1].output.push({
type: "stdout",
type: "return",
message: line,
});
}
}
}
return initCommands;
}

function getCommandlineStr(filenames: string[]) {
return `node ${filenames[0]}`;
}
82 changes: 58 additions & 24 deletions app/terminal/worker/jsEval.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,30 @@

import type { ReplOutput } from "../repl";
import type { MessageType, WorkerRequest, WorkerResponse } from "./runtime";
import inspect from "object-inspect";

function format(...args: unknown[]): string {
// TODO: console.logの第1引数はフォーマット指定文字列を取ることができる
// https://nodejs.org/api/util.html#utilformatformat-args
return args.map((a) => (typeof a === "string" ? a : inspect(a))).join(" ");
}
let jsOutput: ReplOutput[] = [];

// Helper function to capture console output
const originalConsole = self.console;
self.console = {
...originalConsole,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
log: (...args: any[]) => {
jsOutput.push({ type: "stdout", message: args.join(" ") });
log: (...args: unknown[]) => {
jsOutput.push({ type: "stdout", message: format(...args) });
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: (...args: any[]) => {
jsOutput.push({ type: "stderr", message: args.join(" ") });
error: (...args: unknown[]) => {
jsOutput.push({ type: "stderr", message: format(...args) });
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
warn: (...args: any[]) => {
jsOutput.push({ type: "stderr", message: args.join(" ") });
warn: (...args: unknown[]) => {
jsOutput.push({ type: "stderr", message: format(...args) });
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
info: (...args: any[]) => {
jsOutput.push({ type: "stdout", message: args.join(" ") });
info: (...args: unknown[]) => {
jsOutput.push({ type: "stdout", message: format(...args) });
},
};

Expand All @@ -36,18 +38,49 @@ async function init({ id }: WorkerRequest["init"]) {
}

async function runCode({ id, payload }: WorkerRequest["runCode"]) {
const { code } = payload;
let { code } = payload;
try {
// Execute code directly with eval in the worker global scope
// This will preserve variables across calls
const result = self.eval(code);
let result: unknown;

// eval()の中でconst,letを使って変数を作成した場合、
// 次に実行するコマンドはスコープ外扱いでありアクセスできなくなってしまうので、
// varに置き換えている
if (code.trim().startsWith("const ")) {
code = "var " + code.trim().slice(6);
} else if (code.trim().startsWith("let ")) {
code = "var " + code.trim().slice(4);
}
// eval()の中でclassを作成した場合も同様
const classRegExp = /^\s*class\s+(\w+)/;
if (classRegExp.test(code)) {
code = code.replace(classRegExp, "var $1 = class $1");
}

if (result !== undefined) {
jsOutput.push({
type: "return",
message: String(result),
});
if (code.trim().startsWith("{") && code.trim().endsWith("}")) {
// オブジェクトは ( ) で囲わなければならない
try {
result = self.eval(`(${code})`);
} catch (e) {
if (e instanceof SyntaxError) {
// オブジェクトではなくブロックだった場合、再度普通に実行
result = self.eval(code);
} else {
throw e;
}
}
} else if (/^\s*await\W/.test(code)) {
// promiseをawaitする場合は、promiseの部分だけをevalし、それを外からawaitする
result = await self.eval(code.trim().slice(5));
} else {
// Execute code directly with eval in the worker global scope
// This will preserve variables across calls
result = self.eval(code);
}

jsOutput.push({
type: "return",
message: inspect(result),
});
} catch (e) {
originalConsole.log(e);
// TODO: stack trace?
Expand Down Expand Up @@ -110,7 +143,8 @@ async function checkSyntax({ id, payload }: WorkerRequest["checkSyntax"]) {

try {
// Try to create a Function to check syntax
new Function(code);
// new Function(code); // <- not working
self.eval(`() => {${code}}`);
self.postMessage({
id,
payload: { status: "complete" },
Expand All @@ -120,8 +154,8 @@ async function checkSyntax({ id, payload }: WorkerRequest["checkSyntax"]) {
if (e instanceof SyntaxError) {
// Simple heuristic: check for "Unexpected end of input"
if (
e.message.includes("Unexpected end of input") ||
e.message.includes("expected expression")
e.message.includes("Unexpected token '}'") ||
e.message.includes("Unexpected end of input")
) {
self.postMessage({
id,
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"drizzle-orm": "^0.44.7",
"mocha": "^11.7.4",
"next": "<15.5",
"object-inspect": "^1.13.4",
"pg": "^8.16.3",
"prismjs": "^1.30.0",
"pyodide": "^0.29.0",
Expand All @@ -53,6 +54,7 @@
"@types/chai": "^5.2.3",
"@types/mocha": "^10.0.10",
"@types/node": "^20",
"@types/object-inspect": "^1.13.0",
"@types/pg": "^8.15.5",
"@types/prismjs": "^1.26.5",
"@types/react": "^19",
Expand Down
Loading