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.
@@ -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 DEFAULT_CPP_TEMPLATE = `#include <bits/stdc++.h>
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
- const DEFAULT_PYTHON_TEMPLATE = `import sys
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
- (0, config_store_1.saveConfig)(targetDir, {
71
- ...config_store_1.DEFAULT_CONFIG,
72
- defaultLanguage
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 cppTemplateDir = path.join(templatesDir, 'cpp');
78
- const pythonTemplateDir = path.join(templatesDir, 'python');
79
- if (!fs.existsSync(cppTemplateDir)) {
80
- fs.mkdirSync(cppTemplateDir, { recursive: true });
81
- }
82
- const cppFile = path.join(cppTemplateDir, 'main.cpp');
83
- if (!fs.existsSync(cppFile)) {
84
- fs.writeFileSync(cppFile, DEFAULT_CPP_TEMPLATE, 'utf8');
85
- }
86
- if (!fs.existsSync(pythonTemplateDir)) {
87
- fs.mkdirSync(pythonTemplateDir, { recursive: true });
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 pythonFile = path.join(pythonTemplateDir, 'main.py');
90
- if (!fs.existsSync(pythonFile)) {
91
- fs.writeFileSync(pythonFile, DEFAULT_PYTHON_TEMPLATE, 'utf8');
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 ignoreRule = '\n# AtCoder CLI Session\n.atcoder-cli/session.json\n';
140
+ const ignoreSession = '.atcoder-cli/session.json';
141
+ const ignoreProblem = 'problem.md';
96
142
  let gitignoreUpdated = false;
97
143
  if (fs.existsSync(gitignorePath)) {
98
- const content = fs.readFileSync(gitignorePath, 'utf8');
99
- if (!content.includes('.atcoder-cli/session.json')) {
100
- fs.writeFileSync(gitignorePath, content + ignoreRule, 'utf8');
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
- fs.writeFileSync(gitignorePath, ignoreRule.trim() + '\n', 'utf8');
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.1",
4
- "description": "AtCoder All-in-One CLI (Local-first)",
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.1.tgz"
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
+ });
@@ -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 problemDetails = parseProblemPage(problemHtml);
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
  });