Skip to content

Commit f59d2fc

Browse files
authored
Merge pull request #119 from ut-code/js
javascriptのドキュメントを追加
2 parents 728a6dc + 8054bdb commit f59d2fc

20 files changed

+2459
-30
lines changed

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,59 @@ npm run lint
5555
* GOOGLE_CLIENT_IDとGOOGLE_CLIENT_SECRETにGoogle OAuthのクライアントIDとシークレット https://www.better-auth.com/docs/authentication/google
5656
* GITHUB_CLIENT_IDとGITHUB_CLIENT_SECRETにGitHub OAuthのクライアントIDとシークレット https://www.better-auth.com/docs/authentication/github
5757

58+
## ベースとなるドキュメントの作り方
59+
60+
- web版の ~~Gemini2.5Pro~~ Gemini3Pro を用いる。
61+
- 以下のプロンプトで章立てを考えさせる
62+
> `n`章前後から構成される`言語名`のチュートリアルを書こうと思います。章立てを考えてください。`言語名`以外の言語でのプログラミングはある程度やったことがある人を対象にします。
63+
>
64+
- nを8, 10, 12, 15 など変えて何回か出力させ、それを統合していい感じの章立てを決める
65+
- 実際にドキュメントを書かせる
66+
> 以下の内容で`言語名`チュートリアルの第`n`章を書いてください。他の言語でのプログラミングは経験がある人を対象にします。
67+
> タイトルにはレベル1の見出し(#), それ以降の見出しにはレベル2以下(##)を使用してください。
68+
REPLで動作可能なコード例はスクリプトではなくREPLの実行例として書いてください。
69+
> コード例はREPLの実行例では \`\`\``言語名`-repl 、ソースファイルの場合は \`\`\``言語名`:ファイル名`.拡張子` ではじまるコードブロックで示してください。ファイル名は被らないようにしてください。
70+
> また、ファイルの場合は \`\`\``言語名`-exec:ファイル名`.拡張子` のコードブロック内に実行結果例を記載してください。
71+
> また、最後には この章のまとめ セクションと、練習問題を2つほど書いてください。練習問題はこの章で学んだ内容を活用してコードを書かせるものにしてください。
72+
>
73+
> 全体の構成
74+
> `1. hoge`
75+
> `2. fuga`
76+
> `3. piyo`
77+
> `4. ...`
78+
>
79+
> `第n章: 第n章のタイトル`
80+
> `第n章内の見出し・内容の概要…`
81+
>
82+
- Gemini出力の調整
83+
- Canvasを使われた場合はやり直す。(Canvasはファイル名付きコードブロックで壊れる)
84+
- 箇条書きの最後に `<!-- end list -->` と出力される場合がある。消す
85+
- 太字がなぜか `**キーワード**` の代わりに `\*\*キーワード\*\*` となっている場合がある。 `\*\*``**` の置き換えで対応
86+
- 見出しの前に `-----` (水平線)が入る場合がある。my.code();は水平線の表示に対応しているが、消す方向で統一
87+
- `言語名-repl` にはページ内で一意なIDを追加する (例: `言語名-repl:1`)
88+
- REPLの出力部分に書かれたコメントは消えるので修正する
89+
- ダメな例
90+
````
91+
```js-repl:1
92+
> console.log("Hello")
93+
Hello // 文字列を表示する
94+
```
95+
````
96+
- 以下のようにすればok
97+
````
98+
```js-repl:1
99+
> console.log("Hello") // 文字列を表示する
100+
Hello
101+
102+
> // 文字列を表示する
103+
> console.log("Hello")
104+
Hello
105+
```
106+
````
107+
- 練習問題の見出しは「この章のまとめ」の直下のレベル3見出しで、 `### 練習問題n` または `### 練習問題n: タイトル` とする
108+
- 練習問題のファイル名は不都合がなければ `practice(章番号)_(問題番号).拡張子` で統一。空でもよいのでファイルコードブロックとexecコードブロックを置く
109+
- 1章にはたぶん練習問題要らない。
110+
58111
## markdown仕様
59112
60113
````

app/[docs_id]/markdown.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ function CodeComponent({
110110
} else if (match[2] === "-repl") {
111111
// repl付きの言語指定
112112
if (!match[3]) {
113-
console.warn(
113+
console.error(
114114
`${match[1]}-repl without terminal id! content: ${String(props.children).slice(0, 20)}...`
115115
);
116116
}

app/[docs_id]/styledSyntaxHighlighter.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export type MarkdownLang =
3232
| "sh"
3333
| "json"
3434
| "csv"
35+
| "html"
3536
| "text"
3637
| "txt";
3738

@@ -45,6 +46,7 @@ export type SyntaxHighlighterLang =
4546
| "javascript"
4647
| "typescript"
4748
| "bash"
49+
| "html"
4850
| "json";
4951
export function getSyntaxHighlighterLang(
5052
lang: MarkdownLang | undefined
@@ -70,14 +72,16 @@ export function getSyntaxHighlighterLang(
7072
return "bash";
7173
case "json":
7274
return "json";
75+
case "html":
76+
return "html";
7377
case "csv": // not supported
7478
case "text":
7579
case "txt":
7680
case undefined:
7781
return undefined;
7882
default:
7983
lang satisfies never;
80-
console.warn(`Language not listed in MarkdownLang: ${lang}`);
84+
console.error(`getSyntaxHighlighterLang() does not handle language ${lang}`);
8185
return undefined;
8286
}
8387
}

app/pagesList.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,23 @@ export const pagesList = [
3939
{ id: 12, title: "メタプログラミング入門" },
4040
],
4141
},
42+
{
43+
id: "javascript",
44+
lang: "JavaScript",
45+
description: "hoge",
46+
pages: [
47+
{ id: 1, title: "JavaScriptへようこそ" },
48+
{ id: 2, title: "基本構文とデータ型" },
49+
{ id: 3, title: "制御構文" },
50+
{ id: 4, title: "関数とクロージャ" },
51+
{ id: 5, title: "'this'の正体" },
52+
{ id: 6, title: "オブジェクトとプロトタイプ" },
53+
{ id: 7, title: "クラス構文" },
54+
{ id: 8, title: "配列とイテレーション" },
55+
{ id: 9, title: "非同期処理①: Promise" },
56+
{ id: 10, title: "非同期処理②: Async/Await" },
57+
],
58+
},
4259
{
4360
id: "cpp",
4461
lang: "C++",

app/terminal/editor.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,13 @@ export function getAceLang(lang: MarkdownLang | undefined): AceLang {
6767
case "bash":
6868
case "text":
6969
case "txt":
70+
case "html":
7071
case undefined:
7172
console.warn(`Ace editor mode not implemented for language: ${lang}`);
7273
return "text";
7374
default:
7475
lang satisfies never;
75-
console.warn(`Language not listed in MarkdownLang: ${lang}`);
76+
console.error(`getAceLang() does not handle language ${lang}`);
7677
return "text";
7778
}
7879
}

app/terminal/runtime.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,13 @@ export function getRuntimeLang(
7373
case "csv":
7474
case "text":
7575
case "txt":
76+
case "html":
7677
case undefined:
7778
// unsupported languages
7879
return undefined;
7980
default:
8081
lang satisfies never;
81-
console.warn(`Language not listed in MarkdownLang: ${lang}`);
82+
console.error(`getRuntimeLang() does not handle language ${lang}`);
8283
return undefined;
8384
}
8485
}
@@ -152,6 +153,7 @@ export function langConstants(lang: RuntimeLang | AceLang): LangConstants {
152153
return {
153154
tabSize: 2,
154155
prompt: "> ",
156+
promptMore: "... ",
155157
};
156158
case "c_cpp":
157159
case "cpp":

app/terminal/worker/jsEval.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function useJSEval() {
1414
return {
1515
...context,
1616
splitReplExamples,
17-
// getCommandlineStr,
17+
getCommandlineStr,
1818
};
1919
}
2020

@@ -24,15 +24,27 @@ function splitReplExamples(content: string): ReplCommand[] {
2424
if (line.startsWith("> ")) {
2525
// Remove the prompt from the command
2626
initCommands.push({ command: line.slice(2), output: [] });
27+
} else if (line.startsWith("... ")) {
28+
if (initCommands.length > 0) {
29+
initCommands[initCommands.length - 1].command += "\n" + line.slice(4);
30+
}
2731
} else {
2832
// Lines without prompt are output from the previous command
33+
// and the last output is return value
2934
if (initCommands.length > 0) {
35+
initCommands[initCommands.length - 1].output.forEach(
36+
(out) => (out.type = "stdout")
37+
);
3038
initCommands[initCommands.length - 1].output.push({
31-
type: "stdout",
39+
type: "return",
3240
message: line,
3341
});
3442
}
3543
}
3644
}
3745
return initCommands;
3846
}
47+
48+
function getCommandlineStr(filenames: string[]) {
49+
return `node ${filenames[0]}`;
50+
}

app/terminal/worker/jsEval.worker.ts

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,30 @@
22

33
import type { ReplOutput } from "../repl";
44
import type { MessageType, WorkerRequest, WorkerResponse } from "./runtime";
5+
import inspect from "object-inspect";
56

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

814
// Helper function to capture console output
915
const originalConsole = self.console;
1016
self.console = {
1117
...originalConsole,
12-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
13-
log: (...args: any[]) => {
14-
jsOutput.push({ type: "stdout", message: args.join(" ") });
18+
log: (...args: unknown[]) => {
19+
jsOutput.push({ type: "stdout", message: format(...args) });
1520
},
16-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
17-
error: (...args: any[]) => {
18-
jsOutput.push({ type: "stderr", message: args.join(" ") });
21+
error: (...args: unknown[]) => {
22+
jsOutput.push({ type: "stderr", message: format(...args) });
1923
},
20-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
21-
warn: (...args: any[]) => {
22-
jsOutput.push({ type: "stderr", message: args.join(" ") });
24+
warn: (...args: unknown[]) => {
25+
jsOutput.push({ type: "stderr", message: format(...args) });
2326
},
24-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
25-
info: (...args: any[]) => {
26-
jsOutput.push({ type: "stdout", message: args.join(" ") });
27+
info: (...args: unknown[]) => {
28+
jsOutput.push({ type: "stdout", message: format(...args) });
2729
},
2830
};
2931

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

3840
async function runCode({ id, payload }: WorkerRequest["runCode"]) {
39-
const { code } = payload;
41+
let { code } = payload;
4042
try {
41-
// Execute code directly with eval in the worker global scope
42-
// This will preserve variables across calls
43-
const result = self.eval(code);
43+
let result: unknown;
44+
45+
// eval()の中でconst,letを使って変数を作成した場合、
46+
// 次に実行するコマンドはスコープ外扱いでありアクセスできなくなってしまうので、
47+
// varに置き換えている
48+
if (code.trim().startsWith("const ")) {
49+
code = "var " + code.trim().slice(6);
50+
} else if (code.trim().startsWith("let ")) {
51+
code = "var " + code.trim().slice(4);
52+
}
53+
// eval()の中でclassを作成した場合も同様
54+
const classRegExp = /^\s*class\s+(\w+)/;
55+
if (classRegExp.test(code)) {
56+
code = code.replace(classRegExp, "var $1 = class $1");
57+
}
4458

45-
if (result !== undefined) {
46-
jsOutput.push({
47-
type: "return",
48-
message: String(result),
49-
});
59+
if (code.trim().startsWith("{") && code.trim().endsWith("}")) {
60+
// オブジェクトは ( ) で囲わなければならない
61+
try {
62+
result = self.eval(`(${code})`);
63+
} catch (e) {
64+
if (e instanceof SyntaxError) {
65+
// オブジェクトではなくブロックだった場合、再度普通に実行
66+
result = self.eval(code);
67+
} else {
68+
throw e;
69+
}
70+
}
71+
} else if (/^\s*await\W/.test(code)) {
72+
// promiseをawaitする場合は、promiseの部分だけをevalし、それを外からawaitする
73+
result = await self.eval(code.trim().slice(5));
74+
} else {
75+
// Execute code directly with eval in the worker global scope
76+
// This will preserve variables across calls
77+
result = self.eval(code);
5078
}
79+
80+
jsOutput.push({
81+
type: "return",
82+
message: inspect(result),
83+
});
5184
} catch (e) {
5285
originalConsole.log(e);
5386
// TODO: stack trace?
@@ -110,7 +143,8 @@ async function checkSyntax({ id, payload }: WorkerRequest["checkSyntax"]) {
110143

111144
try {
112145
// Try to create a Function to check syntax
113-
new Function(code);
146+
// new Function(code); // <- not working
147+
self.eval(`() => {${code}}`);
114148
self.postMessage({
115149
id,
116150
payload: { status: "complete" },
@@ -120,8 +154,8 @@ async function checkSyntax({ id, payload }: WorkerRequest["checkSyntax"]) {
120154
if (e instanceof SyntaxError) {
121155
// Simple heuristic: check for "Unexpected end of input"
122156
if (
123-
e.message.includes("Unexpected end of input") ||
124-
e.message.includes("expected expression")
157+
e.message.includes("Unexpected token '}'") ||
158+
e.message.includes("Unexpected end of input")
125159
) {
126160
self.postMessage({
127161
id,

package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"drizzle-orm": "^0.44.7",
3535
"mocha": "^11.7.4",
3636
"next": "<15.5",
37+
"object-inspect": "^1.13.4",
3738
"pg": "^8.16.3",
3839
"prismjs": "^1.30.0",
3940
"pyodide": "^0.29.0",
@@ -53,6 +54,7 @@
5354
"@types/chai": "^5.2.3",
5455
"@types/mocha": "^10.0.10",
5556
"@types/node": "^20",
57+
"@types/object-inspect": "^1.13.0",
5658
"@types/pg": "^8.15.5",
5759
"@types/prismjs": "^1.26.5",
5860
"@types/react": "^19",

0 commit comments

Comments
 (0)