Skip to content

Commit

Permalink
Add Universal Cup parsers
Browse files Browse the repository at this point in the history
  • Loading branch information
jmerle committed Oct 14, 2024
1 parent ffac835 commit 1775535
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 37 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ A browser extension which parses competitive programming problems from various o
| TLX |||
| Toph || |
| uDebug || |
| Universal Cup |||
| UOJ |||
| USACO || |
| USACO Training || |
Expand Down
45 changes: 8 additions & 37 deletions src/parsers/contest/DOMjudgeContestParser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import JSZip from 'jszip';
import { Task } from '../../models/Task';
import { TaskBuilder } from '../../models/TaskBuilder';
import { fetchZip } from '../../utils/zip';
import { ContestParser } from '../ContestParser';

export class DOMjudgeContestParser extends ContestParser<HTMLDivElement> {
Expand Down Expand Up @@ -73,31 +73,16 @@ export class DOMjudgeContestParser extends ContestParser<HTMLDivElement> {

private async fetchTestCases(zipUrl: string): Promise<{ input: string; output: string }[]> {
try {
const response = await fetch(zipUrl);
if (!response.ok) {
throw new Error(`Failed to download ZIP file from ${zipUrl}`);
}

const blob = await response.blob();
const processedBlob = await this.processBlob(blob);

const zip = new JSZip();
const content = await zip.loadAsync(processedBlob);
const files = await fetchZip(zipUrl, ['.in', '.out', '.ans']);

const testCases: Record<string, { input: string; output: string }> = {};
for (const [fileName, fileContent] of Object.entries(files)) {
const fileNumber = fileName.match(/(\d+[a-zA-Z]*)/)?.[0];

for (const fileName in content.files) {
if (Object.prototype.hasOwnProperty.call(content.files, fileName)) {
if (fileName.endsWith('.in') || fileName.endsWith('.out') || fileName.endsWith('.ans')) {
const fileContent = await content.files[fileName].async('string');
const fileNumber = fileName.match(/(\d+[a-zA-Z]*)/)?.[0];

if (fileNumber) {
testCases[fileNumber] = testCases[fileNumber] || { input: '', output: '' };
const fileType = fileName.endsWith('.in') ? 'input' : 'output';
testCases[fileNumber][fileType] = fileContent;
}
}
if (fileNumber) {
testCases[fileNumber] = testCases[fileNumber] || { input: '', output: '' };
const fileType = fileName.endsWith('.in') ? 'input' : 'output';
testCases[fileNumber][fileType] = fileContent;
}
}

Expand All @@ -120,18 +105,4 @@ export class DOMjudgeContestParser extends ContestParser<HTMLDivElement> {

return [time, memory];
}

// JSZip doesn't seem to work properly in Firefox addons, this is a workaround to the issue
// See https://github.com/Stuk/jszip/issues/759 for more information
private processBlob(blob: Blob): Promise<ArrayBuffer | string> {
return new Promise(resolve => {
const fileReader = new FileReader();

fileReader.onload = event => {
resolve(event.target.result);
};

fileReader.readAsBinaryString(blob);
});
}
}
11 changes: 11 additions & 0 deletions src/parsers/contest/UniversalCupContestParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { UniversalCupProblemParser } from '../problem/UniversalCupProblemParser';
import { SimpleContestParser } from '../SimpleContestParser';

export class UniversalCupContestParser extends SimpleContestParser {
protected linkSelector = '.table-text-center > tbody > tr > td > a';
protected problemParser = new UniversalCupProblemParser();

public getMatchPatterns(): string[] {
return ['https://contest.ucup.ac/contest/*'];
}
}
5 changes: 5 additions & 0 deletions src/parsers/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { QDUOJContestParser } from './contest/QDUOJContestParser';
import { RoboContestContestParser } from './contest/RoboContestContestParser';
import { TimusOnlineJudgeContestParser } from './contest/TimusOnlineJudgeContestParser';
import { TLXContestParser } from './contest/TLXContestParser';
import { UniversalCupContestParser } from './contest/UniversalCupContestParser';
import { UOJContestParser } from './contest/UOJContestParser';
import { VirtualJudgeContestParser } from './contest/VirtualJudgeContestParser';
import { YandexContestParser } from './contest/YandexContestParser';
Expand Down Expand Up @@ -128,6 +129,7 @@ import { TimusOnlineJudgeProblemParser } from './problem/TimusOnlineJudgeProblem
import { TLXProblemParser } from './problem/TLXProblemParser';
import { TophProblemParser } from './problem/TophProblemParser';
import { UDebugProblemParser } from './problem/UDebugProblemParser';
import { UniversalCupProblemParser } from './problem/UniversalCupProblemParser';
import { UOJProblemParser } from './problem/UOJProblemParser';
import { USACOProblemParser } from './problem/USACOProblemParser';
import { USACOTrainingProblemParser } from './problem/USACOTrainingProblemParser';
Expand Down Expand Up @@ -342,6 +344,9 @@ export const parsers: Parser[] = [

new UDebugProblemParser(),

new UniversalCupProblemParser(),
new UniversalCupContestParser(),

new UOJProblemParser(),
new UOJContestParser(),

Expand Down
42 changes: 42 additions & 0 deletions src/parsers/problem/UniversalCupProblemParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Sendable } from '../../models/Sendable';
import { TaskBuilder } from '../../models/TaskBuilder';
import { htmlToElement } from '../../utils/dom';
import { fetchZip } from '../../utils/zip';
import { Parser } from '../Parser';

export class UniversalCupProblemParser extends Parser {
public getMatchPatterns(): string[] {
return ['https://contest.ucup.ac/contest/*/problem/*'];
}

public async parse(url: string, html: string): Promise<Sendable> {
const elem = htmlToElement(html);
const task = new TaskBuilder('Universal Cup').setUrl(url);

task.setName(elem.querySelector('h1.text-center').textContent.replace(/\s+/g, ' ').trim());
task.setCategory(elem.querySelector('h1.text-left').textContent.replace(/\s+/g, ' ').trim());

const attachmentsUrl = elem.querySelector<HTMLLinkElement>('a.nav-link[href^="/download"]').href;

try {
const files = await fetchZip(attachmentsUrl, ['.in', '.ans']);

const testCases: Record<string, { input: string; output: string }> = {};
for (const [fileName, fileContent] of Object.entries(files)) {
const fileNumber = fileName.match(/(\d+)/)?.[0];

if (fileNumber) {
testCases[fileNumber] = testCases[fileNumber] || { input: '', output: '' };
const fileType = fileName.endsWith('.in') ? 'input' : 'output';
testCases[fileNumber][fileType] = fileContent;
}
}

Object.values(testCases).forEach(t => task.addTest(t.input, t.output, false));
} catch (error) {
console.error('Error extracting test cases from ZIP:', error);
}

return task.build();
}
}
44 changes: 44 additions & 0 deletions src/utils/zip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import JSZip from 'jszip';

// JSZip doesn't seem to work properly in Firefox addons, this is a workaround to the issue
// See https://github.com/Stuk/jszip/issues/759 for more information
function processBlob(blob: Blob): Promise<ArrayBuffer | string> {
return new Promise(resolve => {
const fileReader = new FileReader();

fileReader.onload = event => {
resolve(event.target.result);
};

fileReader.readAsBinaryString(blob);
});
}

export async function fetchZip(url: string, extensions: string[]): Promise<Record<string, string>> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download ZIP file from ${url}`);
}

const blob = await response.blob();
const processedBlob = await processBlob(blob);

const zip = new JSZip();
const content = await zip.loadAsync(processedBlob);

const files: Record<string, string> = {};

for (const fileName in content.files) {
if (!Object.prototype.hasOwnProperty.call(content.files, fileName)) {
continue;
}

if (!extensions.some(ext => fileName.endsWith(ext))) {
continue;
}

files[fileName] = await content.files[fileName].async('string');
}

return files;
}

0 comments on commit 1775535

Please sign in to comment.