Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## 关联 Issue

## 变更概述

## 变更类型

- [ ] fix Bug
- [ ] Feature
- [ ] UI / Style
- [ ] Refactor
- [ ] Documentation

## 涉及文件

- [ ] 前端

```
```

- [ ] 后端

```
```
24 changes: 21 additions & 3 deletions apps/vscode-extension/src/atcoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface AtCoderProblem {
inputFormat: string;
outputFormat: string;
samples: SampleCase[];
sampleUrl?: string;
}

function decodeEntities(text: string): string {
Expand Down Expand Up @@ -129,9 +130,22 @@ function extractSection(html: string, headings: string[]): string {
}

function getLangContent(html: string, lang: "en" | "ja"): string {
const pattern = `<span[^>]*\\sclass="[^"]*\\blang-${lang}\\b[^"]*"[^>]*>([\\s\\S]*?)<\\/span>`;
const match = html.match(new RegExp(pattern, "i"));
return match ? match[1] : html;
const startMatch = html.match(
new RegExp(`<span[^>]*\\sclass="[^"]*\\blang-${lang}\\b[^"]*"[^>]*>`, "i")
);
if (!startMatch || startMatch.index === undefined) return html;
const start = startMatch.index + startMatch[0].length;
const now = /<\/?span\b[^>]*>/gi;
let dep = 1;
now.lastIndex = start;
let m: RegExpExecArray | null;
for (; (m = now.exec(html)) !== null;) {
if (m[0].startsWith("</")) {
dep--;
if (dep === 0) return html.slice(start, m.index);
} else dep++;
}
return html.slice(start);
}

export function parseProblemPage(html: string, url: string): AtCoderProblem {
Expand All @@ -149,6 +163,9 @@ export function parseProblemPage(html: string, url: string): AtCoderProblem {
const inputFormat = extractSection(contentSrc, ["Input", "入力"]);
const outputFormat = extractSection(contentSrc, ["Output", "出力"]);

const sampleLinkMatch = contentSrc.match(/href="([^"]*output=sample[^"]*)"/i);
const sampleUrl = sampleLinkMatch ? sampleLinkMatch[1] : undefined;

const samples: SampleCase[] = [];
const sampleBlocks = Array.from(html.matchAll(/<h3[^>]*>\s*Sample\s*(Input|Output)\s*(\d+)\s*<\/h3>/gi));

Expand Down Expand Up @@ -190,6 +207,7 @@ export function parseProblemPage(html: string, url: string): AtCoderProblem {
inputFormat,
outputFormat,
samples,
sampleUrl,
};
}

Expand Down
33 changes: 24 additions & 9 deletions apps/vscode-extension/src/tools/deepl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,36 +33,51 @@ export async function translateTextFree(text: string, lang: string): Promise<str
}, id);

return new Promise((resolve, reject) => {
let settled = false;
const settle = (fn: () => void) => { if (!settled) { settled = true; fn(); } };

const req = https.request(
{
hostname: "www2.deepl.com",
path: "/jsonrpc",
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(postData),
"Host": "www2.deepl.com",
"Origin": "https://www.deepl.com",
"Referer": "https://www.deepl.com/",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
},
},
(res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
try {
const json = JSON.parse(data);
if (json?.result?.texts?.[0]?.text) {
resolve(json.result.texts[0].text);
} else {
settle(() => {
if (res.statusCode && res.statusCode >= 400) {
reject(new Error(res.statusCode === 429 ? "翻译请求过于频繁,请稍后再试" : `翻译接口错误 (${res.statusCode})`));
return;
}
try {
const json = JSON.parse(data);
if (json?.result?.texts?.[0]?.text) {
resolve(json.result.texts[0].text);
} else {
reject(new Error("翻译接口返回异常"));
}
} catch {
reject(new Error("翻译接口返回异常"));
}
} catch {
reject(new Error("翻译接口返回异常"));
}
});
});
}
);
req.on("error", () => reject(new Error("翻译请求失败")));

req.setTimeout(20000, () => req.destroy(new Error("翻译请求超时")));
req.on("error", (err: Error) =>
settle(() => reject(new Error(err.message === "翻译请求超时" ? "翻译请求超时" : `翻译请求失败: ${err.message}`)))
);
req.write(postData);
req.end();
});
Expand Down
11 changes: 11 additions & 0 deletions packages/webview/src/WebviewApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,17 @@ const WebviewApp: React.FC<WebviewAppProps> = ({
</div>
</div>
))
) : problem.sampleUrl ? (
<div className="flex flex-col items-start gap-2">
<div className="text-[12px] opacity-60">该题目没有内嵌样例,样例在外部链接中。</div>
<Button
onClick={() => vscode.postMessage({ command: "openBrowser", url: problem.sampleUrl! })}
size="sm"
className="h-[26px] text-[11px]"
>
查看样例
</Button>
</div>
) : (
<div className="text-[12px] opacity-60">当前题目没有找到样例。</div>
)}
Expand Down
1 change: 1 addition & 0 deletions packages/webview/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface ContestProblem {
inputFormat: string;
outputFormat: string;
samples: SampleCase[];
sampleUrl?: string;
}

export interface SubmitResult {
Expand Down
Loading