@jpetit/toolkit 3.0.22 → 3.1.1
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/assets/prompts/creators/create-solution.tpl.txt +10 -0
- package/assets/prompts/creators/create-statement.tpl.txt +21 -0
- package/assets/prompts/creators/create-translation.tpl.txt +5 -0
- package/assets/prompts/creators/private-test-cases.txt +6 -0
- package/assets/prompts/creators/sample-test-cases.txt +6 -0
- package/assets/prompts/creators/system-prompt.txt +2 -0
- package/assets/prompts/examples/statement-coda.tex +7 -0
- package/assets/prompts/examples/statement.tex +19 -0
- package/assets/prompts/generators/efficiency.md +41 -0
- package/assets/prompts/generators/hard.md +47 -0
- package/assets/prompts/generators/random.md +39 -0
- package/assets/prompts/proglangs/cc.md +3 -0
- package/assets/prompts/proglangs/py.md +40 -0
- package/package.json +48 -11
- package/toolkit/index.ts +32 -21
- package/lib/ai.ts +0 -144
- package/lib/cleaner.ts +0 -66
- package/lib/compilers/base.ts +0 -103
- package/lib/compilers/clojure.ts +0 -76
- package/lib/compilers/gcc.ts +0 -68
- package/lib/compilers/ghc.ts +0 -75
- package/lib/compilers/gxx.ts +0 -68
- package/lib/compilers/index.ts +0 -72
- package/lib/compilers/python3.ts +0 -105
- package/lib/compilers/run-haskell.ts +0 -113
- package/lib/compilers/run-python.ts +0 -109
- package/lib/data.ts +0 -19
- package/lib/doctor.ts +0 -158
- package/lib/generate.ts +0 -329
- package/lib/maker.ts +0 -700
- package/lib/settings.ts +0 -42
- package/lib/tui.ts +0 -142
- package/lib/types.ts +0 -20
- package/lib/utils.ts +0 -133
- package/toolkit/ai.ts +0 -30
- package/toolkit/clean.ts +0 -37
- package/toolkit/compilers.ts +0 -29
- package/toolkit/create-jutge-ai.ts +0 -101
- package/toolkit/create-template.ts +0 -55
- package/toolkit/create-wizard.ts +0 -6
- package/toolkit/create.ts +0 -65
- package/toolkit/doctor.ts +0 -18
- package/toolkit/generate.ts +0 -116
- package/toolkit/init.ts +0 -56
- package/toolkit/make.ts +0 -118
- package/toolkit/verify.ts +0 -19
package/lib/maker.ts
DELETED
|
@@ -1,700 +0,0 @@
|
|
|
1
|
-
import tui from '../lib/tui'
|
|
2
|
-
import { filesAreEqual, fileSize, nothing, projectDir, readText, readYaml, writeText } from '../lib/utils'
|
|
3
|
-
import chalk from 'chalk'
|
|
4
|
-
import { execa } from 'execa'
|
|
5
|
-
import { cp, exists, glob, mkdir, mkdtemp, rename } from 'fs/promises'
|
|
6
|
-
import { imageSizeFromFile } from 'image-size/fromFile'
|
|
7
|
-
import { tmpdir } from 'os'
|
|
8
|
-
import { basename, dirname, join, normalize, resolve, sep } from 'path'
|
|
9
|
-
import prettyBytes from 'pretty-bytes'
|
|
10
|
-
import prettyMs from 'pretty-ms'
|
|
11
|
-
import { getCompilerByExtension, getCompilerById } from './compilers'
|
|
12
|
-
import type { Compiler } from './compilers/base'
|
|
13
|
-
import { languageNames, proglangExtensions, proglangNames } from './data'
|
|
14
|
-
import { type Handler, ZHandler, ZScores } from './types'
|
|
15
|
-
import { th } from 'zod/locales'
|
|
16
|
-
|
|
17
|
-
export interface MakerOptions {
|
|
18
|
-
verbose?: boolean
|
|
19
|
-
directory: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function newMaker(options: MakerOptions): Promise<Maker> {
|
|
23
|
-
const maker = new Maker(options)
|
|
24
|
-
await maker.makeInspection()
|
|
25
|
-
return maker
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
type ExecutionResult = {
|
|
29
|
-
testcase: string
|
|
30
|
-
time: number
|
|
31
|
-
inputSize: number
|
|
32
|
-
outputSize: number
|
|
33
|
-
error: boolean
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export class Maker {
|
|
37
|
-
verbose: boolean
|
|
38
|
-
directory: string
|
|
39
|
-
structure: 'multi' | 'single' = 'multi'
|
|
40
|
-
// multi: many languages in the same directory
|
|
41
|
-
// single: one language in a subdirectory of a .pbm directory
|
|
42
|
-
language: string | null = null // only for single structure, tells which language it is
|
|
43
|
-
|
|
44
|
-
// @ts-expect-error: will be initialized later
|
|
45
|
-
handler: Handler
|
|
46
|
-
languages: string[] = []
|
|
47
|
-
originalLanguage: string | null = null
|
|
48
|
-
problemYmls: Record<string, any> = {} // language -> content
|
|
49
|
-
solutions: string[] = []
|
|
50
|
-
goldenSolution: string | null = null
|
|
51
|
-
testcases: string[] = [] // testcase names without extensions
|
|
52
|
-
|
|
53
|
-
public constructor(options: MakerOptions) {
|
|
54
|
-
this.verbose = options.verbose || false
|
|
55
|
-
this.directory = normalize(join(options.directory, '.')) // normalize path and ensure no trailing slash
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
public async makeInspection() {
|
|
59
|
-
await tui.section('Inspecting problem', async () => {
|
|
60
|
-
await this.loadStructure()
|
|
61
|
-
await this.loadLanguages()
|
|
62
|
-
await this.loadHandler()
|
|
63
|
-
await this.loadOriginalLanguage()
|
|
64
|
-
await this.loadSolutions()
|
|
65
|
-
await this.loadGoldenSolution()
|
|
66
|
-
await this.loadTestcases()
|
|
67
|
-
await this.loadScores()
|
|
68
|
-
await this.loadAwards()
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private async loadStructure() {
|
|
73
|
-
await tui.section('Determining structure', async () => {
|
|
74
|
-
await nothing()
|
|
75
|
-
if (this.directory.endsWith('.pbm')) {
|
|
76
|
-
this.structure = 'multi'
|
|
77
|
-
} else {
|
|
78
|
-
this.structure = 'single'
|
|
79
|
-
}
|
|
80
|
-
console.log(this.structure)
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private async loadLanguages() {
|
|
85
|
-
await tui.section('Loading languages', async () => {
|
|
86
|
-
if (this.structure === 'multi') {
|
|
87
|
-
await this.loadLanguagesMulti()
|
|
88
|
-
} else {
|
|
89
|
-
await this.loadLanguagesSingle()
|
|
90
|
-
}
|
|
91
|
-
})
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
private async loadLanguagesMulti() {
|
|
95
|
-
const files = await Array.fromAsync(glob('problem.*.yml', { cwd: this.directory }))
|
|
96
|
-
const languages = files
|
|
97
|
-
.map((file) => {
|
|
98
|
-
const match = file.match(/problem\.(.+)\.yml/)
|
|
99
|
-
return match ? match[1] : null
|
|
100
|
-
})
|
|
101
|
-
.filter((language) => language && language in languageNames)
|
|
102
|
-
this.languages = languages as string[]
|
|
103
|
-
tui.yaml(this.languages)
|
|
104
|
-
|
|
105
|
-
for (const language of this.languages) {
|
|
106
|
-
await tui.section(`Loading problem.${language}.yml`, async () => {
|
|
107
|
-
this.problemYmls[language] = await readYaml(`${this.directory}/problem.${language}.yml`)
|
|
108
|
-
tui.yaml(this.problemYmls[language])
|
|
109
|
-
})
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private async loadLanguagesSingle() {
|
|
114
|
-
for (const language of Object.keys(languageNames)) {
|
|
115
|
-
const name = join(this.directory, '..', language, `problem.${language}.yml`)
|
|
116
|
-
if (await exists(name)) {
|
|
117
|
-
this.languages.push(language)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
tui.yaml(this.languages)
|
|
121
|
-
|
|
122
|
-
for (const language of this.languages) {
|
|
123
|
-
const name = join(this.directory, '..', language, `problem.${language}.yml`)
|
|
124
|
-
await tui.section(`Loading ..${sep}${language}${sep}problem.${language}.yml`, async () => {
|
|
125
|
-
this.problemYmls[language] = await readYaml(name)
|
|
126
|
-
tui.yaml(this.problemYmls[language])
|
|
127
|
-
})
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
this.language = basename(this.directory)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
private async loadHandler() {
|
|
134
|
-
await tui.section('Loading handler.yml', async () => {
|
|
135
|
-
const data = await readYaml(`${this.directory}/handler.yml`)
|
|
136
|
-
this.handler = ZHandler.parse(data)
|
|
137
|
-
tui.yaml(this.handler)
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
private async loadScores() {
|
|
142
|
-
await tui.section('Loading scores.yml', async () => {
|
|
143
|
-
if (await exists(`${this.directory}/scores.yml`)) {
|
|
144
|
-
const data = await readYaml(`${this.directory}/scores.yml`)
|
|
145
|
-
const scores = ZScores.parse(data)
|
|
146
|
-
tui.yaml(scores)
|
|
147
|
-
} else {
|
|
148
|
-
tui.print('scores.yml not defined')
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
}
|
|
152
|
-
private async loadOriginalLanguage() {
|
|
153
|
-
await tui.section('Determining original language', async () => {
|
|
154
|
-
await nothing()
|
|
155
|
-
for (const language of this.languages) {
|
|
156
|
-
if ('author' in this.problemYmls[language]) {
|
|
157
|
-
this.originalLanguage = language
|
|
158
|
-
break
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
if (!this.originalLanguage) {
|
|
162
|
-
throw new Error('No original language found (a language with an author field)')
|
|
163
|
-
}
|
|
164
|
-
tui.print(this.originalLanguage)
|
|
165
|
-
})
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private async loadSolutions() {
|
|
169
|
-
await tui.section('Loading solutions', async () => {
|
|
170
|
-
const comaSeparatedExtensions = Object.keys(proglangNames).join(',')
|
|
171
|
-
const files = await Array.fromAsync(glob(`solution.{${comaSeparatedExtensions}}`, { cwd: this.directory }))
|
|
172
|
-
this.solutions = files
|
|
173
|
-
tui.yaml(this.solutions)
|
|
174
|
-
if (this.solutions.length === 0) throw new Error('No solutions found')
|
|
175
|
-
})
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
private async loadGoldenSolution() {
|
|
179
|
-
await tui.section('Determining golden solution', async () => {
|
|
180
|
-
if (this.handler.compilers === 'RunPython') {
|
|
181
|
-
this.goldenSolution = 'solution.py'
|
|
182
|
-
} else if (this.handler.compilers === 'RunHaskell' || this.handler.compilers === 'GHC') {
|
|
183
|
-
this.goldenSolution = 'solution.hs'
|
|
184
|
-
} else if (this.handler.compilers === 'RunClojure' || this.handler.compilers === 'Clojure') {
|
|
185
|
-
this.goldenSolution = 'solution.clj'
|
|
186
|
-
} else {
|
|
187
|
-
// any compilers
|
|
188
|
-
const solutionProglang = this.handler.solution
|
|
189
|
-
const extension = proglangExtensions[solutionProglang]
|
|
190
|
-
if (!extension) {
|
|
191
|
-
throw new Error(`Unknown programming language ${solutionProglang} for solution`)
|
|
192
|
-
}
|
|
193
|
-
const goldenSolutionPath = join(this.directory, `solution.${extension}`)
|
|
194
|
-
const fileExists = await exists(goldenSolutionPath)
|
|
195
|
-
if (!fileExists) {
|
|
196
|
-
throw new Error(`Golden solution file ${goldenSolutionPath} not found`)
|
|
197
|
-
}
|
|
198
|
-
this.goldenSolution = `solution.${extension}`
|
|
199
|
-
}
|
|
200
|
-
tui.print(this.goldenSolution)
|
|
201
|
-
})
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
private async loadTestcases() {
|
|
205
|
-
await tui.section('Loading testcases', async () => {
|
|
206
|
-
const files = await Array.fromAsync(glob('*.inp', { cwd: this.directory }))
|
|
207
|
-
this.testcases = files.map((file) => file.replace('.inp', '')).sort()
|
|
208
|
-
tui.yaml(this.testcases)
|
|
209
|
-
})
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
private async loadAwards() {
|
|
213
|
-
await tui.section('Loading awards', async () => {
|
|
214
|
-
if (await exists(join(this.directory, 'award.html'))) {
|
|
215
|
-
tui.success(tui.hyperlink(this.directory, 'award.html') + ' found')
|
|
216
|
-
} else {
|
|
217
|
-
tui.warning('award.html not found')
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (await exists(join(this.directory, 'award.png'))) {
|
|
221
|
-
tui.success(tui.hyperlink(this.directory, 'award.png') + ' found')
|
|
222
|
-
const dimensions = await imageSizeFromFile(join(this.directory, 'award.png'))
|
|
223
|
-
await tui.image(join(this.directory, 'award.png'), 6, 3)
|
|
224
|
-
tui.print(`${dimensions.width}x${dimensions.height}`)
|
|
225
|
-
} else {
|
|
226
|
-
tui.warning('award.png not found')
|
|
227
|
-
}
|
|
228
|
-
})
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async showDirectory() {
|
|
232
|
-
await tui.section('Directory', async () => {
|
|
233
|
-
await nothing()
|
|
234
|
-
tui.directory(resolve(this.directory))
|
|
235
|
-
const fullPath = normalize(resolve(this.directory))
|
|
236
|
-
if (!fullPath.endsWith('.pbm')) {
|
|
237
|
-
const lastPath = basename(fullPath)
|
|
238
|
-
const butLastPath = dirname(fullPath)
|
|
239
|
-
/*
|
|
240
|
-
*/
|
|
241
|
-
if (!Object.keys(languageNames).includes(lastPath) || !butLastPath.endsWith('.pbm')) {
|
|
242
|
-
throw new Error(
|
|
243
|
-
'The problem directory should end with .pbm or be a language id inside a .pbm directory',
|
|
244
|
-
)
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
})
|
|
248
|
-
}
|
|
249
|
-
public async makeProblem() {
|
|
250
|
-
await this.makeGoldenExecutable()
|
|
251
|
-
await this.makeCorrects()
|
|
252
|
-
await this.verifySolutions()
|
|
253
|
-
await this.makePdfs()
|
|
254
|
-
await this.makeTexts()
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
public async makeExecutable(program: string) {
|
|
258
|
-
const compiler = this.selectCompiler()
|
|
259
|
-
await tui.section(`Compiling ${tui.hyperlink(this.directory, program)} with ${compiler.name()}`, async () => {
|
|
260
|
-
try {
|
|
261
|
-
await compiler.compile(this.handler, this.directory, program)
|
|
262
|
-
} catch (error) {
|
|
263
|
-
throw new Error(`Compilation failed`)
|
|
264
|
-
}
|
|
265
|
-
})
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
public async makeExecutables() {
|
|
269
|
-
await tui.section('Compiling solutions', async () => {
|
|
270
|
-
for (const solution of this.solutions) {
|
|
271
|
-
await this.makeExecutable(solution)
|
|
272
|
-
}
|
|
273
|
-
})
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
public async makeGoldenExecutable() {
|
|
277
|
-
await tui.section(
|
|
278
|
-
`Compiling golden solution ${tui.hyperlink(this.directory, this.goldenSolution!)}`,
|
|
279
|
-
async () => {
|
|
280
|
-
if (!this.goldenSolution) {
|
|
281
|
-
throw new Error('Golden solution not set')
|
|
282
|
-
}
|
|
283
|
-
await this.makeExecutable(this.goldenSolution)
|
|
284
|
-
},
|
|
285
|
-
)
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
public async verifySolutions() {
|
|
289
|
-
for (const solution of this.solutions) {
|
|
290
|
-
if (solution !== this.goldenSolution) {
|
|
291
|
-
await this.verifyCandidate(solution)
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async makeCorrect(testcase: string, compiler: Compiler, sourcePath: string): Promise<ExecutionResult> {
|
|
297
|
-
return await this.runTestcase(testcase, `${testcase}.inp`, `${testcase}.cor`, compiler, sourcePath)
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
async runTestcase(
|
|
301
|
-
testcase: string,
|
|
302
|
-
input: string,
|
|
303
|
-
output: string,
|
|
304
|
-
compiler: Compiler,
|
|
305
|
-
sourcePath: string,
|
|
306
|
-
): Promise<ExecutionResult> {
|
|
307
|
-
let error = false
|
|
308
|
-
const start = Date.now()
|
|
309
|
-
try {
|
|
310
|
-
await compiler.execute(this.handler, this.directory, sourcePath, input, output)
|
|
311
|
-
} catch (e) {
|
|
312
|
-
tui.error(`Execution failed for testcase '${testcase}'`)
|
|
313
|
-
error = true
|
|
314
|
-
}
|
|
315
|
-
const end = Date.now()
|
|
316
|
-
const time = end - start
|
|
317
|
-
|
|
318
|
-
if (this.handler.handler === 'graphic') {
|
|
319
|
-
await rename(join(this.directory, 'output.png'), join(this.directory, output))
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const inputSize = await fileSize(`${this.directory}/${input}`)
|
|
323
|
-
const outputSize = await fileSize(`${this.directory}/${output}`)
|
|
324
|
-
|
|
325
|
-
return { testcase, error, time, inputSize, outputSize }
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
selectCompiler(): Compiler {
|
|
329
|
-
if (this.handler.compilers === 'RunPython') {
|
|
330
|
-
return getCompilerById('RunPython')
|
|
331
|
-
} else if (this.handler.compilers === 'RunHaskell') {
|
|
332
|
-
return getCompilerById('RunHaskell')
|
|
333
|
-
} else if (this.handler.compilers === 'RunClojure') {
|
|
334
|
-
return getCompilerById('RunClojure')
|
|
335
|
-
} else {
|
|
336
|
-
const extension = this.goldenSolution!.split('.').pop()!
|
|
337
|
-
return getCompilerByExtension(extension)
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
public async makeCorrects() {
|
|
342
|
-
const compiler = this.selectCompiler()
|
|
343
|
-
await tui.section(`Making corrects with golden solution`, async () => {
|
|
344
|
-
await tui.section(`Executing testcases using ${compiler.name()}`, async () => {
|
|
345
|
-
const results: ExecutionResult[] = []
|
|
346
|
-
|
|
347
|
-
for (const testcase of this.testcases) {
|
|
348
|
-
results.push(await this.makeCorrect(testcase, compiler, this.goldenSolution!))
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
console.log()
|
|
352
|
-
console.log(`testcase time input output`)
|
|
353
|
-
for (const result of results) {
|
|
354
|
-
const time = prettyMs(result.time)
|
|
355
|
-
const inputSize = prettyBytes(result.inputSize).replace(' ', '')
|
|
356
|
-
const outputSize = prettyBytes(result.outputSize).replace(' ', '')
|
|
357
|
-
console.log(
|
|
358
|
-
(result.error ? chalk.red : chalk.green)(
|
|
359
|
-
`${result.testcase.padEnd(12)} ${time.padStart(10)} ${inputSize.padStart(10)} ${outputSize.padStart(
|
|
360
|
-
10,
|
|
361
|
-
)}`,
|
|
362
|
-
),
|
|
363
|
-
)
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const errors = results.filter((result) => result.error).length
|
|
367
|
-
if (errors > 0) {
|
|
368
|
-
console.log()
|
|
369
|
-
throw new Error(`${errors} errors occurred while making correct answers`)
|
|
370
|
-
}
|
|
371
|
-
})
|
|
372
|
-
})
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
public async makePdfs() {
|
|
376
|
-
await tui.section('Making PDF statements', async () => {
|
|
377
|
-
const tmpDirBase = await mkdtemp(join(tmpdir(), `jutge-toolkit-pdf-`))
|
|
378
|
-
await tui.section('Creating working directory', async () => {
|
|
379
|
-
await nothing()
|
|
380
|
-
tui.directory(tmpDirBase)
|
|
381
|
-
})
|
|
382
|
-
try {
|
|
383
|
-
for (const language of this.languages) {
|
|
384
|
-
if (this.structure === 'multi' || (this.structure === 'single' && language === this.language)) {
|
|
385
|
-
await tui.section(`Making PDF statement for ${languageNames[language]}`, async () => {
|
|
386
|
-
await this.makePdf(tmpDirBase, language)
|
|
387
|
-
})
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
} finally {
|
|
391
|
-
// do not clean up because of hyperlinks
|
|
392
|
-
}
|
|
393
|
-
})
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
async makeSamples(tmpDir: string, language: string): Promise<[string, string]> {
|
|
397
|
-
const graphic = this.handler.handler === 'graphic'
|
|
398
|
-
const samples1c: string[] = []
|
|
399
|
-
const samples2c: string[] = []
|
|
400
|
-
let index = 1
|
|
401
|
-
for (const testcase of this.testcases) {
|
|
402
|
-
if (testcase.startsWith('sample')) {
|
|
403
|
-
let size = ''
|
|
404
|
-
if (graphic) {
|
|
405
|
-
await cp(join(this.directory, `${testcase}.cor`), join(tmpDir, `${testcase}.cor.png`))
|
|
406
|
-
const dimensions = await imageSizeFromFile(join(tmpDir, `${testcase}.cor.png`))
|
|
407
|
-
size = `(${dimensions.width}$\\times$${dimensions.height})`
|
|
408
|
-
}
|
|
409
|
-
samples1c.push(`\n\\SampleOneColInputOutput[${size}]{${testcase}}{${index}}\n`)
|
|
410
|
-
samples2c.push(`\n\\SampleTwoColInputOutput[${size}]{${testcase}}{${index}}\n`)
|
|
411
|
-
index++
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return [samples1c.join('\n'), samples2c.join('\n')]
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
async makePdf(tmpDirBase: string, language: string) {
|
|
418
|
-
const tmpDir = join(tmpDirBase, language)
|
|
419
|
-
await mkdir(tmpDir)
|
|
420
|
-
|
|
421
|
-
const date = new Date().toISOString().substring(0, 10)
|
|
422
|
-
const year = new Date().getFullYear()
|
|
423
|
-
const author = this.problemYmls[language].author || 'Unknown'
|
|
424
|
-
const authorEmail = this.problemYmls[language].author_email || 'unknown email'
|
|
425
|
-
const translator = this.problemYmls[language].translator || ''
|
|
426
|
-
|
|
427
|
-
const [samples1c, samples2c] = await this.makeSamples(tmpDir, language)
|
|
428
|
-
|
|
429
|
-
const root = `
|
|
430
|
-
|
|
431
|
-
\\documentclass[11pt]{article}
|
|
432
|
-
|
|
433
|
-
\\usepackage{judgeit}
|
|
434
|
-
\\usepackage{judgeit.${language}}
|
|
435
|
-
|
|
436
|
-
% TODO?
|
|
437
|
-
\\lstMakeShortInline@
|
|
438
|
-
|
|
439
|
-
\\begin{document}
|
|
440
|
-
|
|
441
|
-
\\providecommand{\\SampleOneCol}{${samples1c}}
|
|
442
|
-
\\providecommand{\\SampleTwoCol}{${samples2c}}
|
|
443
|
-
\\ProblemId{{DRAFT ${language}}}
|
|
444
|
-
\\DoProblem{${language}}
|
|
445
|
-
|
|
446
|
-
\\ProblemInformation
|
|
447
|
-
\\Author: ${author}\\\\ ${translator ? `(${language}: ${translator})` : ''}
|
|
448
|
-
\\Generation: ${date}
|
|
449
|
-
|
|
450
|
-
\\subsection*{Draft}
|
|
451
|
-
Draft generated with \\textbf{new-jutge-toolkit}.
|
|
452
|
-
|
|
453
|
-
\\end{document}
|
|
454
|
-
|
|
455
|
-
`
|
|
456
|
-
|
|
457
|
-
// copy files to tmpDir
|
|
458
|
-
await cp(this.directory, tmpDir, { recursive: true })
|
|
459
|
-
|
|
460
|
-
// write root.tex
|
|
461
|
-
await writeText(join(tmpDir, 'root.tex'), root)
|
|
462
|
-
|
|
463
|
-
// tweak the tex file
|
|
464
|
-
const tex1 = await readText(join(tmpDir, `problem.${language}.tex`))
|
|
465
|
-
const tex2 = tex1
|
|
466
|
-
.replace(/\\begin{htmlonly}[\s\S]*?\\end{htmlonly}/g, '')
|
|
467
|
-
.replace(/\\begin{latexonly}/g, '')
|
|
468
|
-
.replace(/\\end{latexonly}/g, '')
|
|
469
|
-
.replace(/\.eps}/g, '}')
|
|
470
|
-
await writeText(join(tmpDir, `problem.${language}.tex`), tex2)
|
|
471
|
-
|
|
472
|
-
// copy style files
|
|
473
|
-
await this.copyStyleFiles(tmpDir, language)
|
|
474
|
-
|
|
475
|
-
// latex
|
|
476
|
-
try {
|
|
477
|
-
tui.command('xelatex -interaction=nonstopmode -file-line-error root.tex')
|
|
478
|
-
await execa({
|
|
479
|
-
stderr: 'inherit',
|
|
480
|
-
// stdout: 'inherit',
|
|
481
|
-
cwd: tmpDir,
|
|
482
|
-
})`xelatex -interaction=nonstopmode -file-line-error root.tex`
|
|
483
|
-
await cp(join(tmpDir, 'root.pdf'), join(this.directory, `problem.${language}.pdf`))
|
|
484
|
-
tui.success(
|
|
485
|
-
`Generated ${tui.hyperlink(this.directory, `problem.${language}.pdf`)} see ${tui.hyperlink(tmpDir, `root.log`)}`,
|
|
486
|
-
)
|
|
487
|
-
} catch (e) {
|
|
488
|
-
tui.error(`Error in LaTeX: ${tui.hyperlink(tmpDir, `root.log`)}`)
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
async copyStyleFiles(tmpDir: string, language: string) {
|
|
493
|
-
const cpSty = async (path: string) => {
|
|
494
|
-
const source = join(projectDir(), 'assets', 'sty', path)
|
|
495
|
-
await cp(source, join(tmpDir, path))
|
|
496
|
-
}
|
|
497
|
-
await cpSty('picins.sty')
|
|
498
|
-
await cpSty('judgeit.sty')
|
|
499
|
-
if (language === 'ca') await cpSty('judgeit.ca.sty')
|
|
500
|
-
if (language === 'es') await cpSty('judgeit.es.sty')
|
|
501
|
-
if (language === 'en') await cpSty('judgeit.en.sty')
|
|
502
|
-
if (language === 'fr') await cpSty('judgeit.fr.sty')
|
|
503
|
-
if (language === 'de') await cpSty('judgeit.de.sty')
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
public async makeTexts() {
|
|
507
|
-
await tui.section('Making text statements', async () => {
|
|
508
|
-
const tmpDirBase = await mkdtemp(join(tmpdir(), `jutge-toolkit-text-`))
|
|
509
|
-
await tui.section('Creating working directory', async () => {
|
|
510
|
-
await nothing()
|
|
511
|
-
tui.directory(tmpDirBase)
|
|
512
|
-
})
|
|
513
|
-
try {
|
|
514
|
-
for (const language of this.languages) {
|
|
515
|
-
if (this.structure === 'multi' || (this.structure === 'single' && language === this.language)) {
|
|
516
|
-
await tui.section(`Making text statements for ${languageNames[language]}`, async () => {
|
|
517
|
-
await this.makeText(tmpDirBase, language)
|
|
518
|
-
})
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
} finally {
|
|
522
|
-
// do not clean up because of hyperlinks
|
|
523
|
-
}
|
|
524
|
-
})
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
async makeText(tmpDirBase: string, language: string) {
|
|
528
|
-
const tmpDir = join(tmpDirBase, language)
|
|
529
|
-
await mkdir(tmpDir)
|
|
530
|
-
|
|
531
|
-
const date = new Date().toISOString().substring(0, 10)
|
|
532
|
-
const year = new Date().getFullYear()
|
|
533
|
-
const author = this.problemYmls[language].author || 'Unknown'
|
|
534
|
-
const authorEmail = this.problemYmls[language].author_email || 'unknown email'
|
|
535
|
-
const translator = this.problemYmls[language].translator || ''
|
|
536
|
-
|
|
537
|
-
const root = `
|
|
538
|
-
|
|
539
|
-
\\documentclass[11pt]{article}
|
|
540
|
-
|
|
541
|
-
\\usepackage{judgeit}
|
|
542
|
-
\\usepackage{judgeit.${language}}
|
|
543
|
-
|
|
544
|
-
% TODO?
|
|
545
|
-
\\lstMakeShortInline@
|
|
546
|
-
|
|
547
|
-
% redefine commands to simplify text output
|
|
548
|
-
\\renewcommand{\\Sample}{}
|
|
549
|
-
\\renewcommand{\\Statement}{}
|
|
550
|
-
\\renewcommand{\\Problem}[1]{\\section{#1}}
|
|
551
|
-
\\renewcommand{\\ProblemId}[1]{DRAFT ${language}}
|
|
552
|
-
|
|
553
|
-
% redefine figure commands to avoid troubles with \\parpic
|
|
554
|
-
\\renewcommand{\\FigureL}[2]{\\includegraphics[#1]{#2}}
|
|
555
|
-
\\renewcommand{\\FigureC}[2]{\\includegraphics[#1]{#2}}
|
|
556
|
-
\\renewcommand{\\FigureR}[2]{\\includegraphics[#1]{#2}}
|
|
557
|
-
|
|
558
|
-
\\begin{document}
|
|
559
|
-
|
|
560
|
-
\\DoProblem{${language}}
|
|
561
|
-
|
|
562
|
-
\\subsection*{\\TxtAuthor}
|
|
563
|
-
${author} ${translator ? `(${language}: ${translator})` : ''}
|
|
564
|
-
|
|
565
|
-
\\subsection*{Draft}
|
|
566
|
-
Draft generated with \\textbf{new-jutge-toolkit}.
|
|
567
|
-
|
|
568
|
-
\\end{document}
|
|
569
|
-
|
|
570
|
-
`
|
|
571
|
-
|
|
572
|
-
// copy files to tmpDir
|
|
573
|
-
await cp(this.directory, tmpDir, { recursive: true })
|
|
574
|
-
|
|
575
|
-
// write root.tex
|
|
576
|
-
await writeText(join(tmpDir, 'root.tex'), root)
|
|
577
|
-
|
|
578
|
-
// tweak the tex file
|
|
579
|
-
const tex1 = await readText(join(tmpDir, `problem.${language}.tex`))
|
|
580
|
-
const tex2 = tex1
|
|
581
|
-
.replace(/\\begin{latexonly}[\s\S]*?\\end{latexonly}/g, '')
|
|
582
|
-
.replace(/\\begin{htmlonly}/g, '')
|
|
583
|
-
.replace(/\\end{htmlonly}/g, '')
|
|
584
|
-
.replace(/\\begin{minipage}/g, '')
|
|
585
|
-
.replace(/\\end{minipage}/g, '')
|
|
586
|
-
.replace(/\.eps}/g, '}')
|
|
587
|
-
await writeText(join(tmpDir, `problem.${language}.tex`), tex2)
|
|
588
|
-
|
|
589
|
-
// copy style files
|
|
590
|
-
await this.copyStyleFiles(tmpDir, language)
|
|
591
|
-
|
|
592
|
-
// convert .eps files to png
|
|
593
|
-
const files = (await Array.fromAsync(glob('*.eps', { cwd: tmpDir }))).sort()
|
|
594
|
-
for (const file of files) {
|
|
595
|
-
tui.command(`convert ${file} ${file.replace(/\.eps$/, '.png')}`)
|
|
596
|
-
await execa({ cwd: tmpDir })`convert ${file} ${file.replace(/\.eps$/, '.png')}`
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// create lua files
|
|
600
|
-
const luaSource = join(projectDir(), 'assets', 'lua', 'fixCodeBlocks.lua')
|
|
601
|
-
await cp(luaSource, join(tmpDir, 'fixCodeBlocks.lua'))
|
|
602
|
-
|
|
603
|
-
// txt
|
|
604
|
-
try {
|
|
605
|
-
tui.command('pandoc --quiet root.tex --to plain --output root.txt')
|
|
606
|
-
await execa({ cwd: tmpDir })`pandoc --quiet root.tex --to plain --output root.txt`
|
|
607
|
-
await cp(join(tmpDir, 'root.txt'), join(this.directory, `problem.${language}.txt`))
|
|
608
|
-
tui.success('Generated ' + tui.hyperlink(this.directory, `problem.${language}.txt`))
|
|
609
|
-
} catch (e) {
|
|
610
|
-
console.error('pandoc error', e)
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// md
|
|
614
|
-
try {
|
|
615
|
-
tui.command(
|
|
616
|
-
'pandoc --quiet root.tex --to markdown --to markdown-header_attributes --lua-filter=fixCodeBlocks.lua --output root.md',
|
|
617
|
-
)
|
|
618
|
-
await execa({
|
|
619
|
-
cwd: tmpDir,
|
|
620
|
-
})`pandoc --quiet root.tex --to markdown --to markdown-header_attributes --lua-filter=fixCodeBlocks.lua --output root.md`
|
|
621
|
-
await cp(join(tmpDir, 'root.md'), join(this.directory, `problem.${language}.md`))
|
|
622
|
-
tui.success('Generated ' + tui.hyperlink(this.directory, `problem.${language}.md`))
|
|
623
|
-
} catch (e) {
|
|
624
|
-
console.error('pandoc error', e)
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// md
|
|
628
|
-
try {
|
|
629
|
-
tui.command('pandoc --quiet root.tex --to html --mathml --embed-resources --output root.html')
|
|
630
|
-
await execa({
|
|
631
|
-
cwd: tmpDir,
|
|
632
|
-
})`pandoc --quiet root.tex --to html --mathml --embed-resources --output root.html`
|
|
633
|
-
await cp(join(tmpDir, 'root.html'), join(this.directory, `problem.${language}.html`))
|
|
634
|
-
tui.success('Generated ' + tui.hyperlink(this.directory, `problem.${language}.html`))
|
|
635
|
-
} catch (e) {
|
|
636
|
-
console.error('pandoc error', e)
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
public async verifyCandidate(program: string) {
|
|
641
|
-
await tui.section(`Verifying ${program}`, async () => {
|
|
642
|
-
const extension = program.split('.').pop()!
|
|
643
|
-
const compiler = getCompilerByExtension(extension)
|
|
644
|
-
|
|
645
|
-
await tui.section(
|
|
646
|
-
`Using compiler ${compiler.name()} to compile ${tui.hyperlink(this.directory, program)}`,
|
|
647
|
-
async () => {
|
|
648
|
-
try {
|
|
649
|
-
await compiler.compile(this.handler, this.directory, program)
|
|
650
|
-
} catch (error) {
|
|
651
|
-
throw new Error(`Compilation failed`)
|
|
652
|
-
}
|
|
653
|
-
},
|
|
654
|
-
)
|
|
655
|
-
|
|
656
|
-
const results: ExecutionResult[] = []
|
|
657
|
-
await tui.section('Executing testcases', async () => {
|
|
658
|
-
for (const testcase of this.testcases) {
|
|
659
|
-
results.push(
|
|
660
|
-
await this.runTestcase(
|
|
661
|
-
testcase,
|
|
662
|
-
`${testcase}.inp`,
|
|
663
|
-
`${testcase}.${extension}.out`,
|
|
664
|
-
compiler,
|
|
665
|
-
program,
|
|
666
|
-
),
|
|
667
|
-
)
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
console.log()
|
|
671
|
-
console.log(`testcase time status`)
|
|
672
|
-
let errors = 0
|
|
673
|
-
for (const result of results) {
|
|
674
|
-
const status = result.error
|
|
675
|
-
? 'EE'
|
|
676
|
-
: (await filesAreEqual(
|
|
677
|
-
`${this.directory}/${result.testcase}.cor`,
|
|
678
|
-
`${this.directory}/${result.testcase}.${extension}.out`,
|
|
679
|
-
))
|
|
680
|
-
? 'OK'
|
|
681
|
-
: 'WA'
|
|
682
|
-
const time = prettyMs(result.time)
|
|
683
|
-
console.log(
|
|
684
|
-
(status !== 'OK' ? chalk.red : chalk.green)(
|
|
685
|
-
`${result.testcase.padEnd(12)} ${time.padStart(10)} ${status.padStart(10)}`,
|
|
686
|
-
),
|
|
687
|
-
)
|
|
688
|
-
if (status !== 'OK') errors++
|
|
689
|
-
}
|
|
690
|
-
console.log()
|
|
691
|
-
|
|
692
|
-
if (errors) {
|
|
693
|
-
tui.error(`${errors} errors found for ${program}`)
|
|
694
|
-
} else {
|
|
695
|
-
tui.success(`All testcases passed successfully for ${program}`)
|
|
696
|
-
}
|
|
697
|
-
})
|
|
698
|
-
})
|
|
699
|
-
}
|
|
700
|
-
}
|