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
|
@@ -33,11 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.LANGUAGE_PRESETS = exports.DEFAULT_PYTHON_TEMPLATE = exports.DEFAULT_CPP_TEMPLATE = void 0;
|
|
36
37
|
exports.initWorkspace = initWorkspace;
|
|
38
|
+
exports.addLanguage = addLanguage;
|
|
37
39
|
const fs = __importStar(require("fs"));
|
|
38
40
|
const path = __importStar(require("path"));
|
|
39
41
|
const config_store_1 = require("../config/config-store");
|
|
40
|
-
const
|
|
42
|
+
const errors_1 = require("../utils/errors");
|
|
43
|
+
exports.DEFAULT_CPP_TEMPLATE = `#include <bits/stdc++.h>
|
|
41
44
|
|
|
42
45
|
using namespace std;
|
|
43
46
|
|
|
@@ -46,7 +49,7 @@ int main() {
|
|
|
46
49
|
return 0;
|
|
47
50
|
}
|
|
48
51
|
`;
|
|
49
|
-
|
|
52
|
+
exports.DEFAULT_PYTHON_TEMPLATE = `import sys
|
|
50
53
|
|
|
51
54
|
def main():
|
|
52
55
|
# Solve the problem here
|
|
@@ -55,6 +58,28 @@ def main():
|
|
|
55
58
|
if __name__ == '__main__':
|
|
56
59
|
main()
|
|
57
60
|
`;
|
|
61
|
+
exports.LANGUAGE_PRESETS = {
|
|
62
|
+
cpp: {
|
|
63
|
+
config: {
|
|
64
|
+
extension: 'cpp',
|
|
65
|
+
templateDir: 'templates/cpp',
|
|
66
|
+
build: 'g++ -O2 -std=gnu++20 -o a.out main.cpp',
|
|
67
|
+
run: './a.out'
|
|
68
|
+
},
|
|
69
|
+
template: exports.DEFAULT_CPP_TEMPLATE,
|
|
70
|
+
filename: 'main.cpp'
|
|
71
|
+
},
|
|
72
|
+
python: {
|
|
73
|
+
config: {
|
|
74
|
+
extension: 'py',
|
|
75
|
+
templateDir: 'templates/python',
|
|
76
|
+
build: '',
|
|
77
|
+
run: 'python3 main.py'
|
|
78
|
+
},
|
|
79
|
+
template: exports.DEFAULT_PYTHON_TEMPLATE,
|
|
80
|
+
filename: 'main.py'
|
|
81
|
+
}
|
|
82
|
+
};
|
|
58
83
|
function initWorkspace(targetDir = process.cwd(), defaultLanguage = 'cpp') {
|
|
59
84
|
const atCoderCliDir = path.join(targetDir, '.atcoder-cli');
|
|
60
85
|
let alreadyInitialized = false;
|
|
@@ -67,43 +92,103 @@ function initWorkspace(targetDir = process.cwd(), defaultLanguage = 'cpp') {
|
|
|
67
92
|
// Create default config if it doesn't exist
|
|
68
93
|
const configPath = path.join(atCoderCliDir, 'config.json');
|
|
69
94
|
if (!fs.existsSync(configPath)) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
95
|
+
const preset = exports.LANGUAGE_PRESETS[defaultLanguage] || {
|
|
96
|
+
config: {
|
|
97
|
+
extension: defaultLanguage,
|
|
98
|
+
templateDir: `templates/${defaultLanguage}`,
|
|
99
|
+
build: '',
|
|
100
|
+
run: ''
|
|
101
|
+
},
|
|
102
|
+
template: '',
|
|
103
|
+
filename: `main.${defaultLanguage}`
|
|
104
|
+
};
|
|
105
|
+
const initialConfig = {
|
|
106
|
+
defaultLanguage,
|
|
107
|
+
languages: {
|
|
108
|
+
[defaultLanguage]: preset.config
|
|
109
|
+
},
|
|
110
|
+
testDirName: 'tests',
|
|
111
|
+
contestDir: '',
|
|
112
|
+
lang: 'en',
|
|
113
|
+
extractProblemStatement: false,
|
|
114
|
+
problemLang: 'ja'
|
|
115
|
+
};
|
|
116
|
+
(0, config_store_1.saveConfig)(targetDir, initialConfig);
|
|
74
117
|
}
|
|
75
118
|
// Create default templates
|
|
76
119
|
const templatesDir = path.join(atCoderCliDir, 'templates');
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
120
|
+
const preset = exports.LANGUAGE_PRESETS[defaultLanguage] || {
|
|
121
|
+
config: {
|
|
122
|
+
extension: defaultLanguage,
|
|
123
|
+
templateDir: `templates/${defaultLanguage}`,
|
|
124
|
+
build: '',
|
|
125
|
+
run: ''
|
|
126
|
+
},
|
|
127
|
+
template: '',
|
|
128
|
+
filename: `main.${defaultLanguage}`
|
|
129
|
+
};
|
|
130
|
+
const langTemplateDir = path.join(templatesDir, defaultLanguage);
|
|
131
|
+
if (!fs.existsSync(langTemplateDir)) {
|
|
132
|
+
fs.mkdirSync(langTemplateDir, { recursive: true });
|
|
88
133
|
}
|
|
89
|
-
const
|
|
90
|
-
if (!fs.existsSync(
|
|
91
|
-
fs.writeFileSync(
|
|
134
|
+
const templateFile = path.join(langTemplateDir, preset.filename);
|
|
135
|
+
if (!fs.existsSync(templateFile)) {
|
|
136
|
+
fs.writeFileSync(templateFile, preset.template, 'utf8');
|
|
92
137
|
}
|
|
93
138
|
// Update or create .gitignore
|
|
94
139
|
const gitignorePath = path.join(targetDir, '.gitignore');
|
|
95
|
-
const
|
|
140
|
+
const ignoreSession = '.atcoder-cli/session.json';
|
|
141
|
+
const ignoreProblem = 'problem.md';
|
|
96
142
|
let gitignoreUpdated = false;
|
|
97
143
|
if (fs.existsSync(gitignorePath)) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
144
|
+
let content = fs.readFileSync(gitignorePath, 'utf8');
|
|
145
|
+
let updated = false;
|
|
146
|
+
if (!content.includes(ignoreSession)) {
|
|
147
|
+
content += (content.endsWith('\n') ? '' : '\n') + '\n# AtCoder CLI Session\n' + ignoreSession + '\n';
|
|
148
|
+
updated = true;
|
|
149
|
+
}
|
|
150
|
+
if (!content.includes(ignoreProblem)) {
|
|
151
|
+
content += (content.endsWith('\n') ? '' : '\n') + '\n# AtCoder problem statements\n' + ignoreProblem + '\n';
|
|
152
|
+
updated = true;
|
|
153
|
+
}
|
|
154
|
+
if (updated) {
|
|
155
|
+
fs.writeFileSync(gitignorePath, content, 'utf8');
|
|
101
156
|
gitignoreUpdated = true;
|
|
102
157
|
}
|
|
103
158
|
}
|
|
104
159
|
else {
|
|
105
|
-
|
|
160
|
+
const defaultIgnore = `# AtCoder CLI Session\n${ignoreSession}\n\n# AtCoder problem statements\n${ignoreProblem}\n`;
|
|
161
|
+
fs.writeFileSync(gitignorePath, defaultIgnore, 'utf8');
|
|
106
162
|
gitignoreUpdated = true;
|
|
107
163
|
}
|
|
108
164
|
return { alreadyInitialized, gitignoreUpdated };
|
|
109
165
|
}
|
|
166
|
+
function addLanguage(workspaceRoot, langName, options) {
|
|
167
|
+
const config = (0, config_store_1.loadConfig)(workspaceRoot);
|
|
168
|
+
const cleanLang = langName.trim().toLowerCase();
|
|
169
|
+
if (config.languages[cleanLang]) {
|
|
170
|
+
throw new errors_1.AtcError(`Language "${cleanLang}" is already configured.`);
|
|
171
|
+
}
|
|
172
|
+
const preset = exports.LANGUAGE_PRESETS[cleanLang];
|
|
173
|
+
const extension = options.extension || (preset ? preset.config.extension : cleanLang);
|
|
174
|
+
const build = options.build !== undefined ? options.build : (preset ? preset.config.build : '');
|
|
175
|
+
const run = options.run || (preset ? preset.config.run : '');
|
|
176
|
+
const template = options.template !== undefined ? options.template : (preset ? preset.template : '// Solve the problem here\n');
|
|
177
|
+
const templatesDir = path.join(workspaceRoot, '.atcoder-cli', 'templates');
|
|
178
|
+
const langTemplateDir = path.join(templatesDir, cleanLang);
|
|
179
|
+
if (!fs.existsSync(langTemplateDir)) {
|
|
180
|
+
fs.mkdirSync(langTemplateDir, { recursive: true });
|
|
181
|
+
}
|
|
182
|
+
const filename = preset ? preset.filename : `main.${extension}`;
|
|
183
|
+
const templateFile = path.join(langTemplateDir, filename);
|
|
184
|
+
if (!fs.existsSync(templateFile)) {
|
|
185
|
+
fs.writeFileSync(templateFile, template, 'utf8');
|
|
186
|
+
}
|
|
187
|
+
config.languages[cleanLang] = {
|
|
188
|
+
extension,
|
|
189
|
+
templateDir: `templates/${cleanLang}`,
|
|
190
|
+
build,
|
|
191
|
+
run
|
|
192
|
+
};
|
|
193
|
+
(0, config_store_1.saveConfig)(workspaceRoot, config);
|
|
194
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atcoder-workspace",
|
|
3
|
-
"version": "1.1.0-beta.
|
|
4
|
-
"description": "AtCoder
|
|
3
|
+
"version": "1.1.0-beta.3",
|
|
4
|
+
"description": "AtCoder Workspace CLI (Local-first)",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"atc": "./dist/cli.js"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"test": "vitest run",
|
|
13
13
|
"test:watch": "vitest",
|
|
14
14
|
"dev": "tsx src/cli.ts",
|
|
15
|
-
"test-env:setup": "npm run build && npm pack && npm --prefix test install atcoder-workspace-1.1.0-beta.
|
|
15
|
+
"test-env:setup": "npm run build && npm pack && npm --prefix test install atcoder-workspace-1.1.0-beta.3.tgz"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"atcoder",
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { setupTask } from './new';
|
|
5
|
+
import { loadConfig, saveConfig, DEFAULT_CONFIG } from '../config/config-store';
|
|
6
|
+
|
|
7
|
+
vi.mock('./client', () => {
|
|
8
|
+
return {
|
|
9
|
+
createAtCoderClient: vi.fn().mockReturnValue({
|
|
10
|
+
get: vi.fn().mockResolvedValue({
|
|
11
|
+
data: `
|
|
12
|
+
<span class="h2">A - Test Problem</span>
|
|
13
|
+
<div id="task-statement">
|
|
14
|
+
<span class="lang-en">
|
|
15
|
+
<p>English problem text.</p>
|
|
16
|
+
<h3>Sample Input 1</h3>
|
|
17
|
+
<pre>1 2</pre>
|
|
18
|
+
<h3>Sample Output 1</h3>
|
|
19
|
+
<pre>3</pre>
|
|
20
|
+
</span>
|
|
21
|
+
<span class="lang-ja">
|
|
22
|
+
<p>日本語の問題文。</p>
|
|
23
|
+
<h3>入力例 1</h3>
|
|
24
|
+
<pre>1 2</pre>
|
|
25
|
+
<h3>出力例 1</h3>
|
|
26
|
+
<pre>3</pre>
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
29
|
+
`
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('setupTask problem extraction', () => {
|
|
36
|
+
const tempDir = path.join(__dirname, '../../test-temp-new');
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
if (fs.existsSync(tempDir)) {
|
|
40
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
43
|
+
|
|
44
|
+
// Create .atcoder-cli/templates/cpp/main.cpp to avoid errors
|
|
45
|
+
const templatesDir = path.join(tempDir, '.atcoder-cli', 'templates', 'cpp');
|
|
46
|
+
fs.mkdirSync(templatesDir, { recursive: true });
|
|
47
|
+
fs.writeFileSync(path.join(templatesDir, 'main.cpp'), '// template', 'utf8');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
if (fs.existsSync(tempDir)) {
|
|
52
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should NOT extract problem statement by default (extractProblemStatement = false)', async () => {
|
|
57
|
+
const config = {
|
|
58
|
+
...DEFAULT_CONFIG,
|
|
59
|
+
extractProblemStatement: false
|
|
60
|
+
};
|
|
61
|
+
saveConfig(tempDir, config);
|
|
62
|
+
|
|
63
|
+
const task = {
|
|
64
|
+
id: 'abc300_a',
|
|
65
|
+
label: 'a',
|
|
66
|
+
title: 'A - Test Problem'
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const res = await setupTask(tempDir, 'abc300', task);
|
|
70
|
+
expect(res.sampleCount).toBe(1);
|
|
71
|
+
expect(fs.existsSync(path.join(res.taskDir, 'problem.md'))).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should extract problem statement if extractProblemStatement = true', async () => {
|
|
75
|
+
const config = {
|
|
76
|
+
...DEFAULT_CONFIG,
|
|
77
|
+
extractProblemStatement: true,
|
|
78
|
+
problemLang: 'en' as const
|
|
79
|
+
};
|
|
80
|
+
saveConfig(tempDir, config);
|
|
81
|
+
|
|
82
|
+
const task = {
|
|
83
|
+
id: 'abc300_a',
|
|
84
|
+
label: 'a',
|
|
85
|
+
title: 'A - Test Problem'
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const res = await setupTask(tempDir, 'abc300', task);
|
|
89
|
+
expect(res.sampleCount).toBe(1);
|
|
90
|
+
|
|
91
|
+
const problemMdPath = path.join(res.taskDir, 'problem.md');
|
|
92
|
+
expect(fs.existsSync(problemMdPath)).toBe(true);
|
|
93
|
+
|
|
94
|
+
const content = fs.readFileSync(problemMdPath, 'utf8');
|
|
95
|
+
expect(content).toContain('# A - Test Problem');
|
|
96
|
+
expect(content).toContain('English problem text.');
|
|
97
|
+
expect(content).toContain('### Sample Input 1');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should respect problemLang configuration specifically', async () => {
|
|
101
|
+
const config = {
|
|
102
|
+
...DEFAULT_CONFIG,
|
|
103
|
+
extractProblemStatement: true,
|
|
104
|
+
lang: 'en' as const,
|
|
105
|
+
problemLang: 'ja' as const
|
|
106
|
+
};
|
|
107
|
+
saveConfig(tempDir, config);
|
|
108
|
+
|
|
109
|
+
const task = {
|
|
110
|
+
id: 'abc300_a',
|
|
111
|
+
label: 'a',
|
|
112
|
+
title: 'A - Test Problem'
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const res = await setupTask(tempDir, 'abc300', task);
|
|
116
|
+
const content = fs.readFileSync(path.join(res.taskDir, 'problem.md'), 'utf8');
|
|
117
|
+
expect(content).toContain('日本語の問題文。');
|
|
118
|
+
expect(content).not.toContain('English problem text.');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should fallback to lang configuration if problemLang is not specified', async () => {
|
|
122
|
+
const config = {
|
|
123
|
+
...DEFAULT_CONFIG,
|
|
124
|
+
extractProblemStatement: true,
|
|
125
|
+
lang: 'ja' as const
|
|
126
|
+
};
|
|
127
|
+
saveConfig(tempDir, config);
|
|
128
|
+
|
|
129
|
+
const task = {
|
|
130
|
+
id: 'abc300_a',
|
|
131
|
+
label: 'a',
|
|
132
|
+
title: 'A - Test Problem'
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const res = await setupTask(tempDir, 'abc300', task);
|
|
136
|
+
const content = fs.readFileSync(path.join(res.taskDir, 'problem.md'), 'utf8');
|
|
137
|
+
expect(content).toContain('日本語の問題文。');
|
|
138
|
+
expect(content).not.toContain('English problem text.');
|
|
139
|
+
});
|
|
140
|
+
});
|
package/src/atcoder/new.ts
CHANGED
|
@@ -75,7 +75,8 @@ export async function setupTask(
|
|
|
75
75
|
throw new AtcError(`Failed to fetch problem page for "${task.id}": ${err.message}`);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
const
|
|
78
|
+
const preferredProblemLang = config.problemLang || config.lang;
|
|
79
|
+
const problemDetails = parseProblemPage(problemHtml, preferredProblemLang);
|
|
79
80
|
|
|
80
81
|
// Write sample cases to tests/ directory
|
|
81
82
|
const testDirName = config.testDirName || 'tests';
|
|
@@ -98,6 +99,11 @@ export async function setupTask(
|
|
|
98
99
|
fs.writeFileSync(path.join(testDir, `sample-${sample.index}.out`), sample.output, 'utf8');
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
// Write problem statement if enabled in config
|
|
103
|
+
if (config.extractProblemStatement && problemDetails.problemStatementMd) {
|
|
104
|
+
fs.writeFileSync(path.join(taskDir, 'problem.md'), problemDetails.problemStatementMd, 'utf8');
|
|
105
|
+
}
|
|
106
|
+
|
|
101
107
|
return {
|
|
102
108
|
contestId,
|
|
103
109
|
taskLabel: task.label,
|
|
@@ -64,5 +64,130 @@ describe('problem-page parser', () => {
|
|
|
64
64
|
input: '10 20\n30\n',
|
|
65
65
|
output: '60\n'
|
|
66
66
|
});
|
|
67
|
+
expect(details.problemStatementMd).toContain('# A - ABC Problem');
|
|
68
|
+
expect(details.problemStatementMd).toContain('### Sample Input 1');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should parse Japanese problem statement with preferredLang ja', () => {
|
|
72
|
+
const details = parseProblemPage(MOCK_PROBLEM_HTML, 'ja');
|
|
73
|
+
expect(details.problemStatementMd).toContain('### 入力例 1');
|
|
74
|
+
expect(details.problemStatementMd).not.toContain('### Sample Input 1');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should preserve LaTeX formatting without escaping math expressions', () => {
|
|
78
|
+
const htmlWithLatex = `
|
|
79
|
+
<div id="task-statement">
|
|
80
|
+
<span class="lang-en">
|
|
81
|
+
<p>Given $N$ integers $A_1, A_2, \\dots, A_N$.</p>
|
|
82
|
+
<p>Find $$ \\sum_{i=1}^N A_i $$.</p>
|
|
83
|
+
<p>Constraints: $1 \\le N \\le 100$ and $A_i \\le 1000$ with some _underscores_ outside and grid cell # symbols.</p>
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
`;
|
|
87
|
+
const details = parseProblemPage(htmlWithLatex);
|
|
88
|
+
expect(details.problemStatementMd).toContain('Given $N$ integers $A_1, A_2, \\dots, A_N$.');
|
|
89
|
+
expect(details.problemStatementMd).toContain('Find $$\n\\sum_{i=1}^N A_i\n$$.');
|
|
90
|
+
expect(details.problemStatementMd).toContain('Constraints: $1 \\le N \\le 100$ and $A_i \\le 1000$');
|
|
91
|
+
expect(details.problemStatementMd).toContain('\\_underscores\\_');
|
|
92
|
+
expect(details.problemStatementMd).toContain('grid cell # symbols');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should translate \\( and \\[] delimiters to $ and $$, and keep content unescaped', () => {
|
|
96
|
+
const htmlWithLatex = `
|
|
97
|
+
<div id="task-statement">
|
|
98
|
+
<span class="lang-en">
|
|
99
|
+
<p>Given \\(T_i\\) for all \\(i\\).</p>
|
|
100
|
+
<p>Find \\[ \\sum_{i=1}^N T_i \\].</p>
|
|
101
|
+
<p>Constraints: \\(1 \\le N,Q \\le 1000\\).</p>
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
104
|
+
`;
|
|
105
|
+
const details = parseProblemPage(htmlWithLatex);
|
|
106
|
+
expect(details.problemStatementMd).toContain('Given $T_i$ for all $i$.');
|
|
107
|
+
expect(details.problemStatementMd).toContain('$$\n\\sum_{i=1}^N T_i\n$$');
|
|
108
|
+
expect(details.problemStatementMd).toContain('Constraints: $1 \\le N,Q \\le 1000$.');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should convert <var> and span.math tags to $ math blocks and keep content unescaped', () => {
|
|
112
|
+
const htmlWithVar = `
|
|
113
|
+
<div id="task-statement">
|
|
114
|
+
<span class="lang-en">
|
|
115
|
+
<p>Let <var>N</var> be the number of elements.</p>
|
|
116
|
+
<p>Constraints: <ul><li><var>1 \\le N \\le 300</var></li><li><span class="math">A_i \\le 1000</span></li></ul></p>
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
`;
|
|
120
|
+
const details = parseProblemPage(htmlWithVar);
|
|
121
|
+
expect(details.problemStatementMd).toContain('Let $N$ be the number of elements.');
|
|
122
|
+
expect(details.problemStatementMd).toContain('- $1 \\le N \\le 300$');
|
|
123
|
+
expect(details.problemStatementMd).toContain('- $A_i \\le 1000$');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should format <pre> blocks containing math or var tags as LaTeX array block', () => {
|
|
127
|
+
const htmlWithPre = `
|
|
128
|
+
<div id="task-statement">
|
|
129
|
+
<span class="lang-en">
|
|
130
|
+
<h3>Input</h3>
|
|
131
|
+
<pre>
|
|
132
|
+
N M
|
|
133
|
+
A_1 A_2 ... A_N
|
|
134
|
+
:
|
|
135
|
+
query_Q
|
|
136
|
+
</pre>
|
|
137
|
+
</span>
|
|
138
|
+
</div>
|
|
139
|
+
`;
|
|
140
|
+
// Add <var> tag to trigger math conversion
|
|
141
|
+
const htmlWithPreVar = htmlWithPre.replace('N M', '<var>N</var> <var>M</var>');
|
|
142
|
+
const details = parseProblemPage(htmlWithPreVar);
|
|
143
|
+
expect(details.problemStatementMd).toContain('\\begin{array}{l}');
|
|
144
|
+
expect(details.problemStatementMd).toContain('\\text{N M}');
|
|
145
|
+
expect(details.problemStatementMd).toContain('\\text{A}_1');
|
|
146
|
+
expect(details.problemStatementMd).toContain('\\dots');
|
|
147
|
+
expect(details.problemStatementMd).toContain('\\vdots');
|
|
148
|
+
expect(details.problemStatementMd).toContain('\\text{query}_Q');
|
|
149
|
+
expect(details.problemStatementMd).toContain('\\end{array}');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should keep standard pre blocks (sample cases) as markdown code blocks', () => {
|
|
153
|
+
const htmlWithStandardPre = `
|
|
154
|
+
<div id="task-statement">
|
|
155
|
+
<span class="lang-en">
|
|
156
|
+
<h3>Sample Input 1</h3>
|
|
157
|
+
<pre>3 125 175
|
|
158
|
+
200 300 400</pre>
|
|
159
|
+
</span>
|
|
160
|
+
</div>
|
|
161
|
+
`;
|
|
162
|
+
const details = parseProblemPage(htmlWithStandardPre);
|
|
163
|
+
expect(details.problemStatementMd).toContain('\`\`\`\n3 125 175\n200 300 400\n\`\`\`');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should format pre blocks that already contain LaTeX \\text{} commands correctly', () => {
|
|
167
|
+
const htmlWithPreLaTeX = `
|
|
168
|
+
<div id="task-statement">
|
|
169
|
+
<span class="lang-en">
|
|
170
|
+
<h3>Input</h3>
|
|
171
|
+
<pre>
|
|
172
|
+
Q
|
|
173
|
+
\\text{query}_1
|
|
174
|
+
\\text{query}_2
|
|
175
|
+
:
|
|
176
|
+
\\text{query}_Q
|
|
177
|
+
</pre>
|
|
178
|
+
</span>
|
|
179
|
+
</div>
|
|
180
|
+
`;
|
|
181
|
+
// Add <var> tag to trigger math conversion
|
|
182
|
+
const htmlWithPreVar = htmlWithPreLaTeX.replace('Q', '<var>Q</var>');
|
|
183
|
+
const details = parseProblemPage(htmlWithPreVar);
|
|
184
|
+
expect(details.problemStatementMd).toContain('\\begin{array}{l}');
|
|
185
|
+
expect(details.problemStatementMd).toContain('\\text{Q}');
|
|
186
|
+
expect(details.problemStatementMd).toContain('\\text{query}_1');
|
|
187
|
+
expect(details.problemStatementMd).toContain('\\text{query}_2');
|
|
188
|
+
expect(details.problemStatementMd).toContain('\\vdots');
|
|
189
|
+
expect(details.problemStatementMd).toContain('\\text{query}_Q');
|
|
190
|
+
expect(details.problemStatementMd).toContain('\\end{array}');
|
|
191
|
+
expect(details.problemStatementMd).not.toContain('\\text\\text');
|
|
67
192
|
});
|
|
68
193
|
});
|