@rhseung/ps-cli 1.4.0 → 1.6.0
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/dist/chunk-AG6KWWHS.js +342 -0
- package/dist/{chunk-NB4OIWND.js → chunk-ASMT3CRD.js} +1 -142
- package/dist/chunk-GCOFFYJ3.js +47 -0
- package/dist/chunk-NH36IFWR.js +255 -0
- package/dist/{chunk-7SVCS23X.js → chunk-RVD22OUQ.js} +53 -25
- package/dist/commands/config.js +1 -1
- package/dist/commands/fetch.js +14 -256
- package/dist/commands/init.js +77 -22
- package/dist/commands/open.js +4 -44
- package/dist/commands/run.js +1 -1
- package/dist/commands/search.js +510 -0
- package/dist/commands/solve.js +163 -0
- package/dist/commands/stats.js +30 -16
- package/dist/commands/submit.js +1 -1
- package/dist/commands/test.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/utils/tier.ts
|
|
4
|
+
import gradient from "gradient-string";
|
|
5
|
+
var TIER_NAMES = [
|
|
6
|
+
void 0,
|
|
7
|
+
"Bronze V",
|
|
8
|
+
"Bronze IV",
|
|
9
|
+
"Bronze III",
|
|
10
|
+
"Bronze II",
|
|
11
|
+
"Bronze I",
|
|
12
|
+
"Silver V",
|
|
13
|
+
"Silver IV",
|
|
14
|
+
"Silver III",
|
|
15
|
+
"Silver II",
|
|
16
|
+
"Silver I",
|
|
17
|
+
"Gold V",
|
|
18
|
+
"Gold IV",
|
|
19
|
+
"Gold III",
|
|
20
|
+
"Gold II",
|
|
21
|
+
"Gold I",
|
|
22
|
+
"Platinum V",
|
|
23
|
+
"Platinum IV",
|
|
24
|
+
"Platinum III",
|
|
25
|
+
"Platinum II",
|
|
26
|
+
"Platinum I",
|
|
27
|
+
"Diamond V",
|
|
28
|
+
"Diamond IV",
|
|
29
|
+
"Diamond III",
|
|
30
|
+
"Diamond II",
|
|
31
|
+
"Diamond I",
|
|
32
|
+
"Ruby V",
|
|
33
|
+
"Ruby IV",
|
|
34
|
+
"Ruby III",
|
|
35
|
+
"Ruby II",
|
|
36
|
+
"Ruby I",
|
|
37
|
+
"Master"
|
|
38
|
+
];
|
|
39
|
+
var TIER_COLORS = [
|
|
40
|
+
void 0,
|
|
41
|
+
"#9d4900",
|
|
42
|
+
"#a54f00",
|
|
43
|
+
"#ad5600",
|
|
44
|
+
"#b55d0a",
|
|
45
|
+
"#c67739",
|
|
46
|
+
"#38546e",
|
|
47
|
+
"#3d5a74",
|
|
48
|
+
"#435f7a",
|
|
49
|
+
"#496580",
|
|
50
|
+
"#4e6a86",
|
|
51
|
+
"#d28500",
|
|
52
|
+
"#df8f00",
|
|
53
|
+
"#ec9a00",
|
|
54
|
+
"#f9a518",
|
|
55
|
+
"#ffb028",
|
|
56
|
+
"#00c78b",
|
|
57
|
+
"#00d497",
|
|
58
|
+
"#27e2a4",
|
|
59
|
+
"#3ef0b1",
|
|
60
|
+
"#51fdbd",
|
|
61
|
+
"#009ee5",
|
|
62
|
+
"#00a9f0",
|
|
63
|
+
"#00b4fc",
|
|
64
|
+
"#2bbfff",
|
|
65
|
+
"#41caff",
|
|
66
|
+
"#e0004c",
|
|
67
|
+
"#ea0053",
|
|
68
|
+
"#f5005a",
|
|
69
|
+
"#ff0062",
|
|
70
|
+
"#ff3071",
|
|
71
|
+
"#b300e0"
|
|
72
|
+
];
|
|
73
|
+
var TIER_IMAGE_BASE_URL = "https://d2gd6pc034wcta.cloudfront.net/tier";
|
|
74
|
+
var MASTER_TIER_GRADIENT = [
|
|
75
|
+
{ r: 255, g: 124, b: 168 },
|
|
76
|
+
{ r: 180, g: 145, b: 255 },
|
|
77
|
+
{ r: 124, g: 249, b: 255 }
|
|
78
|
+
];
|
|
79
|
+
var TIER_MIN_RATINGS = [
|
|
80
|
+
0,
|
|
81
|
+
// Unrated (tier 0): 0-29
|
|
82
|
+
30,
|
|
83
|
+
// Bronze V (tier 1)
|
|
84
|
+
60,
|
|
85
|
+
// Bronze IV (tier 2)
|
|
86
|
+
90,
|
|
87
|
+
// Bronze III (tier 3)
|
|
88
|
+
120,
|
|
89
|
+
// Bronze II (tier 4)
|
|
90
|
+
150,
|
|
91
|
+
// Bronze I (tier 5)
|
|
92
|
+
200,
|
|
93
|
+
// Silver V (tier 6)
|
|
94
|
+
300,
|
|
95
|
+
// Silver IV (tier 7)
|
|
96
|
+
400,
|
|
97
|
+
// Silver III (tier 8)
|
|
98
|
+
500,
|
|
99
|
+
// Silver II (tier 9)
|
|
100
|
+
650,
|
|
101
|
+
// Silver I (tier 10)
|
|
102
|
+
800,
|
|
103
|
+
// Gold V (tier 11)
|
|
104
|
+
950,
|
|
105
|
+
// Gold IV (tier 12)
|
|
106
|
+
1100,
|
|
107
|
+
// Gold III (tier 13)
|
|
108
|
+
1250,
|
|
109
|
+
// Gold II (tier 14)
|
|
110
|
+
1400,
|
|
111
|
+
// Gold I (tier 15)
|
|
112
|
+
1600,
|
|
113
|
+
// Platinum V (tier 16)
|
|
114
|
+
1750,
|
|
115
|
+
// Platinum IV (tier 17)
|
|
116
|
+
1900,
|
|
117
|
+
// Platinum III (tier 18)
|
|
118
|
+
2e3,
|
|
119
|
+
// Platinum II (tier 19)
|
|
120
|
+
2100,
|
|
121
|
+
// Platinum I (tier 20)
|
|
122
|
+
2200,
|
|
123
|
+
// Diamond V (tier 21)
|
|
124
|
+
2300,
|
|
125
|
+
// Diamond IV (tier 22)
|
|
126
|
+
2400,
|
|
127
|
+
// Diamond III (tier 23)
|
|
128
|
+
2500,
|
|
129
|
+
// Diamond II (tier 24)
|
|
130
|
+
2600,
|
|
131
|
+
// Diamond I (tier 25)
|
|
132
|
+
2700,
|
|
133
|
+
// Ruby V (tier 26)
|
|
134
|
+
2800,
|
|
135
|
+
// Ruby IV (tier 27)
|
|
136
|
+
2850,
|
|
137
|
+
// Ruby III (tier 28)
|
|
138
|
+
2900,
|
|
139
|
+
// Ruby II (tier 29)
|
|
140
|
+
2950,
|
|
141
|
+
// Ruby I (tier 30)
|
|
142
|
+
3e3
|
|
143
|
+
// Master (tier 31)
|
|
144
|
+
];
|
|
145
|
+
function getTierMinRating(tier) {
|
|
146
|
+
if (tier >= 0 && tier < TIER_MIN_RATINGS.length) {
|
|
147
|
+
return TIER_MIN_RATINGS[tier] ?? 0;
|
|
148
|
+
}
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
function getNextTierMinRating(tier) {
|
|
152
|
+
if (tier === 31) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
if (tier >= 0 && tier < TIER_MIN_RATINGS.length - 1) {
|
|
156
|
+
return TIER_MIN_RATINGS[tier + 1] ?? null;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
function calculateTierProgress(currentRating, tier) {
|
|
161
|
+
if (tier === 31) {
|
|
162
|
+
return 100;
|
|
163
|
+
}
|
|
164
|
+
const currentTierMin = getTierMinRating(tier);
|
|
165
|
+
const nextTierMin = getNextTierMinRating(tier);
|
|
166
|
+
if (nextTierMin === null) {
|
|
167
|
+
return 100;
|
|
168
|
+
}
|
|
169
|
+
if (currentRating < currentTierMin) {
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
if (currentRating >= nextTierMin) {
|
|
173
|
+
return 100;
|
|
174
|
+
}
|
|
175
|
+
const progress = (currentRating - currentTierMin) / (nextTierMin - currentTierMin) * 100;
|
|
176
|
+
return Math.max(0, Math.min(100, progress));
|
|
177
|
+
}
|
|
178
|
+
function getTierName(level) {
|
|
179
|
+
if (level === 0) return "Unrated";
|
|
180
|
+
if (level >= 1 && level < TIER_NAMES.length) {
|
|
181
|
+
return TIER_NAMES[level] || "Unrated";
|
|
182
|
+
}
|
|
183
|
+
return "Unrated";
|
|
184
|
+
}
|
|
185
|
+
function getTierColor(level) {
|
|
186
|
+
if (level === 0) return "#2d2d2d";
|
|
187
|
+
if (level === 31) {
|
|
188
|
+
return gradient([...MASTER_TIER_GRADIENT]);
|
|
189
|
+
}
|
|
190
|
+
if (level >= 1 && level < TIER_COLORS.length) {
|
|
191
|
+
return TIER_COLORS[level] || "#2d2d2d";
|
|
192
|
+
}
|
|
193
|
+
return "#2d2d2d";
|
|
194
|
+
}
|
|
195
|
+
function getTierImageUrl(level) {
|
|
196
|
+
return `${TIER_IMAGE_BASE_URL}/${level}.svg`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/services/solved-api.ts
|
|
200
|
+
var BASE_URL = "https://solved.ac/api/v3";
|
|
201
|
+
var USER_AGENT = "ps-cli/1.0.0";
|
|
202
|
+
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
|
|
203
|
+
let lastError = null;
|
|
204
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
205
|
+
try {
|
|
206
|
+
const response = await fetch(url, {
|
|
207
|
+
...options,
|
|
208
|
+
headers: {
|
|
209
|
+
"User-Agent": USER_AGENT,
|
|
210
|
+
...options.headers
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
if (response.status === 429) {
|
|
214
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
215
|
+
const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1e3 : (attempt + 1) * 1e3;
|
|
216
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
221
|
+
}
|
|
222
|
+
return response;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
225
|
+
if (attempt < maxRetries - 1) {
|
|
226
|
+
await new Promise(
|
|
227
|
+
(resolve) => setTimeout(resolve, (attempt + 1) * 1e3)
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
throw lastError || new Error("Request failed after retries");
|
|
233
|
+
}
|
|
234
|
+
async function getProblem(problemId) {
|
|
235
|
+
const url = `${BASE_URL}/problem/show?problemId=${problemId}`;
|
|
236
|
+
const response = await fetchWithRetry(url);
|
|
237
|
+
const data = await response.json();
|
|
238
|
+
return data;
|
|
239
|
+
}
|
|
240
|
+
async function getUserStats(handle) {
|
|
241
|
+
const url = `${BASE_URL}/user/show?handle=${handle}`;
|
|
242
|
+
const response = await fetchWithRetry(url);
|
|
243
|
+
const data = await response.json();
|
|
244
|
+
return data;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export {
|
|
248
|
+
getNextTierMinRating,
|
|
249
|
+
calculateTierProgress,
|
|
250
|
+
getTierName,
|
|
251
|
+
getTierColor,
|
|
252
|
+
getTierImageUrl,
|
|
253
|
+
getProblem,
|
|
254
|
+
getUserStats
|
|
255
|
+
};
|
|
@@ -9924,8 +9924,10 @@ var config = new Conf({
|
|
|
9924
9924
|
autoOpenEditor: false,
|
|
9925
9925
|
// 기본값: 자동 열기 비활성화
|
|
9926
9926
|
solvedAcHandle: void 0,
|
|
9927
|
-
problemDir: "problems"
|
|
9927
|
+
problemDir: "problems",
|
|
9928
9928
|
// 기본값: problems 디렉토리
|
|
9929
|
+
solvingDir: "solving"
|
|
9930
|
+
// 기본값: solving 디렉토리
|
|
9929
9931
|
}
|
|
9930
9932
|
});
|
|
9931
9933
|
var projectConfigCache = null;
|
|
@@ -10014,44 +10016,58 @@ function getProblemDir() {
|
|
|
10014
10016
|
}
|
|
10015
10017
|
return config.get("problemDir") ?? "problems";
|
|
10016
10018
|
}
|
|
10019
|
+
function getSolvingDir() {
|
|
10020
|
+
const projectConfig = getProjectConfigSync();
|
|
10021
|
+
if (projectConfig?.solvingDir !== void 0) {
|
|
10022
|
+
return projectConfig.solvingDir;
|
|
10023
|
+
}
|
|
10024
|
+
return config.get("solvingDir") ?? "solving";
|
|
10025
|
+
}
|
|
10017
10026
|
|
|
10018
10027
|
// src/utils/problem-id.ts
|
|
10019
10028
|
import { join as join2 } from "path";
|
|
10020
10029
|
function detectProblemIdFromPath(cwd = process.cwd()) {
|
|
10021
10030
|
const problemDir = getProblemDir();
|
|
10031
|
+
const solvingDir = getSolvingDir();
|
|
10022
10032
|
const normalizedPath = cwd.replace(/\\/g, "/");
|
|
10023
|
-
|
|
10033
|
+
const dirsToCheck = [problemDir, solvingDir].filter(
|
|
10034
|
+
(dir) => dir && dir !== "." && dir !== ""
|
|
10035
|
+
);
|
|
10036
|
+
if (dirsToCheck.length === 0) {
|
|
10024
10037
|
const segments = normalizedPath.split("/").filter(Boolean);
|
|
10025
10038
|
const lastSegment = segments[segments.length - 1];
|
|
10026
10039
|
if (lastSegment) {
|
|
10027
|
-
const
|
|
10028
|
-
if (!isNaN(
|
|
10029
|
-
return
|
|
10040
|
+
const problemId = parseInt(lastSegment, 10);
|
|
10041
|
+
if (!isNaN(problemId) && problemId > 0 && lastSegment === problemId.toString()) {
|
|
10042
|
+
return problemId;
|
|
10030
10043
|
}
|
|
10031
10044
|
}
|
|
10032
10045
|
return null;
|
|
10033
10046
|
}
|
|
10034
|
-
const
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10047
|
+
for (const dir of dirsToCheck) {
|
|
10048
|
+
const dirPattern = `/${dir}/`;
|
|
10049
|
+
const dirIndex = normalizedPath.indexOf(dirPattern);
|
|
10050
|
+
if (dirIndex === -1) {
|
|
10051
|
+
continue;
|
|
10052
|
+
}
|
|
10053
|
+
const afterDir = normalizedPath.substring(dirIndex + dirPattern.length);
|
|
10054
|
+
if (!afterDir) {
|
|
10055
|
+
continue;
|
|
10056
|
+
}
|
|
10057
|
+
const firstSegment = afterDir.split("/")[0];
|
|
10058
|
+
if (!firstSegment) {
|
|
10059
|
+
continue;
|
|
10060
|
+
}
|
|
10061
|
+
const problemId = parseInt(firstSegment, 10);
|
|
10062
|
+
if (isNaN(problemId) || problemId <= 0) {
|
|
10063
|
+
continue;
|
|
10064
|
+
}
|
|
10065
|
+
if (firstSegment !== problemId.toString()) {
|
|
10066
|
+
continue;
|
|
10067
|
+
}
|
|
10068
|
+
return problemId;
|
|
10053
10069
|
}
|
|
10054
|
-
return
|
|
10070
|
+
return null;
|
|
10055
10071
|
}
|
|
10056
10072
|
function getProblemId(args, cwd = process.cwd()) {
|
|
10057
10073
|
if (args.length > 0 && args[0]) {
|
|
@@ -10071,6 +10087,15 @@ function getProblemDirPath(problemId, cwd = process.cwd()) {
|
|
|
10071
10087
|
}
|
|
10072
10088
|
return join2(baseDir, problemDir, problemId.toString());
|
|
10073
10089
|
}
|
|
10090
|
+
function getSolvingDirPath(problemId, cwd = process.cwd()) {
|
|
10091
|
+
const solvingDir = getSolvingDir();
|
|
10092
|
+
const projectRoot = findProjectRoot(cwd);
|
|
10093
|
+
const baseDir = projectRoot || cwd;
|
|
10094
|
+
if (solvingDir === "." || solvingDir === "") {
|
|
10095
|
+
return join2(baseDir, problemId.toString());
|
|
10096
|
+
}
|
|
10097
|
+
return join2(baseDir, solvingDir, problemId.toString());
|
|
10098
|
+
}
|
|
10074
10099
|
|
|
10075
10100
|
// src/utils/execution-context.ts
|
|
10076
10101
|
import { readdir } from "fs/promises";
|
|
@@ -10249,14 +10274,17 @@ var CommandBuilder = class {
|
|
|
10249
10274
|
|
|
10250
10275
|
export {
|
|
10251
10276
|
Command,
|
|
10277
|
+
findProjectRoot,
|
|
10252
10278
|
getDefaultLanguage,
|
|
10253
10279
|
getEditor,
|
|
10254
10280
|
getAutoOpenEditor,
|
|
10255
10281
|
getSolvedAcHandle,
|
|
10256
10282
|
getProblemDir,
|
|
10283
|
+
getSolvingDir,
|
|
10257
10284
|
detectProblemIdFromPath,
|
|
10258
10285
|
getProblemId,
|
|
10259
10286
|
getProblemDirPath,
|
|
10287
|
+
getSolvingDirPath,
|
|
10260
10288
|
resolveProblemContext,
|
|
10261
10289
|
resolveLanguage,
|
|
10262
10290
|
findSolutionFile,
|
package/dist/commands/config.js
CHANGED
package/dist/commands/fetch.js
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
scrapeProblem
|
|
4
|
+
} from "../chunk-AG6KWWHS.js";
|
|
2
5
|
import {
|
|
3
6
|
getProblem,
|
|
4
7
|
getTierColor,
|
|
5
8
|
getTierImageUrl,
|
|
6
|
-
getTierName
|
|
7
|
-
|
|
8
|
-
} from "../chunk-NB4OIWND.js";
|
|
9
|
+
getTierName
|
|
10
|
+
} from "../chunk-NH36IFWR.js";
|
|
9
11
|
import {
|
|
10
12
|
Command,
|
|
11
13
|
CommandBuilder,
|
|
12
14
|
CommandDef,
|
|
13
15
|
getAutoOpenEditor,
|
|
14
16
|
getEditor,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from "../chunk-
|
|
17
|
+
getProblemId,
|
|
18
|
+
getSolvingDirPath
|
|
19
|
+
} from "../chunk-RVD22OUQ.js";
|
|
18
20
|
import {
|
|
19
21
|
__decorateClass,
|
|
20
22
|
getLanguageConfig,
|
|
@@ -33,16 +35,18 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
33
35
|
function ProblemDashboard({ problem }) {
|
|
34
36
|
const tierName = getTierName(problem.level);
|
|
35
37
|
const tierColor = getTierColor(problem.level);
|
|
38
|
+
const borderColorString = typeof tierColor === "string" ? tierColor : "#ff7ca8";
|
|
39
|
+
const textColorString = borderColorString;
|
|
36
40
|
return /* @__PURE__ */ jsx(
|
|
37
41
|
Box,
|
|
38
42
|
{
|
|
39
43
|
flexDirection: "column",
|
|
40
44
|
borderStyle: "round",
|
|
41
|
-
borderColor:
|
|
45
|
+
borderColor: borderColorString,
|
|
42
46
|
paddingX: 1,
|
|
43
47
|
alignSelf: "flex-start",
|
|
44
|
-
children: /* @__PURE__ */ jsxs(Text, { bold: true, children: [
|
|
45
|
-
|
|
48
|
+
children: /* @__PURE__ */ jsxs(Text, { bold: true, color: textColorString, children: [
|
|
49
|
+
tierName,
|
|
46
50
|
" ",
|
|
47
51
|
/* @__PURE__ */ jsxs(Text, { color: "white", children: [
|
|
48
52
|
"#",
|
|
@@ -80,7 +84,7 @@ function getProjectRoot() {
|
|
|
80
84
|
return join(__dirname, "../..");
|
|
81
85
|
}
|
|
82
86
|
async function generateProblemFiles(problem, language = "python") {
|
|
83
|
-
const problemDir =
|
|
87
|
+
const problemDir = getSolvingDirPath(problem.id);
|
|
84
88
|
await mkdir(problemDir, { recursive: true });
|
|
85
89
|
const langConfig = getLanguageConfig(language);
|
|
86
90
|
const projectRoot = getProjectRoot();
|
|
@@ -191,252 +195,6 @@ ${tags}
|
|
|
191
195
|
return problemDir;
|
|
192
196
|
}
|
|
193
197
|
|
|
194
|
-
// src/services/scraper.ts
|
|
195
|
-
import * as cheerio from "cheerio";
|
|
196
|
-
var BOJ_BASE_URL = "https://www.acmicpc.net";
|
|
197
|
-
function htmlToMarkdown($, element) {
|
|
198
|
-
if (element.length === 0) return "";
|
|
199
|
-
let result = "";
|
|
200
|
-
const contents = element.contents();
|
|
201
|
-
if (contents.length === 0) {
|
|
202
|
-
return element.text().trim();
|
|
203
|
-
}
|
|
204
|
-
contents.each((_, node) => {
|
|
205
|
-
if (node.type === "text") {
|
|
206
|
-
const text = node.data || "";
|
|
207
|
-
if (text.trim()) {
|
|
208
|
-
result += text;
|
|
209
|
-
}
|
|
210
|
-
} else if (node.type === "tag") {
|
|
211
|
-
const tagName = node.name.toLowerCase();
|
|
212
|
-
const $node = $(node);
|
|
213
|
-
switch (tagName) {
|
|
214
|
-
case "sup":
|
|
215
|
-
result += `^${htmlToMarkdown($, $node)}`;
|
|
216
|
-
break;
|
|
217
|
-
case "sub":
|
|
218
|
-
result += `<sub>${htmlToMarkdown($, $node)}</sub>`;
|
|
219
|
-
break;
|
|
220
|
-
case "strong":
|
|
221
|
-
case "b":
|
|
222
|
-
result += `**${htmlToMarkdown($, $node)}**`;
|
|
223
|
-
break;
|
|
224
|
-
case "em":
|
|
225
|
-
case "i":
|
|
226
|
-
result += `*${htmlToMarkdown($, $node)}*`;
|
|
227
|
-
break;
|
|
228
|
-
case "br":
|
|
229
|
-
result += "\n";
|
|
230
|
-
break;
|
|
231
|
-
case "p": {
|
|
232
|
-
const pContent = htmlToMarkdown($, $node);
|
|
233
|
-
if (pContent) {
|
|
234
|
-
result += pContent + "\n\n";
|
|
235
|
-
}
|
|
236
|
-
break;
|
|
237
|
-
}
|
|
238
|
-
case "div": {
|
|
239
|
-
const divContent = htmlToMarkdown($, $node);
|
|
240
|
-
if (divContent) {
|
|
241
|
-
result += divContent + "\n";
|
|
242
|
-
}
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
case "span":
|
|
246
|
-
result += htmlToMarkdown($, $node);
|
|
247
|
-
break;
|
|
248
|
-
case "code":
|
|
249
|
-
result += `\`${htmlToMarkdown($, $node)}\``;
|
|
250
|
-
break;
|
|
251
|
-
case "pre": {
|
|
252
|
-
const preContent = htmlToMarkdown($, $node);
|
|
253
|
-
if (preContent) {
|
|
254
|
-
result += `
|
|
255
|
-
\`\`\`
|
|
256
|
-
${preContent}
|
|
257
|
-
\`\`\`
|
|
258
|
-
`;
|
|
259
|
-
}
|
|
260
|
-
break;
|
|
261
|
-
}
|
|
262
|
-
case "ul":
|
|
263
|
-
case "ol":
|
|
264
|
-
$node.find("li").each((i, li) => {
|
|
265
|
-
const liContent = htmlToMarkdown($, $(li));
|
|
266
|
-
if (liContent) {
|
|
267
|
-
result += `- ${liContent}
|
|
268
|
-
`;
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
break;
|
|
272
|
-
case "li":
|
|
273
|
-
result += htmlToMarkdown($, $node);
|
|
274
|
-
break;
|
|
275
|
-
case "img": {
|
|
276
|
-
const imgSrc = $node.attr("src") || "";
|
|
277
|
-
const imgAlt = $node.attr("alt") || "";
|
|
278
|
-
if (imgSrc) {
|
|
279
|
-
let imageUrl = imgSrc;
|
|
280
|
-
if (imgSrc.startsWith("/")) {
|
|
281
|
-
imageUrl = `${BOJ_BASE_URL}${imgSrc}`;
|
|
282
|
-
} else if (!imgSrc.startsWith("http") && !imgSrc.startsWith("data:")) {
|
|
283
|
-
imageUrl = `${BOJ_BASE_URL}/${imgSrc}`;
|
|
284
|
-
}
|
|
285
|
-
result += ``;
|
|
286
|
-
}
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
default:
|
|
290
|
-
result += htmlToMarkdown($, $node);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
return result.trim();
|
|
295
|
-
}
|
|
296
|
-
async function scrapeProblem(problemId) {
|
|
297
|
-
const url = `${BOJ_BASE_URL}/problem/${problemId}`;
|
|
298
|
-
const response = await fetch(url, {
|
|
299
|
-
headers: {
|
|
300
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
if (!response.ok) {
|
|
304
|
-
throw new Error(`Failed to fetch problem page: HTTP ${response.status}`);
|
|
305
|
-
}
|
|
306
|
-
const html = await response.text();
|
|
307
|
-
const $ = cheerio.load(html);
|
|
308
|
-
const title = $("#problem_title").text().trim();
|
|
309
|
-
const descriptionEl = $("#problem_description");
|
|
310
|
-
let description = "";
|
|
311
|
-
if (descriptionEl.length > 0) {
|
|
312
|
-
description = htmlToMarkdown($, descriptionEl).trim();
|
|
313
|
-
if (!description) {
|
|
314
|
-
description = descriptionEl.text().trim();
|
|
315
|
-
}
|
|
316
|
-
} else {
|
|
317
|
-
const altDesc = $('[id*="description"]').first();
|
|
318
|
-
if (altDesc.length > 0) {
|
|
319
|
-
description = altDesc.text().trim();
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
const inputEl = $("#problem_input");
|
|
323
|
-
let inputFormat = "";
|
|
324
|
-
if (inputEl.length > 0) {
|
|
325
|
-
inputFormat = htmlToMarkdown($, inputEl).trim();
|
|
326
|
-
if (!inputFormat) {
|
|
327
|
-
inputFormat = inputEl.text().trim();
|
|
328
|
-
}
|
|
329
|
-
} else {
|
|
330
|
-
const altInput = $('[id*="input"]').first();
|
|
331
|
-
if (altInput.length > 0) {
|
|
332
|
-
inputFormat = altInput.text().trim();
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
const outputEl = $("#problem_output");
|
|
336
|
-
let outputFormat = "";
|
|
337
|
-
if (outputEl.length > 0) {
|
|
338
|
-
outputFormat = htmlToMarkdown($, outputEl).trim();
|
|
339
|
-
if (!outputFormat) {
|
|
340
|
-
outputFormat = outputEl.text().trim();
|
|
341
|
-
}
|
|
342
|
-
} else {
|
|
343
|
-
const altOutput = $('[id*="output"]').first();
|
|
344
|
-
if (altOutput.length > 0) {
|
|
345
|
-
outputFormat = altOutput.text().trim();
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
const problemInfo = {};
|
|
349
|
-
const problemInfoTable = $("#problem-info");
|
|
350
|
-
const tableInResponsive = $(".table-responsive table");
|
|
351
|
-
const targetTable = problemInfoTable.length > 0 ? problemInfoTable : tableInResponsive;
|
|
352
|
-
if (targetTable.length > 0) {
|
|
353
|
-
const headerRow = targetTable.find("thead tr");
|
|
354
|
-
const dataRow = targetTable.find("tbody tr");
|
|
355
|
-
if (headerRow.length > 0 && dataRow.length > 0) {
|
|
356
|
-
const headers = headerRow.find("th").map((_, th) => $(th).text().trim()).get();
|
|
357
|
-
const values = dataRow.find("td").map((_, td) => $(td).text().trim()).get();
|
|
358
|
-
headers.forEach((header, index) => {
|
|
359
|
-
if (values[index]) {
|
|
360
|
-
problemInfo[header] = values[index];
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
} else {
|
|
364
|
-
targetTable.find("tr").each((_, row) => {
|
|
365
|
-
const tds = $(row).find("td");
|
|
366
|
-
if (tds.length >= 2) {
|
|
367
|
-
const label = $(tds[0]).text().trim();
|
|
368
|
-
const value = $(tds[1]).text().trim();
|
|
369
|
-
problemInfo[label] = value;
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
const timeLimit = problemInfo["\uC2DC\uAC04 \uC81C\uD55C"] || problemInfo["Time Limit"] || void 0;
|
|
375
|
-
const memoryLimit = problemInfo["\uBA54\uBAA8\uB9AC \uC81C\uD55C"] || problemInfo["Memory Limit"] || void 0;
|
|
376
|
-
const submissions = problemInfo["\uC81C\uCD9C"] || problemInfo["Submit"] || void 0;
|
|
377
|
-
const accepted = problemInfo["\uC815\uB2F5"] || problemInfo["Accepted"] || void 0;
|
|
378
|
-
const acceptedUsers = problemInfo["\uB9DE\uD78C \uC0AC\uB78C"] || problemInfo["Accepted Users"] || void 0;
|
|
379
|
-
const acceptedRate = problemInfo["\uC815\uB2F5 \uBE44\uC728"] || problemInfo["Accepted Rate"] || void 0;
|
|
380
|
-
const testCases = [];
|
|
381
|
-
const sampleInputs = $(".sampledata").filter((_, el) => {
|
|
382
|
-
const id = $(el).attr("id");
|
|
383
|
-
return id?.startsWith("sample-input-") ?? false;
|
|
384
|
-
});
|
|
385
|
-
sampleInputs.each((_, el) => {
|
|
386
|
-
const inputId = $(el).attr("id");
|
|
387
|
-
if (!inputId) return;
|
|
388
|
-
const match = inputId.match(/sample-input-(\d+)/);
|
|
389
|
-
if (!match) return;
|
|
390
|
-
const sampleNumber = match[1];
|
|
391
|
-
const outputId = `sample-output-${sampleNumber}`;
|
|
392
|
-
const outputEl2 = $(`#${outputId}`);
|
|
393
|
-
if (outputEl2.length > 0) {
|
|
394
|
-
testCases.push({
|
|
395
|
-
input: $(el).text(),
|
|
396
|
-
output: outputEl2.text()
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
if (testCases.length === 0) {
|
|
401
|
-
$("pre").each((_, el) => {
|
|
402
|
-
const text = $(el).text().trim();
|
|
403
|
-
const prevText = $(el).prev().text().toLowerCase();
|
|
404
|
-
if (prevText.includes("\uC785\uB825") || prevText.includes("input")) {
|
|
405
|
-
const nextPre = $(el).next("pre");
|
|
406
|
-
if (nextPre.length > 0) {
|
|
407
|
-
testCases.push({
|
|
408
|
-
input: text,
|
|
409
|
-
output: nextPre.text().trim()
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
if (!title) {
|
|
416
|
-
throw new Error(
|
|
417
|
-
`\uBB38\uC81C ${problemId}\uC758 \uC81C\uBAA9\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. BOJ \uD398\uC774\uC9C0 \uAD6C\uC870\uAC00 \uBCC0\uACBD\uB418\uC5C8\uAC70\uB098 \uBB38\uC81C\uAC00 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
if (!description && !inputFormat && !outputFormat) {
|
|
421
|
-
throw new Error(
|
|
422
|
-
`\uBB38\uC81C ${problemId}\uC758 \uB0B4\uC6A9\uC744 \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. BOJ \uD398\uC774\uC9C0 \uAD6C\uC870\uAC00 \uBCC0\uACBD\uB418\uC5C8\uAC70\uB098 API \uC81C\uD55C\uC5D0 \uAC78\uB838\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.`
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
return {
|
|
426
|
-
title,
|
|
427
|
-
description,
|
|
428
|
-
inputFormat,
|
|
429
|
-
outputFormat,
|
|
430
|
-
testCases,
|
|
431
|
-
timeLimit,
|
|
432
|
-
memoryLimit,
|
|
433
|
-
submissions,
|
|
434
|
-
accepted,
|
|
435
|
-
acceptedUsers,
|
|
436
|
-
acceptedRate
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
|
|
440
198
|
// src/hooks/use-fetch-problem.ts
|
|
441
199
|
function useFetchProblem({
|
|
442
200
|
problemId,
|