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
|
@@ -13,9 +13,338 @@ export interface TaskDetails {
|
|
|
13
13
|
timeLimitMs: number;
|
|
14
14
|
memoryLimitBytes: number;
|
|
15
15
|
samples: SampleCase[];
|
|
16
|
+
problemStatementMd?: string;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
function escapeMarkdown(text: string): string {
|
|
20
|
+
return text
|
|
21
|
+
.replace(/\\/g, '\\\\')
|
|
22
|
+
.replace(/\*/g, '\\*')
|
|
23
|
+
.replace(/_/g, '\\_')
|
|
24
|
+
.replace(/`/g, '\\`')
|
|
25
|
+
.replace(/\[/g, '\\[')
|
|
26
|
+
.replace(/\]/g, '\\]');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function convertLineToMath(line: string): string {
|
|
30
|
+
const trimmed = line.trim();
|
|
31
|
+
if (trimmed === ':' || trimmed === '\\vdots') {
|
|
32
|
+
return '\\vdots';
|
|
33
|
+
}
|
|
34
|
+
if (trimmed === '...' || trimmed === '\\dots' || trimmed === '\\cdots') {
|
|
35
|
+
return '\\dots';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let result = '';
|
|
39
|
+
let i = 0;
|
|
40
|
+
let textBuffer = '';
|
|
41
|
+
|
|
42
|
+
const flushBuffer = () => {
|
|
43
|
+
if (textBuffer) {
|
|
44
|
+
result += `\\text{${textBuffer}}`;
|
|
45
|
+
textBuffer = '';
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
while (i < line.length) {
|
|
50
|
+
const remaining = line.slice(i);
|
|
51
|
+
|
|
52
|
+
// 0. Match existing \text{...} optionally followed by a subscript
|
|
53
|
+
const textSubMatch = remaining.match(/^\\text\{([^{}]+)\}(?:_([a-zA-Z0-9]+|\{[^{}]+\}))?/);
|
|
54
|
+
if (textSubMatch) {
|
|
55
|
+
flushBuffer();
|
|
56
|
+
const baseText = textSubMatch[1];
|
|
57
|
+
const subText = textSubMatch[2];
|
|
58
|
+
if (subText) {
|
|
59
|
+
const formattedSub = (subText.startsWith('{') && subText.endsWith('}'))
|
|
60
|
+
? subText
|
|
61
|
+
: (subText.length > 1 ? `{${subText}}` : subText);
|
|
62
|
+
result += `\\text{${baseText}}_${formattedSub}`;
|
|
63
|
+
} else {
|
|
64
|
+
result += `\\text{${baseText}}`;
|
|
65
|
+
}
|
|
66
|
+
i += textSubMatch[0].length;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 1. Match LaTeX commands like \dots, \vdots, \cdots
|
|
71
|
+
const cmdMatch = remaining.match(/^(\\[a-zA-Z]+)/);
|
|
72
|
+
if (cmdMatch) {
|
|
73
|
+
flushBuffer();
|
|
74
|
+
result += cmdMatch[1];
|
|
75
|
+
i += cmdMatch[1].length;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 2. Match subscript pattern: e.g. A_i, query_1
|
|
80
|
+
const subMatch = remaining.match(/^([a-zA-Z]+)_([a-zA-Z0-9]+)/);
|
|
81
|
+
if (subMatch) {
|
|
82
|
+
flushBuffer();
|
|
83
|
+
const base = subMatch[1];
|
|
84
|
+
const sub = subMatch[2];
|
|
85
|
+
const formattedSub = sub.length > 1 ? `\\text{${sub}}` : sub;
|
|
86
|
+
result += `\\text{${base}}_${formattedSub}`;
|
|
87
|
+
i += subMatch[0].length;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. Match raw ellipsis "..."
|
|
92
|
+
if (remaining.startsWith('...')) {
|
|
93
|
+
flushBuffer();
|
|
94
|
+
result += '\\dots';
|
|
95
|
+
i += 3;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 4. Match colon ":"
|
|
100
|
+
if (remaining.startsWith(':')) {
|
|
101
|
+
flushBuffer();
|
|
102
|
+
result += '\\vdots';
|
|
103
|
+
i += 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 5. Normal character (including spaces)
|
|
108
|
+
textBuffer += line[i];
|
|
109
|
+
i += 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
flushBuffer();
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function renderPreAsMathArray(node: any, $: cheerio.CheerioAPI): string {
|
|
117
|
+
const rawText = $(node).text();
|
|
118
|
+
const lines = rawText.split('\n');
|
|
119
|
+
|
|
120
|
+
let startIdx = 0;
|
|
121
|
+
while (startIdx < lines.length && lines[startIdx].trim() === '') {
|
|
122
|
+
startIdx++;
|
|
123
|
+
}
|
|
124
|
+
let endIdx = lines.length - 1;
|
|
125
|
+
while (endIdx >= startIdx && lines[endIdx].trim() === '') {
|
|
126
|
+
endIdx--;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const activeLines = lines.slice(startIdx, endIdx + 1);
|
|
130
|
+
if (activeLines.length === 0) {
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const convertedLines = activeLines.map(line => convertLineToMath(line));
|
|
135
|
+
|
|
136
|
+
let result = '\n\n$$\n\\begin{array}{l}\n';
|
|
137
|
+
result += convertedLines.join(' \\\\\n');
|
|
138
|
+
result += '\n\\end{array}\n$$\n\n';
|
|
139
|
+
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function processTextNode(text: string): string {
|
|
144
|
+
const parts: string[] = [];
|
|
145
|
+
let i = 0;
|
|
146
|
+
while (i < text.length) {
|
|
147
|
+
if (text.startsWith('\\[', i)) {
|
|
148
|
+
const start = i;
|
|
149
|
+
const end = text.indexOf('\\]', i + 2);
|
|
150
|
+
if (end !== -1) {
|
|
151
|
+
const mathContent = text.slice(start + 2, end);
|
|
152
|
+
parts.push(`$$\n${mathContent.trim()}\n$$`);
|
|
153
|
+
i = end + 2;
|
|
154
|
+
} else {
|
|
155
|
+
parts.push(escapeMarkdown(text.slice(i, i + 2)));
|
|
156
|
+
i += 2;
|
|
157
|
+
}
|
|
158
|
+
} else if (text.startsWith('\\(', i)) {
|
|
159
|
+
const start = i;
|
|
160
|
+
const end = text.indexOf('\\)', i + 2);
|
|
161
|
+
if (end !== -1) {
|
|
162
|
+
const mathContent = text.slice(start + 2, end);
|
|
163
|
+
parts.push(`$${mathContent.trim()}$`);
|
|
164
|
+
i = end + 2;
|
|
165
|
+
} else {
|
|
166
|
+
parts.push(escapeMarkdown(text.slice(i, i + 2)));
|
|
167
|
+
i += 2;
|
|
168
|
+
}
|
|
169
|
+
} else if (text.startsWith('$$', i)) {
|
|
170
|
+
const start = i;
|
|
171
|
+
const end = text.indexOf('$$', i + 2);
|
|
172
|
+
if (end !== -1) {
|
|
173
|
+
const mathContent = text.slice(start + 2, end);
|
|
174
|
+
parts.push(`$$\n${mathContent.trim()}\n$$`);
|
|
175
|
+
i = end + 2;
|
|
176
|
+
} else {
|
|
177
|
+
parts.push(escapeMarkdown(text.slice(i, i + 2)));
|
|
178
|
+
i += 2;
|
|
179
|
+
}
|
|
180
|
+
} else if (text.startsWith('$', i)) {
|
|
181
|
+
const start = i;
|
|
182
|
+
const end = text.indexOf('$', i + 1);
|
|
183
|
+
if (end !== -1) {
|
|
184
|
+
const mathContent = text.slice(start + 1, end);
|
|
185
|
+
parts.push(`$${mathContent.trim()}$`);
|
|
186
|
+
i = end + 1;
|
|
187
|
+
} else {
|
|
188
|
+
parts.push('$');
|
|
189
|
+
i += 1;
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
let nextIndex = -1;
|
|
193
|
+
let selectedDelim = '';
|
|
194
|
+
|
|
195
|
+
const delims = ['\\[', '\\(', '$$', '$'];
|
|
196
|
+
for (const delim of delims) {
|
|
197
|
+
const idx = text.indexOf(delim, i);
|
|
198
|
+
if (idx !== -1 && (nextIndex === -1 || idx < nextIndex)) {
|
|
199
|
+
nextIndex = idx;
|
|
200
|
+
selectedDelim = delim;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (nextIndex === -1) {
|
|
205
|
+
parts.push(escapeMarkdown(text.slice(i)));
|
|
206
|
+
break;
|
|
207
|
+
} else {
|
|
208
|
+
parts.push(escapeMarkdown(text.slice(i, nextIndex)));
|
|
209
|
+
i = nextIndex;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return parts.join('');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function nodeToMarkdown(node: any, $: cheerio.CheerioAPI, isInsideMath = false): string {
|
|
217
|
+
if (node.type === 'text') {
|
|
218
|
+
return isInsideMath ? ((node as any).data || '') : processTextNode((node as any).data || '');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (node.type === 'tag') {
|
|
222
|
+
const tagName = (node as any).name;
|
|
223
|
+
const className = $(node).attr('class') || '';
|
|
224
|
+
const classes = className.split(/\s+/);
|
|
225
|
+
const isMathContainer = tagName === 'var' || classes.includes('math');
|
|
226
|
+
const isBlockMath = tagName === 'div' && classes.includes('math');
|
|
227
|
+
|
|
228
|
+
if (isMathContainer) {
|
|
229
|
+
let mathBody = '';
|
|
230
|
+
if ((node as any).children) {
|
|
231
|
+
for (const child of (node as any).children) {
|
|
232
|
+
mathBody += nodeToMarkdown(child, $, true);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const delim = isBlockMath ? '$$' : '$';
|
|
236
|
+
if (isBlockMath) {
|
|
237
|
+
return `\n\n$$\n${mathBody.trim()}\n$$\n\n`;
|
|
238
|
+
}
|
|
239
|
+
return `${delim}${mathBody.trim()}${delim}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (tagName === 'table') {
|
|
243
|
+
const $table = $(node).clone();
|
|
244
|
+
$table.find('a').each((_, el) => {
|
|
245
|
+
const href = $(el).attr('href');
|
|
246
|
+
if (href && href.startsWith('/') && !href.startsWith('//')) {
|
|
247
|
+
$(el).attr('href', 'https://atcoder.jp' + href);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
$table.find('img').each((_, el) => {
|
|
251
|
+
const src = $(el).attr('src');
|
|
252
|
+
if (src && src.startsWith('/') && !src.startsWith('//')) {
|
|
253
|
+
$(el).attr('src', 'https://atcoder.jp' + src);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
return '\n\n' + $.html($table) + '\n\n';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let childrenMarkdown = '';
|
|
260
|
+
if ((node as any).children) {
|
|
261
|
+
for (const child of (node as any).children) {
|
|
262
|
+
childrenMarkdown += nodeToMarkdown(child, $, isInsideMath);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
switch (tagName) {
|
|
267
|
+
case 'h1':
|
|
268
|
+
case 'h2':
|
|
269
|
+
case 'h3':
|
|
270
|
+
case 'h4':
|
|
271
|
+
case 'h5':
|
|
272
|
+
case 'h6': {
|
|
273
|
+
const level = parseInt(tagName.substring(1), 10);
|
|
274
|
+
return `\n\n${'#'.repeat(level)} ${childrenMarkdown.trim()}\n\n`;
|
|
275
|
+
}
|
|
276
|
+
case 'p':
|
|
277
|
+
return `\n\n${childrenMarkdown.trim()}\n\n`;
|
|
278
|
+
case 'br':
|
|
279
|
+
return '\n';
|
|
280
|
+
case 'strong':
|
|
281
|
+
case 'b':
|
|
282
|
+
return `**${childrenMarkdown.trim()}**`;
|
|
283
|
+
case 'em':
|
|
284
|
+
case 'i':
|
|
285
|
+
return `*${childrenMarkdown.trim()}*`;
|
|
286
|
+
case 'code': {
|
|
287
|
+
const parentTagName = (node.parent as any)?.name;
|
|
288
|
+
if (parentTagName === 'pre') {
|
|
289
|
+
return childrenMarkdown;
|
|
290
|
+
}
|
|
291
|
+
return `\`${childrenMarkdown}\``;
|
|
292
|
+
}
|
|
293
|
+
case 'pre': {
|
|
294
|
+
const text = $(node).text();
|
|
295
|
+
const hasVar = $(node).find('var').length > 0;
|
|
296
|
+
const hasMathSymbols = text.includes('\\dots') || text.includes('\\vdots') || text.includes('\\cdots') || text.includes('\\le');
|
|
297
|
+
if (hasVar || hasMathSymbols) {
|
|
298
|
+
return renderPreAsMathArray(node, $);
|
|
299
|
+
}
|
|
300
|
+
return `\n\n\`\`\`\n${text.trim()}\n\`\`\`\n\n`;
|
|
301
|
+
}
|
|
302
|
+
case 'a': {
|
|
303
|
+
let href = $(node).attr('href') || '';
|
|
304
|
+
if (href.startsWith('/') && !href.startsWith('//')) {
|
|
305
|
+
href = 'https://atcoder.jp' + href;
|
|
306
|
+
}
|
|
307
|
+
return `[${childrenMarkdown.trim()}](${href})`;
|
|
308
|
+
}
|
|
309
|
+
case 'img': {
|
|
310
|
+
let src = $(node).attr('src') || '';
|
|
311
|
+
if (src.startsWith('/') && !src.startsWith('//')) {
|
|
312
|
+
src = 'https://atcoder.jp' + src;
|
|
313
|
+
}
|
|
314
|
+
const alt = $(node).attr('alt') || '';
|
|
315
|
+
return ``;
|
|
316
|
+
}
|
|
317
|
+
case 'ul':
|
|
318
|
+
case 'ol':
|
|
319
|
+
return `\n\n${childrenMarkdown}\n\n`;
|
|
320
|
+
case 'li': {
|
|
321
|
+
let indentLevel = 0;
|
|
322
|
+
let parent = node.parent;
|
|
323
|
+
while (parent) {
|
|
324
|
+
const parentName = (parent as any).name;
|
|
325
|
+
if (parentName === 'ul' || parentName === 'ol') {
|
|
326
|
+
indentLevel++;
|
|
327
|
+
}
|
|
328
|
+
parent = parent.parent;
|
|
329
|
+
}
|
|
330
|
+
const indent = ' '.repeat(Math.max(0, indentLevel - 1));
|
|
331
|
+
const parentTagName = (node.parent as any)?.name;
|
|
332
|
+
const prefix = parentTagName === 'ol' ? '1. ' : '- ';
|
|
333
|
+
return `\n${indent}${prefix}${childrenMarkdown.trim()}`;
|
|
334
|
+
}
|
|
335
|
+
case 'div':
|
|
336
|
+
case 'span':
|
|
337
|
+
case 'section':
|
|
338
|
+
return childrenMarkdown;
|
|
339
|
+
default:
|
|
340
|
+
return childrenMarkdown;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return '';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function parseProblemPage(html: string, preferredLang?: 'en' | 'ja'): TaskDetails {
|
|
19
348
|
const $ = cheerio.load(html);
|
|
20
349
|
|
|
21
350
|
let title = $('span.h2').first().text().trim();
|
|
@@ -53,10 +382,33 @@ export function parseProblemPage(html: string): TaskDetails {
|
|
|
53
382
|
}
|
|
54
383
|
|
|
55
384
|
let $container = $('#task-statement');
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
385
|
+
if (preferredLang === 'ja') {
|
|
386
|
+
if ($container.find('.lang-ja').length > 0) {
|
|
387
|
+
$container = $container.find('.lang-ja');
|
|
388
|
+
} else if ($container.find('.lang-en').length > 0) {
|
|
389
|
+
$container = $container.find('.lang-en');
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
if ($container.find('.lang-en').length > 0) {
|
|
393
|
+
$container = $container.find('.lang-en');
|
|
394
|
+
} else if ($container.find('.lang-ja').length > 0) {
|
|
395
|
+
$container = $container.find('.lang-ja');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Convert task statement HTML to Markdown if task statement exists
|
|
400
|
+
let problemStatementMd = '';
|
|
401
|
+
if ($container.length > 0) {
|
|
402
|
+
let mdBody = '';
|
|
403
|
+
$container.contents().each((_, node) => {
|
|
404
|
+
mdBody += nodeToMarkdown(node, $);
|
|
405
|
+
});
|
|
406
|
+
mdBody = mdBody.replace(/\n{3,}/g, '\n\n').trim();
|
|
407
|
+
|
|
408
|
+
problemStatementMd = `# ${title}\n\n`;
|
|
409
|
+
problemStatementMd += `- Time Limit: ${timeLimitMs / 1000} sec\n`;
|
|
410
|
+
problemStatementMd += `- Memory Limit: ${Math.round(memoryLimitBytes / (1024 * 1024))} MB\n\n`;
|
|
411
|
+
problemStatementMd += mdBody + '\n';
|
|
60
412
|
}
|
|
61
413
|
|
|
62
414
|
const sampleMap: Record<number, { input?: string; output?: string }> = {};
|
|
@@ -121,6 +473,7 @@ export function parseProblemPage(html: string): TaskDetails {
|
|
|
121
473
|
title,
|
|
122
474
|
timeLimitMs,
|
|
123
475
|
memoryLimitBytes,
|
|
124
|
-
samples
|
|
476
|
+
samples,
|
|
477
|
+
problemStatementMd
|
|
125
478
|
};
|
|
126
479
|
}
|
package/src/cli.ts
CHANGED
|
@@ -8,7 +8,7 @@ import * as fs from 'fs';
|
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
|
|
10
10
|
import { findWorkspaceRoot } from './workspace/finder';
|
|
11
|
-
import { initWorkspace } from './workspace/initializer';
|
|
11
|
+
import { initWorkspace, LANGUAGE_PRESETS, addLanguage } from './workspace/initializer';
|
|
12
12
|
import { loginWithCookie, whoami } from './session/auth';
|
|
13
13
|
import { clearSession } from './session/store';
|
|
14
14
|
import { fetchContestTasks, setupTask } from './atcoder/new';
|
|
@@ -36,8 +36,8 @@ const program = new Command();
|
|
|
36
36
|
|
|
37
37
|
program
|
|
38
38
|
.name('atc')
|
|
39
|
-
.description('AtCoder
|
|
40
|
-
.version('1.1.0-
|
|
39
|
+
.description('AtCoder Workspace')
|
|
40
|
+
.version('1.1.0-beta.3');
|
|
41
41
|
|
|
42
42
|
function handleAction(fn: (...args: any[]) => Promise<void>) {
|
|
43
43
|
return async (...args: any[]) => {
|
|
@@ -243,42 +243,101 @@ program
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
p.outro(pc.green(t('newScaffoldingComplete', lang, selectedTasks.length)));
|
|
246
|
+
|
|
247
|
+
if (config.extractProblemStatement) {
|
|
248
|
+
p.note(
|
|
249
|
+
t('newStatementWarningBody', lang),
|
|
250
|
+
pc.yellow(t('newStatementWarningTitle', lang))
|
|
251
|
+
);
|
|
252
|
+
}
|
|
246
253
|
})
|
|
247
254
|
);
|
|
248
255
|
|
|
249
|
-
function resolveArgs(
|
|
256
|
+
function resolveArgs(
|
|
257
|
+
workspaceRoot: string,
|
|
258
|
+
arg1: string | undefined,
|
|
259
|
+
arg2: string | undefined,
|
|
260
|
+
arg3: string | undefined
|
|
261
|
+
) {
|
|
262
|
+
const cwd = process.cwd();
|
|
263
|
+
const config = loadConfig(workspaceRoot);
|
|
264
|
+
const contestDir = config.contestDir || '';
|
|
265
|
+
|
|
250
266
|
let resolvedTaskDir = '';
|
|
251
267
|
let resolvedFile: string | undefined;
|
|
252
268
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
path.resolve(
|
|
259
|
-
path.resolve(workspaceRoot, taskArg)
|
|
269
|
+
// Helper function to check if relative path parts exist in cwd, workspaceRoot or contestDir
|
|
270
|
+
function checkPath(relativeParts: string[]): { isFile: boolean; isDir: boolean; path: string } | null {
|
|
271
|
+
const pathsToTry = [
|
|
272
|
+
path.resolve(cwd, ...relativeParts),
|
|
273
|
+
path.resolve(workspaceRoot, ...relativeParts),
|
|
274
|
+
path.resolve(workspaceRoot, contestDir, ...relativeParts)
|
|
260
275
|
];
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
276
|
+
|
|
277
|
+
for (const p of pathsToTry) {
|
|
278
|
+
if (fs.existsSync(p)) {
|
|
279
|
+
const stat = fs.statSync(p);
|
|
280
|
+
return {
|
|
281
|
+
isFile: stat.isFile(),
|
|
282
|
+
isDir: stat.isDirectory(),
|
|
283
|
+
path: p
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 1. Three arguments provided: (arg1, arg2, arg3)
|
|
291
|
+
if (arg1 && arg2 && arg3) {
|
|
292
|
+
// e.g., abc300 a main.cpp -> check if arg1/arg2 is a directory
|
|
293
|
+
const checkDir = checkPath([arg1, arg2]);
|
|
294
|
+
if (checkDir && checkDir.isDir) {
|
|
295
|
+
resolvedTaskDir = checkDir.path;
|
|
296
|
+
resolvedFile = arg3;
|
|
265
297
|
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 2. Two arguments provided: (arg1, arg2)
|
|
301
|
+
if (!resolvedTaskDir && arg1 && arg2) {
|
|
302
|
+
// Pattern A: arg1/arg2 is a directory (e.g., abc300 a)
|
|
303
|
+
const checkDir = checkPath([arg1, arg2]);
|
|
304
|
+
if (checkDir && checkDir.isDir) {
|
|
305
|
+
resolvedTaskDir = checkDir.path;
|
|
306
|
+
resolvedFile = undefined;
|
|
307
|
+
} else {
|
|
308
|
+
// Pattern B: arg1 is a directory, arg2 is a file (e.g., a main.cpp)
|
|
309
|
+
const checkArg1 = checkPath([arg1]);
|
|
310
|
+
if (checkArg1 && checkArg1.isDir) {
|
|
311
|
+
resolvedTaskDir = checkArg1.path;
|
|
312
|
+
resolvedFile = arg2;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
266
316
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
317
|
+
// 3. One argument provided: (arg1)
|
|
318
|
+
if (!resolvedTaskDir && arg1) {
|
|
319
|
+
const checkArg1 = checkPath([arg1]);
|
|
320
|
+
if (checkArg1) {
|
|
321
|
+
if (checkArg1.isFile) {
|
|
322
|
+
// Pattern A: directly specified a file (e.g., main.cpp)
|
|
323
|
+
resolvedTaskDir = path.dirname(checkArg1.path);
|
|
324
|
+
resolvedFile = path.basename(checkArg1.path);
|
|
325
|
+
} else if (checkArg1.isDir) {
|
|
326
|
+
// Pattern B: directly specified a directory (e.g., a or abc300/a)
|
|
327
|
+
resolvedTaskDir = checkArg1.path;
|
|
328
|
+
resolvedFile = undefined;
|
|
272
329
|
}
|
|
330
|
+
} else {
|
|
331
|
+
// Fallback to resolveTaskDirectory (legacy behavior)
|
|
332
|
+
resolvedTaskDir = resolveTaskDirectory(workspaceRoot, arg1);
|
|
333
|
+
resolvedFile = undefined;
|
|
273
334
|
}
|
|
274
335
|
}
|
|
275
336
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
resolvedTaskDir =
|
|
279
|
-
|
|
280
|
-
resolvedTaskDir = resolveTaskDirectory(workspaceRoot, taskArg);
|
|
281
|
-
resolvedFile = fileArg;
|
|
337
|
+
// 4. No arguments provided
|
|
338
|
+
if (!resolvedTaskDir) {
|
|
339
|
+
resolvedTaskDir = resolveTaskDirectory(workspaceRoot, undefined);
|
|
340
|
+
resolvedFile = undefined;
|
|
282
341
|
}
|
|
283
342
|
|
|
284
343
|
const taskLabel = path.basename(resolvedTaskDir);
|
|
@@ -288,12 +347,12 @@ function resolveArgs(workspaceRoot: string, taskArg: string | undefined, fileArg
|
|
|
288
347
|
}
|
|
289
348
|
|
|
290
349
|
program
|
|
291
|
-
.command('test [
|
|
350
|
+
.command('test [arg1] [arg2] [arg3]')
|
|
292
351
|
.description(t('descTest', lang))
|
|
293
352
|
.action(
|
|
294
|
-
handleAction(async (
|
|
353
|
+
handleAction(async (arg1: string | undefined, arg2: string | undefined, arg3: string | undefined) => {
|
|
295
354
|
const workspaceRoot = findWorkspaceRoot();
|
|
296
|
-
const { resolvedTaskDir, resolvedFile, taskLabel, contestId } = resolveArgs(workspaceRoot,
|
|
355
|
+
const { resolvedTaskDir, resolvedFile, taskLabel, contestId } = resolveArgs(workspaceRoot, arg1, arg2, arg3);
|
|
297
356
|
|
|
298
357
|
p.intro(pc.cyan(t('testIntro', lang, contestId, taskLabel)));
|
|
299
358
|
|
|
@@ -378,12 +437,12 @@ program
|
|
|
378
437
|
);
|
|
379
438
|
|
|
380
439
|
program
|
|
381
|
-
.command('submit [
|
|
440
|
+
.command('submit [arg1] [arg2] [arg3]')
|
|
382
441
|
.description(t('descSubmit', lang))
|
|
383
442
|
.action(
|
|
384
|
-
handleAction(async (
|
|
443
|
+
handleAction(async (arg1: string | undefined, arg2: string | undefined, arg3: string | undefined) => {
|
|
385
444
|
const workspaceRoot = findWorkspaceRoot();
|
|
386
|
-
const { resolvedTaskDir, resolvedFile, taskLabel, contestId } = resolveArgs(workspaceRoot,
|
|
445
|
+
const { resolvedTaskDir, resolvedFile, taskLabel, contestId } = resolveArgs(workspaceRoot, arg1, arg2, arg3);
|
|
387
446
|
|
|
388
447
|
p.intro(pc.cyan(t('submitPreparing', lang, contestId, taskLabel)));
|
|
389
448
|
|
|
@@ -532,12 +591,26 @@ program
|
|
|
532
591
|
process.exit(1);
|
|
533
592
|
}
|
|
534
593
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
594
|
+
let selectedLang = targetLanguage;
|
|
595
|
+
|
|
596
|
+
if (!selectedLang) {
|
|
597
|
+
const choice = await p.select({
|
|
598
|
+
message: t('langSelectMessage', lang),
|
|
599
|
+
options: [
|
|
600
|
+
{ value: 'en', label: 'English (en)' },
|
|
601
|
+
{ value: 'ja', label: '日本語 (ja)' }
|
|
602
|
+
]
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
if (p.isCancel(choice)) {
|
|
606
|
+
p.cancel(t('langCancelled', lang));
|
|
607
|
+
process.exit(0);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
selectedLang = choice as string;
|
|
538
611
|
}
|
|
539
612
|
|
|
540
|
-
const cleanLang =
|
|
613
|
+
const cleanLang = selectedLang.trim().toLowerCase();
|
|
541
614
|
if (cleanLang !== 'en' && cleanLang !== 'ja') {
|
|
542
615
|
p.log.error(pc.red(t('langInvalid', lang)));
|
|
543
616
|
process.exit(1);
|
|
@@ -551,4 +624,102 @@ program
|
|
|
551
624
|
})
|
|
552
625
|
);
|
|
553
626
|
|
|
627
|
+
program
|
|
628
|
+
.command('add-lang [langName]')
|
|
629
|
+
.description(t('descAddLang', lang))
|
|
630
|
+
.action(
|
|
631
|
+
handleAction(async (langName: string | undefined) => {
|
|
632
|
+
const root = findWorkspaceRoot();
|
|
633
|
+
const config = loadConfig(root);
|
|
634
|
+
|
|
635
|
+
let targetLang = langName;
|
|
636
|
+
if (!targetLang) {
|
|
637
|
+
targetLang = await p.text({
|
|
638
|
+
message: t('addLangEnterName', lang),
|
|
639
|
+
validate: (val) => (!val.trim() ? t('addLangNameNotEmpty', lang) : undefined)
|
|
640
|
+
}) as string;
|
|
641
|
+
|
|
642
|
+
if (p.isCancel(targetLang)) {
|
|
643
|
+
p.cancel(t('addLangCancelled', lang));
|
|
644
|
+
process.exit(0);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
targetLang = targetLang.trim().toLowerCase();
|
|
649
|
+
|
|
650
|
+
// Check if already exists
|
|
651
|
+
if (config.languages[targetLang]) {
|
|
652
|
+
throw new AtcError(t('addLangAlreadyExists', lang, targetLang));
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const preset = LANGUAGE_PRESETS[targetLang];
|
|
656
|
+
let extension = '';
|
|
657
|
+
let build = '';
|
|
658
|
+
let run = '';
|
|
659
|
+
let template = '';
|
|
660
|
+
|
|
661
|
+
if (preset) {
|
|
662
|
+
extension = preset.config.extension;
|
|
663
|
+
build = preset.config.build;
|
|
664
|
+
run = preset.config.run;
|
|
665
|
+
template = preset.template;
|
|
666
|
+
} else {
|
|
667
|
+
// If not preset, prompt for parameters
|
|
668
|
+
const extInput = await p.text({
|
|
669
|
+
message: t('addLangEnterExtension', lang, targetLang),
|
|
670
|
+
placeholder: targetLang,
|
|
671
|
+
validate: (val) => (!val.trim() ? t('addLangExtNotEmpty', lang) : undefined)
|
|
672
|
+
}) as string;
|
|
673
|
+
|
|
674
|
+
if (p.isCancel(extInput)) {
|
|
675
|
+
p.cancel(t('addLangCancelled', lang));
|
|
676
|
+
process.exit(0);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const buildInput = await p.text({
|
|
680
|
+
message: t('addLangEnterBuildCmd', lang),
|
|
681
|
+
placeholder: 'e.g. g++ -O2 main.cpp (leave empty if not needed)'
|
|
682
|
+
}) as string;
|
|
683
|
+
|
|
684
|
+
if (p.isCancel(buildInput)) {
|
|
685
|
+
p.cancel(t('addLangCancelled', lang));
|
|
686
|
+
process.exit(0);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const runInput = await p.text({
|
|
690
|
+
message: t('addLangEnterRunCmd', lang),
|
|
691
|
+
placeholder: `e.g. python3 main.py or ./a.out`,
|
|
692
|
+
validate: (val) => (!val.trim() ? t('addLangRunCmdNotEmpty', lang) : undefined)
|
|
693
|
+
}) as string;
|
|
694
|
+
|
|
695
|
+
if (p.isCancel(runInput)) {
|
|
696
|
+
p.cancel(t('addLangCancelled', lang));
|
|
697
|
+
process.exit(0);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
extension = extInput.trim();
|
|
701
|
+
build = buildInput.trim();
|
|
702
|
+
run = runInput.trim();
|
|
703
|
+
template = `// Solve the problem here\n`;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const s = p.spinner();
|
|
707
|
+
s.start(t('addLangSpinner', lang));
|
|
708
|
+
|
|
709
|
+
try {
|
|
710
|
+
addLanguage(root, targetLang, {
|
|
711
|
+
extension,
|
|
712
|
+
build,
|
|
713
|
+
run,
|
|
714
|
+
template
|
|
715
|
+
});
|
|
716
|
+
} catch (e: any) {
|
|
717
|
+
s.stop('Failed');
|
|
718
|
+
throw e;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
s.stop(t('addLangSuccess', lang, targetLang));
|
|
722
|
+
})
|
|
723
|
+
);
|
|
724
|
+
|
|
554
725
|
program.parse(process.argv);
|