atcoder-workspace 1.1.0-beta.1 → 1.1.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -19,3 +19,61 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
+
23
+ ---
24
+
25
+ ### Third-party Software Licenses
26
+
27
+ This project includes code derived from the following third-party projects:
28
+
29
+ 1. atcoder-cli
30
+ Copyright (c) 2018 Tatamo
31
+ Licensed under the BSD 3-Clause License:
32
+
33
+ Redistribution and use in source and binary forms, with or without
34
+ modification, are permitted provided that the following conditions are met:
35
+
36
+ * Redistributions of source code must retain the above copyright notice, this
37
+ list of conditions and the following disclaimer.
38
+
39
+ * Redistributions in binary form must reproduce the above copyright notice,
40
+ this list of conditions and the following disclaimer in the documentation
41
+ and/or other materials provided with the distribution.
42
+
43
+ * Neither the name of the copyright holder nor the names of its
44
+ contributors may be used to endorse or promote products derived from
45
+ this software without specific prior written permission.
46
+
47
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
48
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
49
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
50
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
51
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
52
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
53
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
54
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
55
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
56
+ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
57
+
58
+ 2. online-judge-tools
59
+ Copyright (c) 2018 Kimiyuki Onaka
60
+ Licensed under the MIT License:
61
+
62
+ Permission is hereby granted, free of charge, to any person obtaining a copy
63
+ of this software and associated documentation files (the "Software"), to deal
64
+ in the Software without restriction, including without limitation the rights
65
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
66
+ copies of the Software, and to permit persons to whom the Software is
67
+ furnished to do so, subject to the following conditions:
68
+
69
+ The above copyright notice and this permission notice shall be included in all
70
+ copies or substantial portions of the Software.
71
+
72
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
73
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
74
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
75
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
76
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
77
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
78
+ SOFTWARE.
79
+
package/README.md CHANGED
@@ -1,9 +1,8 @@
1
1
  # AtCoder Workspace (atc)
2
2
 
3
- **Node.js TypeScript で開発された、ローカルファーストの AtCoder 向け CLI ツールです。**
4
-
5
- AtCoder Workspace は、テンプレート、ログイン状態、テストケースなどをすべてワークスペース内で完結して管理するよう設計されています。グローバル環境を汚さず、どの環境でも同じ設定をそのまま利用できます。
3
+ AtCoder Workspace は、AtCoder のコンテスト環境を効率的に構築・管理するための、TypeScript 製の CLI ツールです。
6
4
 
5
+ `online-judge-tools (oj)` を別途インストールすることなく、テストケースの取得、ローカルテスト、ログイン状態の管理、テンプレート生成をすべて単一のワークスペース内で完結させることができます。
7
6
 
8
7
  ## インストール
9
8
 
@@ -26,17 +25,6 @@ atc whoami # 現在ログイン中のユーザー名を表
26
25
  atc logout # 保存されているセッション情報を削除
27
26
  ```
28
27
 
29
- ### 問題取得・テスト・提出
30
-
31
- ```bash
32
- atc new <contest> [task] # 問題文・サンプルケースを取得し、テンプレート付きで作業ディレクトリを作成
33
- atc new <contest> --all # コンテスト内の全問題を一括で作成
34
- atc test [task] [file] # ローカルのサンプルケースでコンパイル・実行してテスト
35
- atc submit [task] [file] # ローカルテストを実施し、確認後に提出・ジャッジ結果を取得
36
- ```
37
-
38
- ---
39
-
40
28
  ## ワークスペース構成
41
29
 
42
30
  初期化すると、ワークスペースは次のような構成になります。
@@ -55,11 +43,36 @@ my-atcoder-workspace/
55
43
  │ └── b/
56
44
  ```
57
45
 
46
+
47
+ ### 問題取得・テスト・提出
48
+
49
+ ```bash
50
+ atc new <contest> # 問題文・サンプルケースを取得し、テンプレート付きで作業ディレクトリを作成
51
+ atc new <contest> --all # コンテスト内の全問題を一括で作成
52
+ atc test [arg1] [arg2] # ローカルのサンプルケースでコンパイル・実行してテスト
53
+ atc submit [arg1] [arg2] # ローカルテストを実施し、確認後に提出・ジャッジ結果を取得
54
+ ```
55
+ `atc test`または`atc submit`は、特定のコンテストディレクトリに移動するか、引数にコンテスト名と問題名を指定することで実行できます。
56
+
57
+ #### 例
58
+ ```bash
59
+ atc new abc300
60
+ atc test abc300 a
61
+ atc submit abc300 a
62
+
63
+ # または
64
+
65
+ atc new abc300
66
+ cd abc300/a
67
+ atc test
68
+ atc submit
69
+ ```
70
+
58
71
  ---
59
72
 
60
73
  ## 設定ファイル(`.atcoder-cli/config.json`)
61
74
 
62
- デフォルトの設定ファイルでは、使用する言語やビルド・実行コマンド、テンプレート、テストディレクトリなどを自由にカスタマイズできます。
75
+ デフォルトの設定ファイルでは、使用する言語やビルド・実行コマンド、テンプレート、サンプルケースのディレクトリなどを自由にカスタマイズできます。
63
76
 
64
77
  ```json
65
78
  {
@@ -78,21 +91,30 @@ my-atcoder-workspace/
78
91
  "run": "python3 main.py"
79
92
  }
80
93
  },
81
- "testDirName": "tests"
94
+ "testDirName": "tests",
95
+ "contestDir": "",
96
+ "lang": "en",
97
+ "extractProblemStatement": false,
98
+ "problemLang": "ja"
82
99
  }
83
100
  ```
84
101
 
85
- ---
102
+ ### 追加の設定オプション
86
103
 
87
- ## 謝辞
104
+ * **`lang`** (`"en"` | `"ja"`、デフォルト: `"en"`): CLIが表示するメッセージや警告などの表示言語を設定します。
105
+ * **`extractProblemStatement`** (`boolean`、デフォルト: `false`): `true` に設定すると、問題のセットアップ時に `problem.md` として問題文を Markdown 形式で自動抽出します。抽出された `problem.md` は自動的に `.gitignore` の対象となるため、パブリックリポジトリ等へ誤ってコミットされる心配はありません。
106
+ * **`problemLang`** (`"en"` | `"ja"`、デフォルト: `"ja"`): `extractProblemStatement` で抽出する問題文の言語を設定します。未設定の場合は `lang` の設定値にフォールバックします。
88
107
 
89
- AtCoder Workspace は、以下のプロジェクトに着想を得ています。
108
+ ---
90
109
 
91
- * **atcoder-cli (acc)**(開発者: Tatamo)
92
- * **online-judge-tools (oj)**(online-judge-tools Organization)
110
+ ## 謝辞とライセンス
93
111
 
94
- 各ライセンスの詳細については、`THIRD_PARTY_LICENSES` をご覧ください。
112
+ AtCoder Workspace は、以下の素晴らしいプロジェクトに着想を得ており、一部のコードにおいてそれらの実装を複製または改変して利用しています。
113
+
114
+ * **atcoder-cli (acc)** (開発者: Tatamo, BSD 3-Clause License)
115
+ * **online-judge-tools (oj)** (online-judge-tools Organization, MIT License)
95
116
 
96
117
  ## ライセンス
97
118
 
98
- MIT License
119
+ 本プロジェクトは **MIT License** で公開されていますが、一部のコードには上記のサードパーティ製ライセンスが適用されます。詳細な著作権表示およびライセンス文は [LICENSE](file:///Users/dakoyo/src/github.com/dakoyo/atCoder2/LICENSE) を参照してください。
120
+
@@ -94,7 +94,8 @@ async function setupTask(workspaceRoot, contestId, task, languageKey) {
94
94
  catch (err) {
95
95
  throw new errors_1.AtcError(`Failed to fetch problem page for "${task.id}": ${err.message}`);
96
96
  }
97
- const problemDetails = (0, problem_page_1.parseProblemPage)(problemHtml);
97
+ const preferredProblemLang = config.problemLang || config.lang;
98
+ const problemDetails = (0, problem_page_1.parseProblemPage)(problemHtml, preferredProblemLang);
98
99
  // Write sample cases to tests/ directory
99
100
  const testDirName = config.testDirName || 'tests';
100
101
  const testDir = path.join(taskDir, testDirName);
@@ -114,6 +115,10 @@ async function setupTask(workspaceRoot, contestId, task, languageKey) {
114
115
  fs.writeFileSync(path.join(testDir, `sample-${sample.index}.in`), sample.input, 'utf8');
115
116
  fs.writeFileSync(path.join(testDir, `sample-${sample.index}.out`), sample.output, 'utf8');
116
117
  }
118
+ // Write problem statement if enabled in config
119
+ if (config.extractProblemStatement && problemDetails.problemStatementMd) {
120
+ fs.writeFileSync(path.join(taskDir, 'problem.md'), problemDetails.problemStatementMd, 'utf8');
121
+ }
117
122
  return {
118
123
  contestId,
119
124
  taskLabel: task.label,
@@ -8,5 +8,6 @@ export interface TaskDetails {
8
8
  timeLimitMs: number;
9
9
  memoryLimitBytes: number;
10
10
  samples: SampleCase[];
11
+ problemStatementMd?: string;
11
12
  }
12
- export declare function parseProblemPage(html: string): TaskDetails;
13
+ export declare function parseProblemPage(html: string, preferredLang?: 'en' | 'ja'): TaskDetails;
@@ -36,7 +36,317 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.parseProblemPage = parseProblemPage;
37
37
  const cheerio = __importStar(require("cheerio"));
38
38
  const limits_1 = require("./limits");
39
- function parseProblemPage(html) {
39
+ function escapeMarkdown(text) {
40
+ return text
41
+ .replace(/\\/g, '\\\\')
42
+ .replace(/\*/g, '\\*')
43
+ .replace(/_/g, '\\_')
44
+ .replace(/`/g, '\\`')
45
+ .replace(/\[/g, '\\[')
46
+ .replace(/\]/g, '\\]');
47
+ }
48
+ function convertLineToMath(line) {
49
+ const trimmed = line.trim();
50
+ if (trimmed === ':' || trimmed === '\\vdots') {
51
+ return '\\vdots';
52
+ }
53
+ if (trimmed === '...' || trimmed === '\\dots' || trimmed === '\\cdots') {
54
+ return '\\dots';
55
+ }
56
+ let result = '';
57
+ let i = 0;
58
+ let textBuffer = '';
59
+ const flushBuffer = () => {
60
+ if (textBuffer) {
61
+ result += `\\text{${textBuffer}}`;
62
+ textBuffer = '';
63
+ }
64
+ };
65
+ while (i < line.length) {
66
+ const remaining = line.slice(i);
67
+ // 0. Match existing \text{...} optionally followed by a subscript
68
+ const textSubMatch = remaining.match(/^\\text\{([^{}]+)\}(?:_([a-zA-Z0-9]+|\{[^{}]+\}))?/);
69
+ if (textSubMatch) {
70
+ flushBuffer();
71
+ const baseText = textSubMatch[1];
72
+ const subText = textSubMatch[2];
73
+ if (subText) {
74
+ const formattedSub = (subText.startsWith('{') && subText.endsWith('}'))
75
+ ? subText
76
+ : (subText.length > 1 ? `{${subText}}` : subText);
77
+ result += `\\text{${baseText}}_${formattedSub}`;
78
+ }
79
+ else {
80
+ result += `\\text{${baseText}}`;
81
+ }
82
+ i += textSubMatch[0].length;
83
+ continue;
84
+ }
85
+ // 1. Match LaTeX commands like \dots, \vdots, \cdots
86
+ const cmdMatch = remaining.match(/^(\\[a-zA-Z]+)/);
87
+ if (cmdMatch) {
88
+ flushBuffer();
89
+ result += cmdMatch[1];
90
+ i += cmdMatch[1].length;
91
+ continue;
92
+ }
93
+ // 2. Match subscript pattern: e.g. A_i, query_1
94
+ const subMatch = remaining.match(/^([a-zA-Z]+)_([a-zA-Z0-9]+)/);
95
+ if (subMatch) {
96
+ flushBuffer();
97
+ const base = subMatch[1];
98
+ const sub = subMatch[2];
99
+ const formattedSub = sub.length > 1 ? `\\text{${sub}}` : sub;
100
+ result += `\\text{${base}}_${formattedSub}`;
101
+ i += subMatch[0].length;
102
+ continue;
103
+ }
104
+ // 3. Match raw ellipsis "..."
105
+ if (remaining.startsWith('...')) {
106
+ flushBuffer();
107
+ result += '\\dots';
108
+ i += 3;
109
+ continue;
110
+ }
111
+ // 4. Match colon ":"
112
+ if (remaining.startsWith(':')) {
113
+ flushBuffer();
114
+ result += '\\vdots';
115
+ i += 1;
116
+ continue;
117
+ }
118
+ // 5. Normal character (including spaces)
119
+ textBuffer += line[i];
120
+ i += 1;
121
+ }
122
+ flushBuffer();
123
+ return result;
124
+ }
125
+ function renderPreAsMathArray(node, $) {
126
+ const rawText = $(node).text();
127
+ const lines = rawText.split('\n');
128
+ let startIdx = 0;
129
+ while (startIdx < lines.length && lines[startIdx].trim() === '') {
130
+ startIdx++;
131
+ }
132
+ let endIdx = lines.length - 1;
133
+ while (endIdx >= startIdx && lines[endIdx].trim() === '') {
134
+ endIdx--;
135
+ }
136
+ const activeLines = lines.slice(startIdx, endIdx + 1);
137
+ if (activeLines.length === 0) {
138
+ return '';
139
+ }
140
+ const convertedLines = activeLines.map(line => convertLineToMath(line));
141
+ let result = '\n\n$$\n\\begin{array}{l}\n';
142
+ result += convertedLines.join(' \\\\\n');
143
+ result += '\n\\end{array}\n$$\n\n';
144
+ return result;
145
+ }
146
+ function processTextNode(text) {
147
+ const parts = [];
148
+ let i = 0;
149
+ while (i < text.length) {
150
+ if (text.startsWith('\\[', i)) {
151
+ const start = i;
152
+ const end = text.indexOf('\\]', i + 2);
153
+ if (end !== -1) {
154
+ const mathContent = text.slice(start + 2, end);
155
+ parts.push(`$$\n${mathContent.trim()}\n$$`);
156
+ i = end + 2;
157
+ }
158
+ else {
159
+ parts.push(escapeMarkdown(text.slice(i, i + 2)));
160
+ i += 2;
161
+ }
162
+ }
163
+ else if (text.startsWith('\\(', i)) {
164
+ const start = i;
165
+ const end = text.indexOf('\\)', i + 2);
166
+ if (end !== -1) {
167
+ const mathContent = text.slice(start + 2, end);
168
+ parts.push(`$${mathContent.trim()}$`);
169
+ i = end + 2;
170
+ }
171
+ else {
172
+ parts.push(escapeMarkdown(text.slice(i, i + 2)));
173
+ i += 2;
174
+ }
175
+ }
176
+ else if (text.startsWith('$$', i)) {
177
+ const start = i;
178
+ const end = text.indexOf('$$', i + 2);
179
+ if (end !== -1) {
180
+ const mathContent = text.slice(start + 2, end);
181
+ parts.push(`$$\n${mathContent.trim()}\n$$`);
182
+ i = end + 2;
183
+ }
184
+ else {
185
+ parts.push(escapeMarkdown(text.slice(i, i + 2)));
186
+ i += 2;
187
+ }
188
+ }
189
+ else if (text.startsWith('$', i)) {
190
+ const start = i;
191
+ const end = text.indexOf('$', i + 1);
192
+ if (end !== -1) {
193
+ const mathContent = text.slice(start + 1, end);
194
+ parts.push(`$${mathContent.trim()}$`);
195
+ i = end + 1;
196
+ }
197
+ else {
198
+ parts.push('$');
199
+ i += 1;
200
+ }
201
+ }
202
+ else {
203
+ let nextIndex = -1;
204
+ let selectedDelim = '';
205
+ const delims = ['\\[', '\\(', '$$', '$'];
206
+ for (const delim of delims) {
207
+ const idx = text.indexOf(delim, i);
208
+ if (idx !== -1 && (nextIndex === -1 || idx < nextIndex)) {
209
+ nextIndex = idx;
210
+ selectedDelim = delim;
211
+ }
212
+ }
213
+ if (nextIndex === -1) {
214
+ parts.push(escapeMarkdown(text.slice(i)));
215
+ break;
216
+ }
217
+ else {
218
+ parts.push(escapeMarkdown(text.slice(i, nextIndex)));
219
+ i = nextIndex;
220
+ }
221
+ }
222
+ }
223
+ return parts.join('');
224
+ }
225
+ function nodeToMarkdown(node, $, isInsideMath = false) {
226
+ if (node.type === 'text') {
227
+ return isInsideMath ? (node.data || '') : processTextNode(node.data || '');
228
+ }
229
+ if (node.type === 'tag') {
230
+ const tagName = node.name;
231
+ const className = $(node).attr('class') || '';
232
+ const classes = className.split(/\s+/);
233
+ const isMathContainer = tagName === 'var' || classes.includes('math');
234
+ const isBlockMath = tagName === 'div' && classes.includes('math');
235
+ if (isMathContainer) {
236
+ let mathBody = '';
237
+ if (node.children) {
238
+ for (const child of node.children) {
239
+ mathBody += nodeToMarkdown(child, $, true);
240
+ }
241
+ }
242
+ const delim = isBlockMath ? '$$' : '$';
243
+ if (isBlockMath) {
244
+ return `\n\n$$\n${mathBody.trim()}\n$$\n\n`;
245
+ }
246
+ return `${delim}${mathBody.trim()}${delim}`;
247
+ }
248
+ if (tagName === 'table') {
249
+ const $table = $(node).clone();
250
+ $table.find('a').each((_, el) => {
251
+ const href = $(el).attr('href');
252
+ if (href && href.startsWith('/') && !href.startsWith('//')) {
253
+ $(el).attr('href', 'https://atcoder.jp' + href);
254
+ }
255
+ });
256
+ $table.find('img').each((_, el) => {
257
+ const src = $(el).attr('src');
258
+ if (src && src.startsWith('/') && !src.startsWith('//')) {
259
+ $(el).attr('src', 'https://atcoder.jp' + src);
260
+ }
261
+ });
262
+ return '\n\n' + $.html($table) + '\n\n';
263
+ }
264
+ let childrenMarkdown = '';
265
+ if (node.children) {
266
+ for (const child of node.children) {
267
+ childrenMarkdown += nodeToMarkdown(child, $, isInsideMath);
268
+ }
269
+ }
270
+ switch (tagName) {
271
+ case 'h1':
272
+ case 'h2':
273
+ case 'h3':
274
+ case 'h4':
275
+ case 'h5':
276
+ case 'h6': {
277
+ const level = parseInt(tagName.substring(1), 10);
278
+ return `\n\n${'#'.repeat(level)} ${childrenMarkdown.trim()}\n\n`;
279
+ }
280
+ case 'p':
281
+ return `\n\n${childrenMarkdown.trim()}\n\n`;
282
+ case 'br':
283
+ return '\n';
284
+ case 'strong':
285
+ case 'b':
286
+ return `**${childrenMarkdown.trim()}**`;
287
+ case 'em':
288
+ case 'i':
289
+ return `*${childrenMarkdown.trim()}*`;
290
+ case 'code': {
291
+ const parentTagName = node.parent?.name;
292
+ if (parentTagName === 'pre') {
293
+ return childrenMarkdown;
294
+ }
295
+ return `\`${childrenMarkdown}\``;
296
+ }
297
+ case 'pre': {
298
+ const text = $(node).text();
299
+ const hasVar = $(node).find('var').length > 0;
300
+ const hasMathSymbols = text.includes('\\dots') || text.includes('\\vdots') || text.includes('\\cdots') || text.includes('\\le');
301
+ if (hasVar || hasMathSymbols) {
302
+ return renderPreAsMathArray(node, $);
303
+ }
304
+ return `\n\n\`\`\`\n${text.trim()}\n\`\`\`\n\n`;
305
+ }
306
+ case 'a': {
307
+ let href = $(node).attr('href') || '';
308
+ if (href.startsWith('/') && !href.startsWith('//')) {
309
+ href = 'https://atcoder.jp' + href;
310
+ }
311
+ return `[${childrenMarkdown.trim()}](${href})`;
312
+ }
313
+ case 'img': {
314
+ let src = $(node).attr('src') || '';
315
+ if (src.startsWith('/') && !src.startsWith('//')) {
316
+ src = 'https://atcoder.jp' + src;
317
+ }
318
+ const alt = $(node).attr('alt') || '';
319
+ return `![${alt}](${src})`;
320
+ }
321
+ case 'ul':
322
+ case 'ol':
323
+ return `\n\n${childrenMarkdown}\n\n`;
324
+ case 'li': {
325
+ let indentLevel = 0;
326
+ let parent = node.parent;
327
+ while (parent) {
328
+ const parentName = parent.name;
329
+ if (parentName === 'ul' || parentName === 'ol') {
330
+ indentLevel++;
331
+ }
332
+ parent = parent.parent;
333
+ }
334
+ const indent = ' '.repeat(Math.max(0, indentLevel - 1));
335
+ const parentTagName = node.parent?.name;
336
+ const prefix = parentTagName === 'ol' ? '1. ' : '- ';
337
+ return `\n${indent}${prefix}${childrenMarkdown.trim()}`;
338
+ }
339
+ case 'div':
340
+ case 'span':
341
+ case 'section':
342
+ return childrenMarkdown;
343
+ default:
344
+ return childrenMarkdown;
345
+ }
346
+ }
347
+ return '';
348
+ }
349
+ function parseProblemPage(html, preferredLang) {
40
350
  const $ = cheerio.load(html);
41
351
  let title = $('span.h2').first().text().trim();
42
352
  if (!title) {
@@ -72,11 +382,34 @@ function parseProblemPage(html) {
72
382
  }
73
383
  }
74
384
  let $container = $('#task-statement');
75
- if ($container.find('.lang-en').length > 0) {
76
- $container = $container.find('.lang-en');
385
+ if (preferredLang === 'ja') {
386
+ if ($container.find('.lang-ja').length > 0) {
387
+ $container = $container.find('.lang-ja');
388
+ }
389
+ else if ($container.find('.lang-en').length > 0) {
390
+ $container = $container.find('.lang-en');
391
+ }
392
+ }
393
+ else {
394
+ if ($container.find('.lang-en').length > 0) {
395
+ $container = $container.find('.lang-en');
396
+ }
397
+ else if ($container.find('.lang-ja').length > 0) {
398
+ $container = $container.find('.lang-ja');
399
+ }
77
400
  }
78
- else if ($container.find('.lang-ja').length > 0) {
79
- $container = $container.find('.lang-ja');
401
+ // Convert task statement HTML to Markdown if task statement exists
402
+ let problemStatementMd = '';
403
+ if ($container.length > 0) {
404
+ let mdBody = '';
405
+ $container.contents().each((_, node) => {
406
+ mdBody += nodeToMarkdown(node, $);
407
+ });
408
+ mdBody = mdBody.replace(/\n{3,}/g, '\n\n').trim();
409
+ problemStatementMd = `# ${title}\n\n`;
410
+ problemStatementMd += `- Time Limit: ${timeLimitMs / 1000} sec\n`;
411
+ problemStatementMd += `- Memory Limit: ${Math.round(memoryLimitBytes / (1024 * 1024))} MB\n\n`;
412
+ problemStatementMd += mdBody + '\n';
80
413
  }
81
414
  const sampleMap = {};
82
415
  const inputRegex = /(?:Sample\s+Input|入力例)\s*(?:#|No\.?)?\s*(\d+)/i;
@@ -131,6 +464,7 @@ function parseProblemPage(html) {
131
464
  title,
132
465
  timeLimitMs,
133
466
  memoryLimitBytes,
134
- samples
467
+ samples,
468
+ problemStatementMd
135
469
  };
136
470
  }