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 +58 -0
- package/README.md +45 -23
- package/dist/atcoder/new.js +6 -1
- package/dist/atcoder/parser/problem-page.d.ts +2 -1
- package/dist/atcoder/parser/problem-page.js +340 -6
- package/dist/cli.js +177 -34
- package/dist/config/config-store.d.ts +2 -0
- package/dist/config/config-store.js +5 -5
- package/dist/utils/i18n.d.ts +64 -0
- package/dist/utils/i18n.js +67 -3
- package/dist/workspace/initializer.d.ts +14 -0
- package/dist/workspace/initializer.js +110 -25
- package/package.json +3 -3
- package/src/atcoder/new.test.ts +140 -0
- package/src/atcoder/new.ts +7 -1
- package/src/atcoder/parser/problem-page.test.ts +125 -0
- package/src/atcoder/parser/problem-page.ts +359 -6
- package/src/cli.ts +207 -36
- package/src/config/config-store.ts +7 -5
- package/src/test-runner/runner.test.ts +2 -0
- package/src/utils/i18n.ts +67 -3
- package/src/workspace/initializer.test.ts +125 -0
- package/src/workspace/initializer.ts +128 -27
- package/THIRD_PARTY_LICENSES +0 -21
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
|
-
|
|
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
|
-
|
|
108
|
+
---
|
|
90
109
|
|
|
91
|
-
|
|
92
|
-
* **online-judge-tools (oj)**(online-judge-tools Organization)
|
|
110
|
+
## 謝辞とライセンス
|
|
93
111
|
|
|
94
|
-
|
|
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
|
+
|
package/dist/atcoder/new.js
CHANGED
|
@@ -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
|
|
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
|
|
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 ``;
|
|
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 (
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
}
|