@jpetit/toolkit 3.0.14 → 3.0.16

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/lib/maker.ts ADDED
@@ -0,0 +1,686 @@
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
+
131
+ private async loadHandler() {
132
+ await tui.section('Loading handler.yml', async () => {
133
+ const data = await readYaml(`${this.directory}/handler.yml`)
134
+ this.handler = ZHandler.parse(data)
135
+ tui.yaml(this.handler)
136
+ })
137
+ }
138
+
139
+ private async loadScores() {
140
+ await tui.section('Loading scores.yml', async () => {
141
+ if (await exists(`${this.directory}/scores.yml`)) {
142
+ const data = await readYaml(`${this.directory}/scores.yml`)
143
+ const scores = ZScores.parse(data)
144
+ tui.yaml(scores)
145
+ } else {
146
+ tui.print('scores.yml not defined')
147
+ }
148
+ })
149
+ }
150
+ private async loadOriginalLanguage() {
151
+ await tui.section('Determining original language', async () => {
152
+ await nothing()
153
+ for (const language of this.languages) {
154
+ if ('author' in this.problemYmls[language]) {
155
+ this.originalLanguage = language
156
+ break
157
+ }
158
+ }
159
+ if (!this.originalLanguage) {
160
+ throw new Error('No original language found (a language with an author field)')
161
+ }
162
+ tui.print(this.originalLanguage)
163
+ })
164
+ }
165
+
166
+ private async loadSolutions() {
167
+ await tui.section('Loading solutions', async () => {
168
+ const comaSeparatedExtensions = Object.keys(proglangNames).join(',')
169
+ const files = await Array.fromAsync(glob(`solution.{${comaSeparatedExtensions}}`, { cwd: this.directory }))
170
+ this.solutions = files
171
+ tui.yaml(this.solutions)
172
+ if (this.solutions.length === 0) throw new Error('No solutions found')
173
+ })
174
+ }
175
+
176
+ private async loadGoldenSolution() {
177
+ await tui.section('Determining golden solution', async () => {
178
+ if (this.handler.compilers === 'RunPython') {
179
+ this.goldenSolution = 'solution.py'
180
+ } else if (this.handler.compilers === 'RunHaskell' || this.handler.compilers === 'GHC') {
181
+ this.goldenSolution = 'solution.hs'
182
+ } else if (this.handler.compilers === 'RunClojure' || this.handler.compilers === 'Clojure') {
183
+ this.goldenSolution = 'solution.clj'
184
+ } else {
185
+ // any compilers
186
+ const solutionProglang = this.handler.solution
187
+ const extension = proglangExtensions[solutionProglang]
188
+ if (!extension) {
189
+ throw new Error(`Unknown programming language ${solutionProglang} for solution`)
190
+ }
191
+ const goldenSolutionPath = join(this.directory, `solution.${extension}`)
192
+ const fileExists = await exists(goldenSolutionPath)
193
+ if (!fileExists) {
194
+ throw new Error(`Golden solution file ${goldenSolutionPath} not found`)
195
+ }
196
+ this.goldenSolution = `solution.${extension}`
197
+ }
198
+ tui.print(this.goldenSolution)
199
+ })
200
+ }
201
+
202
+ private async loadTestcases() {
203
+ await tui.section('Loading testcases', async () => {
204
+ const files = await Array.fromAsync(glob('*.inp', { cwd: this.directory }))
205
+ this.testcases = files.map((file) => file.replace('.inp', '')).sort()
206
+ tui.yaml(this.testcases)
207
+ })
208
+ }
209
+
210
+ private async loadAwards() {
211
+ await tui.section('Loading awards', async () => {
212
+ if (await exists(join(this.directory, 'award.html'))) {
213
+ tui.success(tui.hyperlink(this.directory, 'award.html') + ' found')
214
+ } else {
215
+ tui.warning('award.html not found')
216
+ }
217
+
218
+ if (await exists(join(this.directory, 'award.png'))) {
219
+ tui.success(tui.hyperlink(this.directory, 'award.png') + ' found')
220
+ const dimensions = await imageSizeFromFile(join(this.directory, 'award.png'))
221
+ await tui.image(join(this.directory, 'award.png'), 6, 3)
222
+ tui.print(`${dimensions.width}x${dimensions.height}`)
223
+ } else {
224
+ tui.warning('award.png not found')
225
+ }
226
+ })
227
+ }
228
+
229
+ async showDirectory() {
230
+ await tui.section('Directory', async () => {
231
+ await nothing()
232
+ tui.directory(resolve(this.directory))
233
+ const fullPath = normalize(resolve(this.directory))
234
+ if (!fullPath.endsWith('.pbm')) {
235
+ const lastPath = basename(fullPath)
236
+ const butLastPath = dirname(fullPath)
237
+ /*
238
+ */
239
+ if (!Object.keys(languageNames).includes(lastPath) || !butLastPath.endsWith('.pbm')) {
240
+ throw new Error(
241
+ 'The problem directory should end with .pbm or be a language id inside a .pbm directory',
242
+ )
243
+ }
244
+ }
245
+ })
246
+ }
247
+ public async makeProblem() {
248
+ await this.makeGoldenExecutable()
249
+ await this.makeCorrects()
250
+ await this.verifySolutions()
251
+ await this.makePdfs()
252
+ await this.makeTexts()
253
+ }
254
+
255
+ public async makeExecutable(program: string) {
256
+ const compiler = this.selectCompiler()
257
+ await tui.section(`Compiling ${tui.hyperlink(this.directory, program)} with ${compiler.name()}`, async () => {
258
+ try {
259
+ await compiler.compile(this.handler, this.directory, program)
260
+ } catch (error) {
261
+ throw new Error(`Compilation failed`)
262
+ }
263
+ })
264
+ }
265
+
266
+ public async makeExecutables() {
267
+ await tui.section('Compiling solutions', async () => {
268
+ for (const solution of this.solutions) {
269
+ await this.makeExecutable(solution)
270
+ }
271
+ })
272
+ }
273
+
274
+ public async makeGoldenExecutable() {
275
+ await tui.section(
276
+ `Compiling golden solution ${tui.hyperlink(this.directory, this.goldenSolution!)}`,
277
+ async () => {
278
+ if (!this.goldenSolution) {
279
+ throw new Error('Golden solution not set')
280
+ }
281
+ await this.makeExecutable(this.goldenSolution)
282
+ },
283
+ )
284
+ }
285
+
286
+ public async verifySolutions() {
287
+ for (const solution of this.solutions) {
288
+ if (solution !== this.goldenSolution) {
289
+ await this.verifyCandidate(solution)
290
+ }
291
+ }
292
+ }
293
+
294
+ async makeCorrect(testcase: string, compiler: Compiler): Promise<ExecutionResult> {
295
+ return await this.runTestcase(testcase, `${testcase}.inp`, `${testcase}.cor`, compiler)
296
+ }
297
+
298
+ async runTestcase(testcase: string, input: string, output: string, compiler: Compiler): Promise<ExecutionResult> {
299
+ let error = false
300
+ const start = Date.now()
301
+ try {
302
+ await compiler.execute(this.handler, this.directory, input, output)
303
+ } catch (e) {
304
+ tui.error(`Execution failed for testcase '${testcase}'`)
305
+ error = true
306
+ }
307
+ const end = Date.now()
308
+ const time = end - start
309
+
310
+ if (this.handler.handler === 'graphic') {
311
+ await rename(join(this.directory, 'output.png'), join(this.directory, output))
312
+ }
313
+
314
+ const inputSize = await fileSize(`${this.directory}/${input}`)
315
+ const outputSize = await fileSize(`${this.directory}/${output}`)
316
+
317
+ return { testcase, error, time, inputSize, outputSize }
318
+ }
319
+
320
+ selectCompiler(): Compiler {
321
+ if (this.handler.compilers === 'RunPython') {
322
+ return getCompilerById('RunPython')
323
+ } else if (this.handler.compilers === 'RunHaskell') {
324
+ return getCompilerById('RunHaskell')
325
+ } else if (this.handler.compilers === 'RunClojure') {
326
+ return getCompilerById('RunClojure')
327
+ } else {
328
+ const extension = this.goldenSolution!.split('.').pop()!
329
+ return getCompilerByExtension(extension)
330
+ }
331
+ }
332
+
333
+ public async makeCorrects() {
334
+ const compiler = this.selectCompiler()
335
+ await tui.section(`Making corrects with golden solution`, async () => {
336
+ await tui.section(`Executing testcases using ${compiler.name()}`, async () => {
337
+ const results: ExecutionResult[] = []
338
+
339
+ for (const testcase of this.testcases) {
340
+ results.push(await this.makeCorrect(testcase, compiler))
341
+ }
342
+
343
+ console.log()
344
+ console.log(`testcase time input output`)
345
+ for (const result of results) {
346
+ const time = prettyMs(result.time)
347
+ const inputSize = prettyBytes(result.inputSize).replace(' ', '')
348
+ const outputSize = prettyBytes(result.outputSize).replace(' ', '')
349
+ console.log(
350
+ (result.error ? chalk.red : chalk.green)(
351
+ `${result.testcase.padEnd(12)} ${time.padStart(10)} ${inputSize.padStart(10)} ${outputSize.padStart(
352
+ 10,
353
+ )}`,
354
+ ),
355
+ )
356
+ }
357
+
358
+ const errors = results.filter((result) => result.error).length
359
+ if (errors > 0) {
360
+ console.log()
361
+ throw new Error(`${errors} errors occurred while making correct answers`)
362
+ }
363
+ })
364
+ })
365
+ }
366
+
367
+ public async makePdfs() {
368
+ await tui.section('Making PDF statements', async () => {
369
+ const tmpDirBase = await mkdtemp(join(tmpdir(), `jutge-toolkit-pdf-`))
370
+ await tui.section('Creating working directory', async () => {
371
+ await nothing()
372
+ tui.directory(tmpDirBase)
373
+ })
374
+ try {
375
+ for (const language of this.languages) {
376
+ if (this.structure === 'multi' || (this.structure === 'single' && language === this.language)) {
377
+ await tui.section(`Making PDF statement for ${languageNames[language]}`, async () => {
378
+ await this.makePdf(tmpDirBase, language)
379
+ })
380
+ }
381
+ }
382
+ } finally {
383
+ // do not clean up because of hyperlinks
384
+ }
385
+ })
386
+ }
387
+
388
+ async makeSamples(tmpDir: string, language: string): Promise<[string, string]> {
389
+ const graphic = this.handler.handler === 'graphic'
390
+ const samples1c: string[] = []
391
+ const samples2c: string[] = []
392
+ let index = 1
393
+ for (const testcase of this.testcases) {
394
+ if (testcase.startsWith('sample')) {
395
+ let size = ''
396
+ if (graphic) {
397
+ await cp(join(this.directory, `${testcase}.cor`), join(tmpDir, `${testcase}.cor.png`))
398
+ const dimensions = await imageSizeFromFile(join(tmpDir, `${testcase}.cor.png`))
399
+ size = `(${dimensions.width}$\\times$${dimensions.height})`
400
+ }
401
+ samples1c.push(`\n\\SampleOneColInputOutput[${size}]{${testcase}}{${index}}\n`)
402
+ samples2c.push(`\n\\SampleTwoColInputOutput[${size}]{${testcase}}{${index}}\n`)
403
+ index++
404
+ }
405
+ }
406
+ return [samples1c.join('\n'), samples2c.join('\n')]
407
+ }
408
+
409
+ async makePdf(tmpDirBase: string, language: string) {
410
+ const tmpDir = join(tmpDirBase, language)
411
+ await mkdir(tmpDir)
412
+
413
+ const date = new Date().toISOString().substring(0, 10)
414
+ const year = new Date().getFullYear()
415
+ const author = this.problemYmls[language].author || 'Unknown'
416
+ const authorEmail = this.problemYmls[language].author_email || 'unknown email'
417
+ const translator = this.problemYmls[language].translator || ''
418
+
419
+ const [samples1c, samples2c] = await this.makeSamples(tmpDir, language)
420
+
421
+ const root = `
422
+
423
+ \\documentclass[11pt]{article}
424
+
425
+ \\usepackage{judgeit}
426
+ \\usepackage{judgeit.${language}}
427
+
428
+ % TODO?
429
+ \\lstMakeShortInline@
430
+
431
+ \\begin{document}
432
+
433
+ \\providecommand{\\SampleOneCol}{${samples1c}}
434
+ \\providecommand{\\SampleTwoCol}{${samples2c}}
435
+ \\ProblemId{{DRAFT ${language}}}
436
+ \\DoProblem{${language}}
437
+
438
+ \\ProblemInformation
439
+ \\Author: ${author}\\\\ ${translator ? `(${language}: ${translator})` : ''}
440
+ \\Generation: ${date}
441
+
442
+ \\subsection*{Draft}
443
+ Draft generated with \\textbf{new-jutge-toolkit}.
444
+
445
+ \\end{document}
446
+
447
+ `
448
+
449
+ // copy files to tmpDir
450
+ await cp(this.directory, tmpDir, { recursive: true })
451
+
452
+ // write root.tex
453
+ await writeText(join(tmpDir, 'root.tex'), root)
454
+
455
+ // tweak the tex file
456
+ const tex1 = await readText(join(tmpDir, `problem.${language}.tex`))
457
+ const tex2 = tex1
458
+ .replace(/\\begin{htmlonly}[\s\S]*?\\end{htmlonly}/g, '')
459
+ .replace(/\\begin{latexonly}/g, '')
460
+ .replace(/\\end{latexonly}/g, '')
461
+ .replace(/\.eps}/g, '}')
462
+ await writeText(join(tmpDir, `problem.${language}.tex`), tex2)
463
+
464
+ // copy style files
465
+ await this.copyStyleFiles(tmpDir, language)
466
+
467
+ // latex
468
+ try {
469
+ tui.command('xelatex -interaction=nonstopmode -file-line-error root.tex')
470
+ await execa({
471
+ stderr: 'inherit',
472
+ // stdout: 'inherit',
473
+ cwd: tmpDir,
474
+ })`xelatex -interaction=nonstopmode -file-line-error root.tex`
475
+ await cp(join(tmpDir, 'root.pdf'), join(this.directory, `problem.${language}.pdf`))
476
+ tui.success(
477
+ `Generated ${tui.hyperlink(this.directory, `problem.${language}.pdf`)} see ${tui.hyperlink(tmpDir, `root.log`)}`,
478
+ )
479
+ } catch (e) {
480
+ tui.error(`Error in LaTeX: ${tui.hyperlink(tmpDir, `root.log`)}`)
481
+ }
482
+ }
483
+
484
+ async copyStyleFiles(tmpDir: string, language: string) {
485
+ const cpSty = async (path: string) => {
486
+ const source = join(projectDir(), 'assets', 'sty', path)
487
+ await cp(source, join(tmpDir, path))
488
+ }
489
+ await cpSty('picins.sty')
490
+ await cpSty('judgeit.sty')
491
+ if (language === 'ca') await cpSty('judgeit.ca.sty')
492
+ if (language === 'es') await cpSty('judgeit.es.sty')
493
+ if (language === 'en') await cpSty('judgeit.en.sty')
494
+ if (language === 'fr') await cpSty('judgeit.fr.sty')
495
+ if (language === 'de') await cpSty('judgeit.de.sty')
496
+ }
497
+
498
+ public async makeTexts() {
499
+ await tui.section('Making text statements', async () => {
500
+ const tmpDirBase = await mkdtemp(join(tmpdir(), `jutge-toolkit-text-`))
501
+ await tui.section('Creating working directory', async () => {
502
+ await nothing()
503
+ tui.directory(tmpDirBase)
504
+ })
505
+ try {
506
+ for (const language of this.languages) {
507
+ if (this.structure === 'multi' || (this.structure === 'single' && language === this.language)) {
508
+ await tui.section(`Making text statements for ${languageNames[language]}`, async () => {
509
+ await this.makeText(tmpDirBase, language)
510
+ })
511
+ }
512
+ }
513
+ } finally {
514
+ // do not clean up because of hyperlinks
515
+ }
516
+ })
517
+ }
518
+
519
+ async makeText(tmpDirBase: string, language: string) {
520
+ const tmpDir = join(tmpDirBase, language)
521
+ await mkdir(tmpDir)
522
+
523
+ const date = new Date().toISOString().substring(0, 10)
524
+ const year = new Date().getFullYear()
525
+ const author = this.problemYmls[language].author || 'Unknown'
526
+ const authorEmail = this.problemYmls[language].author_email || 'unknown email'
527
+ const translator = this.problemYmls[language].translator || ''
528
+
529
+ const root = `
530
+
531
+ \\documentclass[11pt]{article}
532
+
533
+ \\usepackage{judgeit}
534
+ \\usepackage{judgeit.${language}}
535
+
536
+ % TODO?
537
+ \\lstMakeShortInline@
538
+
539
+ % redefine commands to simplify text output
540
+ \\renewcommand{\\Sample}{}
541
+ \\renewcommand{\\Statement}{}
542
+ \\renewcommand{\\Problem}[1]{\\section{#1}}
543
+ \\renewcommand{\\ProblemId}[1]{DRAFT ${language}}
544
+
545
+ % redefine figure commands to avoid troubles with \\parpic
546
+ \\renewcommand{\\FigureL}[2]{\\includegraphics[#1]{#2}}
547
+ \\renewcommand{\\FigureC}[2]{\\includegraphics[#1]{#2}}
548
+ \\renewcommand{\\FigureR}[2]{\\includegraphics[#1]{#2}}
549
+
550
+ \\begin{document}
551
+
552
+ \\DoProblem{${language}}
553
+
554
+ \\subsection*{\\TxtAuthor}
555
+ ${author} ${translator ? `(${language}: ${translator})` : ''}
556
+
557
+ \\subsection*{Draft}
558
+ Draft generated with \\textbf{new-jutge-toolkit}.
559
+
560
+ \\end{document}
561
+
562
+ `
563
+
564
+ // copy files to tmpDir
565
+ await cp(this.directory, tmpDir, { recursive: true })
566
+
567
+ // write root.tex
568
+ await writeText(join(tmpDir, 'root.tex'), root)
569
+
570
+ // tweak the tex file
571
+ const tex1 = await readText(join(tmpDir, `problem.${language}.tex`))
572
+ const tex2 = tex1
573
+ .replace(/\\begin{latexonly}[\s\S]*?\\end{latexonly}/g, '')
574
+ .replace(/\\begin{htmlonly}/g, '')
575
+ .replace(/\\end{htmlonly}/g, '')
576
+ .replace(/\\begin{minipage}/g, '')
577
+ .replace(/\\end{minipage}/g, '')
578
+ .replace(/\.eps}/g, '}')
579
+ await writeText(join(tmpDir, `problem.${language}.tex`), tex2)
580
+
581
+ // copy style files
582
+ await this.copyStyleFiles(tmpDir, language)
583
+
584
+ // convert .eps files to png
585
+ const files = (await Array.fromAsync(glob('*.eps', { cwd: tmpDir }))).sort()
586
+ for (const file of files) {
587
+ tui.command(`convert ${file} ${file.replace(/\.eps$/, '.png')}`)
588
+ await execa({ cwd: tmpDir })`convert ${file} ${file.replace(/\.eps$/, '.png')}`
589
+ }
590
+
591
+ // create lua files
592
+ const luaSource = join(projectDir(), 'assets', 'lua', 'fixCodeBlocks.lua')
593
+ await cp(luaSource, join(tmpDir, 'fixCodeBlocks.lua'))
594
+
595
+ // txt
596
+ try {
597
+ tui.command('pandoc --quiet root.tex --to plain --output root.txt')
598
+ await execa({ cwd: tmpDir })`pandoc --quiet root.tex --to plain --output root.txt`
599
+ await cp(join(tmpDir, 'root.txt'), join(this.directory, `problem.${language}.txt`))
600
+ tui.success('Generated ' + tui.hyperlink(this.directory, `problem.${language}.txt`))
601
+ } catch (e) {
602
+ console.error('pandoc error', e)
603
+ }
604
+
605
+ // md
606
+ try {
607
+ tui.command(
608
+ 'pandoc --quiet root.tex --to markdown --to markdown-header_attributes --lua-filter=fixCodeBlocks.lua --output root.md',
609
+ )
610
+ await execa({
611
+ cwd: tmpDir,
612
+ })`pandoc --quiet root.tex --to markdown --to markdown-header_attributes --lua-filter=fixCodeBlocks.lua --output root.md`
613
+ await cp(join(tmpDir, 'root.md'), join(this.directory, `problem.${language}.md`))
614
+ tui.success('Generated ' + tui.hyperlink(this.directory, `problem.${language}.md`))
615
+ } catch (e) {
616
+ console.error('pandoc error', e)
617
+ }
618
+
619
+ // md
620
+ try {
621
+ tui.command('pandoc --quiet root.tex --to html --mathml --embed-resources --output root.html')
622
+ await execa({
623
+ cwd: tmpDir,
624
+ })`pandoc --quiet root.tex --to html --mathml --embed-resources --output root.html`
625
+ await cp(join(tmpDir, 'root.html'), join(this.directory, `problem.${language}.html`))
626
+ tui.success('Generated ' + tui.hyperlink(this.directory, `problem.${language}.html`))
627
+ } catch (e) {
628
+ console.error('pandoc error', e)
629
+ }
630
+ }
631
+
632
+ public async verifyCandidate(program: string) {
633
+ await tui.section(`Verifying ${program}`, async () => {
634
+ const extension = program.split('.').pop()!
635
+ const compiler = getCompilerByExtension(extension)
636
+
637
+ await tui.section(
638
+ `Using compiler ${compiler.name()} to compile ${tui.hyperlink(this.directory, program)}`,
639
+ async () => {
640
+ try {
641
+ await compiler.compile(this.handler, this.directory, program)
642
+ } catch (error) {
643
+ throw new Error(`Compilation failed`)
644
+ }
645
+ },
646
+ )
647
+
648
+ const results: ExecutionResult[] = []
649
+ await tui.section('Executing testcases', async () => {
650
+ for (const testcase of this.testcases) {
651
+ results.push(
652
+ await this.runTestcase(testcase, `${testcase}.inp`, `${testcase}.${extension}.out`, compiler),
653
+ )
654
+ }
655
+
656
+ console.log()
657
+ console.log(`testcase time status`)
658
+ let errors = 0
659
+ for (const result of results) {
660
+ const status = result.error
661
+ ? 'EE'
662
+ : (await filesAreEqual(
663
+ `${this.directory}/${result.testcase}.cor`,
664
+ `${this.directory}/${result.testcase}.${extension}.out`,
665
+ ))
666
+ ? 'OK'
667
+ : 'WA'
668
+ const time = prettyMs(result.time)
669
+ console.log(
670
+ (status !== 'OK' ? chalk.red : chalk.green)(
671
+ `${result.testcase.padEnd(12)} ${time.padStart(10)} ${status.padStart(10)}`,
672
+ ),
673
+ )
674
+ if (status !== 'OK') errors++
675
+ }
676
+ console.log()
677
+
678
+ if (errors) {
679
+ tui.error(`${errors} errors found for ${program}`)
680
+ } else {
681
+ tui.success(`All testcases passed successfully for ${program}`)
682
+ }
683
+ })
684
+ })
685
+ }
686
+ }