@jpetit/toolkit 3.1.1 → 3.1.2
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/ai.ts +200 -0
- package/lib/cleaner.ts +77 -0
- package/lib/compilers/base.ts +159 -0
- package/lib/compilers/clojure.ts +87 -0
- package/lib/compilers/gcc.ts +39 -0
- package/lib/compilers/ghc.ts +39 -0
- package/lib/compilers/gxx.ts +39 -0
- package/lib/compilers/index.ts +81 -0
- package/lib/compilers/java.ts +105 -0
- package/lib/compilers/python3.ts +112 -0
- package/lib/compilers/run-clojure.ts +101 -0
- package/lib/compilers/run-haskell.ts +117 -0
- package/lib/compilers/run-python.ts +103 -0
- package/lib/compilers/rust.ts +39 -0
- package/lib/create-with-jutgeai.ts +407 -0
- package/lib/create-with-template.ts +55 -0
- package/lib/data.ts +25 -0
- package/lib/doctor.ts +238 -0
- package/lib/generate.ts +171 -0
- package/lib/helpers.ts +48 -0
- package/lib/inspector.ts +253 -0
- package/lib/jutge_api_client.ts +4631 -0
- package/lib/maker.ts +613 -0
- package/lib/settings.ts +51 -0
- package/lib/tui.ts +152 -0
- package/lib/types.ts +55 -0
- package/lib/upload.ts +216 -0
- package/lib/utils.ts +201 -0
- package/lib/versions.ts +46 -0
- package/package.json +4 -2
- package/toolkit/about.ts +43 -0
- package/toolkit/ai.ts +56 -0
- package/toolkit/check.ts +16 -0
- package/toolkit/clean.ts +27 -0
- package/toolkit/compilers.ts +29 -0
- package/toolkit/config.ts +91 -0
- package/toolkit/create.ts +37 -0
- package/toolkit/doctor.ts +22 -0
- package/toolkit/generate.ts +213 -0
- package/toolkit/make.ts +82 -0
- package/toolkit/upgrade.ts +9 -0
- package/toolkit/upload.ts +19 -0
package/lib/maker.ts
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { execa } from 'execa'
|
|
3
|
+
import { cp, exists, glob, mkdir, rename } from 'fs/promises'
|
|
4
|
+
import { imageSizeFromFile } from 'image-size/fromFile'
|
|
5
|
+
import { basename, dirname, join, normalize, resolve } from 'path'
|
|
6
|
+
import prettyBytes from 'pretty-bytes'
|
|
7
|
+
import prettyMs from 'pretty-ms'
|
|
8
|
+
import tui from '../lib/tui'
|
|
9
|
+
import {
|
|
10
|
+
filesAreEqual,
|
|
11
|
+
fileSize,
|
|
12
|
+
humanid,
|
|
13
|
+
isDirectory,
|
|
14
|
+
isDirectoryInDir,
|
|
15
|
+
nothing,
|
|
16
|
+
projectDir,
|
|
17
|
+
readText,
|
|
18
|
+
toolkitPrefix,
|
|
19
|
+
writeText,
|
|
20
|
+
} from '../lib/utils'
|
|
21
|
+
import { getCompilerByExtension, getCompilerById } from './compilers'
|
|
22
|
+
import type { Compiler } from './compilers/base'
|
|
23
|
+
import { languageNames } from './data'
|
|
24
|
+
import { newInspector, Inspector } from './inspector'
|
|
25
|
+
|
|
26
|
+
export async function newMaker(directory: string): Promise<Maker> {
|
|
27
|
+
const inspector = await newInspector(directory)
|
|
28
|
+
return new Maker(inspector)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type ExecutionResult = {
|
|
32
|
+
testcase: string
|
|
33
|
+
time: number
|
|
34
|
+
inputSize: number
|
|
35
|
+
outputSize: number
|
|
36
|
+
error: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class Maker {
|
|
40
|
+
inspector: Inspector
|
|
41
|
+
|
|
42
|
+
public constructor(inspector: Inspector) {
|
|
43
|
+
this.inspector = inspector
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async showDirectory() {
|
|
47
|
+
await tui.section('Directory', async () => {
|
|
48
|
+
await nothing()
|
|
49
|
+
tui.directory(resolve(this.inspector.directory))
|
|
50
|
+
const fullPath = normalize(resolve(this.inspector.directory))
|
|
51
|
+
if (!fullPath.endsWith('.pbm')) {
|
|
52
|
+
const lastPath = basename(fullPath)
|
|
53
|
+
const butLastPath = dirname(fullPath)
|
|
54
|
+
|
|
55
|
+
if (!Object.keys(languageNames).includes(lastPath) || !butLastPath.endsWith('.pbm')) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
'The problem directory should end with .pbm or be a language id inside a .pbm directory',
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public async makeProblem() {
|
|
65
|
+
await this.makeGoldenExecutable()
|
|
66
|
+
await this.makeCorrects()
|
|
67
|
+
await this.checkSolutions()
|
|
68
|
+
await this.makePdfs()
|
|
69
|
+
await this.makeTexts()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public async makeExecutable(program: string) {
|
|
73
|
+
const compiler = this.selectCompiler()
|
|
74
|
+
const newProgram = `${toolkitPrefix()}-${program}`
|
|
75
|
+
await tui.section(
|
|
76
|
+
`Copying ${tui.hyperlink(this.inspector.directory, program)} to ${tui.hyperlink(this.inspector.directory, newProgram)}`,
|
|
77
|
+
async () => {
|
|
78
|
+
await cp(join(this.inspector.directory, program), join(this.inspector.directory, newProgram))
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
await tui.section(
|
|
82
|
+
`Compiling ${tui.hyperlink(this.inspector.directory, newProgram)} with ${compiler.name()} (${this.inspector.handler.source_modifier})`,
|
|
83
|
+
async () => {
|
|
84
|
+
try {
|
|
85
|
+
let outputPath: string
|
|
86
|
+
if (this.inspector.handler.source_modifier === 'none') {
|
|
87
|
+
outputPath = await compiler.compileNormal(
|
|
88
|
+
this.inspector.handler,
|
|
89
|
+
this.inspector.directory,
|
|
90
|
+
newProgram,
|
|
91
|
+
)
|
|
92
|
+
} else if (this.inspector.handler.source_modifier === 'no_main') {
|
|
93
|
+
outputPath = await compiler.compileWithMain(
|
|
94
|
+
this.inspector.handler,
|
|
95
|
+
this.inspector.directory,
|
|
96
|
+
newProgram,
|
|
97
|
+
)
|
|
98
|
+
} else {
|
|
99
|
+
throw new Error(`Unknown source modifier: ${this.inspector.handler.source_modifier as string}`)
|
|
100
|
+
}
|
|
101
|
+
if (!(await exists(join(this.inspector.directory, outputPath)))) {
|
|
102
|
+
throw new Error(`Compilation failed for ${newProgram}`)
|
|
103
|
+
}
|
|
104
|
+
tui.success(`Compiled ${newProgram} to ${outputPath}`)
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw new Error(`Compilation failed`)
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public async makeExecutables() {
|
|
113
|
+
await tui.section('Compiling solutions', async () => {
|
|
114
|
+
for (const solution of this.inspector.solutions) {
|
|
115
|
+
await this.makeExecutable(solution)
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public async makeGoldenExecutable() {
|
|
121
|
+
await tui.section(
|
|
122
|
+
`Compiling golden solution from ${tui.hyperlink(this.inspector.directory, this.inspector.goldenSolution!)}`,
|
|
123
|
+
async () => {
|
|
124
|
+
if (!this.inspector.goldenSolution) {
|
|
125
|
+
throw new Error('Golden solution not set')
|
|
126
|
+
}
|
|
127
|
+
await this.makeExecutable(this.inspector.goldenSolution)
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public async checkSolutions() {
|
|
133
|
+
for (const solution of this.inspector.solutions) {
|
|
134
|
+
if (solution !== this.inspector.goldenSolution) {
|
|
135
|
+
await this.checkCandidate(solution)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async makeCorrect(testcase: string, compiler: Compiler, sourcePath: string): Promise<ExecutionResult> {
|
|
141
|
+
return await this.runTestcase(
|
|
142
|
+
testcase,
|
|
143
|
+
`${testcase}.inp`,
|
|
144
|
+
`${testcase}.cor`,
|
|
145
|
+
compiler,
|
|
146
|
+
toolkitPrefix() + '-' + sourcePath,
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async runTestcase(
|
|
151
|
+
testcase: string,
|
|
152
|
+
input: string,
|
|
153
|
+
output: string,
|
|
154
|
+
compiler: Compiler,
|
|
155
|
+
sourcePath: string,
|
|
156
|
+
): Promise<ExecutionResult> {
|
|
157
|
+
let error = false
|
|
158
|
+
const start = Date.now()
|
|
159
|
+
try {
|
|
160
|
+
await compiler.execute(this.inspector.handler, this.inspector.directory, sourcePath, input, output)
|
|
161
|
+
} catch (e) {
|
|
162
|
+
tui.error(`Execution failed for testcase '${testcase}'`)
|
|
163
|
+
error = true
|
|
164
|
+
}
|
|
165
|
+
const end = Date.now()
|
|
166
|
+
const time = end - start
|
|
167
|
+
|
|
168
|
+
if (this.inspector.handler.handler === 'graphic') {
|
|
169
|
+
await rename(join(this.inspector.directory, 'output.png'), join(this.inspector.directory, output))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const inputSize = await fileSize(join(this.inspector.directory, input))
|
|
173
|
+
const outputSize = await fileSize(join(this.inspector.directory, output))
|
|
174
|
+
|
|
175
|
+
return { testcase, error, time, inputSize, outputSize }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
selectCompiler(): Compiler {
|
|
179
|
+
if (this.inspector.handler.compilers === 'RunPython') {
|
|
180
|
+
return getCompilerById('RunPython')
|
|
181
|
+
} else if (this.inspector.handler.compilers === 'RunHaskell') {
|
|
182
|
+
return getCompilerById('RunHaskell')
|
|
183
|
+
} else if (this.inspector.handler.compilers === 'RunClojure') {
|
|
184
|
+
return getCompilerById('RunClojure')
|
|
185
|
+
} else {
|
|
186
|
+
const extension = this.inspector.goldenSolution!.split('.').pop()!
|
|
187
|
+
return getCompilerByExtension(extension)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
public async makeCorrects() {
|
|
192
|
+
const compiler = this.selectCompiler()
|
|
193
|
+
await tui.section(`Making correct outputs with golden solution`, async () => {
|
|
194
|
+
await tui.section(`Executing testcases using ${compiler.name()}`, async () => {
|
|
195
|
+
const results: ExecutionResult[] = []
|
|
196
|
+
|
|
197
|
+
for (const testcase of this.inspector.testcases) {
|
|
198
|
+
results.push(await this.makeCorrect(testcase, compiler, this.inspector.goldenSolution!))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log()
|
|
202
|
+
console.log(`testcase time input output`)
|
|
203
|
+
for (const result of results) {
|
|
204
|
+
const time = prettyMs(result.time)
|
|
205
|
+
const inputSize = prettyBytes(result.inputSize).replace(' ', '')
|
|
206
|
+
const outputSize = prettyBytes(result.outputSize).replace(' ', '')
|
|
207
|
+
console.log(
|
|
208
|
+
(result.error ? chalk.red : chalk.green)(
|
|
209
|
+
`${result.testcase.padEnd(12)} ${time.padStart(10)} ${inputSize.padStart(10)} ${outputSize.padStart(
|
|
210
|
+
10,
|
|
211
|
+
)}`,
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const errors = results.filter((result) => result.error).length
|
|
217
|
+
if (errors > 0) {
|
|
218
|
+
console.log()
|
|
219
|
+
throw new Error(`${errors} errors occurred while making correct answers`)
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
public async makePdfs() {
|
|
226
|
+
await tui.section('Making PDF statements', async () => {
|
|
227
|
+
const tmpDirBase = join(this.inspector.directory, toolkitPrefix() + '-pdf', humanid())
|
|
228
|
+
await mkdir(tmpDirBase, { recursive: true })
|
|
229
|
+
await tui.section('Creating working directory', async () => {
|
|
230
|
+
await nothing()
|
|
231
|
+
tui.directory(tmpDirBase)
|
|
232
|
+
})
|
|
233
|
+
for (const language of this.inspector.languages) {
|
|
234
|
+
if (
|
|
235
|
+
this.inspector.structure === 'multi' ||
|
|
236
|
+
(this.inspector.structure === 'single' && language === this.inspector.language)
|
|
237
|
+
) {
|
|
238
|
+
await tui.section(`Making PDF statement for ${languageNames[language]}`, async () => {
|
|
239
|
+
await this.makePdf(tmpDirBase, language)
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async makeSamples(tmpDir: string, language: string): Promise<[string, string]> {
|
|
247
|
+
const graphic = this.inspector.handler.handler === 'graphic'
|
|
248
|
+
const samples1col: string[] = []
|
|
249
|
+
const samples2col: string[] = []
|
|
250
|
+
let index = 1
|
|
251
|
+
for (const testcase of this.inspector.testcases) {
|
|
252
|
+
if (testcase.startsWith('sample')) {
|
|
253
|
+
let size = ''
|
|
254
|
+
if (graphic) {
|
|
255
|
+
await cp(join(this.inspector.directory, `${testcase}.cor`), join(tmpDir, `${testcase}.cor.png`))
|
|
256
|
+
const dimensions = await imageSizeFromFile(join(tmpDir, `${testcase}.cor.png`))
|
|
257
|
+
size = `(${dimensions.width}$\\times$${dimensions.height})`
|
|
258
|
+
}
|
|
259
|
+
samples1col.push(`\n\\SampleOneColInputOutput[${size}]{${testcase}}{${index}}\n`)
|
|
260
|
+
samples2col.push(`\n\\SampleTwoColInputOutput[${size}]{${testcase}}{${index}}\n`)
|
|
261
|
+
index++
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return [samples1col.join('\n'), samples2col.join('\n')]
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async makePdf(tmpDirBase: string, language: string) {
|
|
268
|
+
const tmpDir = join(tmpDirBase, language)
|
|
269
|
+
await mkdir(tmpDir)
|
|
270
|
+
|
|
271
|
+
const date = new Date().toISOString().substring(0, 10)
|
|
272
|
+
const year = new Date().getFullYear()
|
|
273
|
+
const author = this.inspector.problemLangYmls[language].author || 'Unknown'
|
|
274
|
+
const authorEmail = this.inspector.problemLangYmls[language].author_email || 'unknown email'
|
|
275
|
+
const translator = this.inspector.problemLangYmls[language].translator || ''
|
|
276
|
+
|
|
277
|
+
const [samples1c, samples2c] = await this.makeSamples(tmpDir, language)
|
|
278
|
+
|
|
279
|
+
const root = `
|
|
280
|
+
|
|
281
|
+
\\documentclass[11pt]{article}
|
|
282
|
+
|
|
283
|
+
\\usepackage{judgeit}
|
|
284
|
+
\\usepackage{judgeit.${language}}
|
|
285
|
+
|
|
286
|
+
% TODO?
|
|
287
|
+
\\lstMakeShortInline@
|
|
288
|
+
|
|
289
|
+
\\begin{document}
|
|
290
|
+
|
|
291
|
+
\\providecommand{\\SampleOneCol}{${samples1c}}
|
|
292
|
+
\\providecommand{\\SampleTwoCol}{${samples2c}}
|
|
293
|
+
\\ProblemId{{DRAFT ${language}}}
|
|
294
|
+
\\DoProblem{${language}}
|
|
295
|
+
|
|
296
|
+
\\ProblemInformation
|
|
297
|
+
\\Author: ${author}\\\\ ${translator ? `(${language}: ${translator})` : ''}
|
|
298
|
+
\\Generation: ${date}
|
|
299
|
+
|
|
300
|
+
\\subsection*{Draft}
|
|
301
|
+
Draft generated with \\textbf{new-jutge-toolkit}.
|
|
302
|
+
|
|
303
|
+
\\end{document}
|
|
304
|
+
|
|
305
|
+
`
|
|
306
|
+
|
|
307
|
+
// copy files to tmpDir
|
|
308
|
+
// TODO: only copy needed files
|
|
309
|
+
for await (const entry of glob('*', { cwd: this.inspector.directory })) {
|
|
310
|
+
if (
|
|
311
|
+
entry.startsWith(toolkitPrefix()) ||
|
|
312
|
+
entry.endsWith('.exe') ||
|
|
313
|
+
entry.endsWith('.html') ||
|
|
314
|
+
entry.endsWith('.md') ||
|
|
315
|
+
entry.endsWith('.txt') ||
|
|
316
|
+
(await isDirectoryInDir(this.inspector.directory, entry))
|
|
317
|
+
) {
|
|
318
|
+
continue
|
|
319
|
+
}
|
|
320
|
+
await cp(join(this.inspector.directory, entry), join(tmpDir, entry))
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// write root.tex
|
|
324
|
+
await writeText(join(tmpDir, 'root.tex'), root)
|
|
325
|
+
|
|
326
|
+
// tweak the tex file
|
|
327
|
+
const tex1 = await readText(join(tmpDir, `problem.${language}.tex`))
|
|
328
|
+
const tex2 = tex1
|
|
329
|
+
.replace(/\\begin{htmlonly}[\s\S]*?\\end{htmlonly}/g, '')
|
|
330
|
+
.replace(/\\begin{latexonly}/g, '')
|
|
331
|
+
.replace(/\\end{latexonly}/g, '')
|
|
332
|
+
.replace(/\.eps}/g, '}')
|
|
333
|
+
await writeText(join(tmpDir, `problem.${language}.tex`), tex2)
|
|
334
|
+
|
|
335
|
+
// copy style files
|
|
336
|
+
await this.copyStyleFiles(tmpDir, language)
|
|
337
|
+
|
|
338
|
+
// latex
|
|
339
|
+
try {
|
|
340
|
+
tui.command('xelatex -interaction=nonstopmode -file-line-error root.tex')
|
|
341
|
+
await execa({
|
|
342
|
+
stderr: 'inherit',
|
|
343
|
+
// stdout: 'inherit',
|
|
344
|
+
cwd: tmpDir,
|
|
345
|
+
})`xelatex -interaction=nonstopmode -file-line-error root.tex`
|
|
346
|
+
await cp(join(tmpDir, 'root.pdf'), join(this.inspector.directory, `problem.${language}.pdf`))
|
|
347
|
+
tui.success(
|
|
348
|
+
`Generated ${tui.hyperlink(this.inspector.directory, `problem.${language}.pdf`)} see ${tui.hyperlink(tmpDir, `root.log`)}`,
|
|
349
|
+
)
|
|
350
|
+
} catch (e) {
|
|
351
|
+
tui.error(`Error in LaTeX: ${tui.hyperlink(tmpDir, `root.log`)}`)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async copyStyleFiles(tmpDir: string, language: string) {
|
|
356
|
+
const cpSty = async (path: string) => {
|
|
357
|
+
const source = join(projectDir(), 'assets', 'sty', path)
|
|
358
|
+
await cp(source, join(tmpDir, path))
|
|
359
|
+
}
|
|
360
|
+
await cpSty('picins.sty')
|
|
361
|
+
await cpSty('judgeit.sty')
|
|
362
|
+
if (language === 'ca') await cpSty('judgeit.ca.sty')
|
|
363
|
+
if (language === 'es') await cpSty('judgeit.es.sty')
|
|
364
|
+
if (language === 'en') await cpSty('judgeit.en.sty')
|
|
365
|
+
if (language === 'fr') await cpSty('judgeit.fr.sty')
|
|
366
|
+
if (language === 'de') await cpSty('judgeit.de.sty')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
public async makeTexts() {
|
|
370
|
+
await tui.section('Making text statements (.{md,txt,html})', async () => {
|
|
371
|
+
const tmpDirBase = join(this.inspector.directory, toolkitPrefix() + '-text', humanid())
|
|
372
|
+
await mkdir(tmpDirBase, { recursive: true })
|
|
373
|
+
await tui.section('Creating working directory', async () => {
|
|
374
|
+
await nothing()
|
|
375
|
+
tui.directory(tmpDirBase)
|
|
376
|
+
})
|
|
377
|
+
try {
|
|
378
|
+
for (const language of this.inspector.languages) {
|
|
379
|
+
if (
|
|
380
|
+
this.inspector.structure === 'multi' ||
|
|
381
|
+
(this.inspector.structure === 'single' && language === this.inspector.language)
|
|
382
|
+
) {
|
|
383
|
+
await tui.section(`Making text statements for ${languageNames[language]}`, async () => {
|
|
384
|
+
await this.makeText(tmpDirBase, language)
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
} finally {
|
|
389
|
+
// do not clean up because of hyperlinks
|
|
390
|
+
}
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async makeText(tmpDirBase: string, language: string) {
|
|
395
|
+
const tmpDir = join(tmpDirBase, language)
|
|
396
|
+
await mkdir(tmpDir)
|
|
397
|
+
|
|
398
|
+
const date = new Date().toISOString().substring(0, 10)
|
|
399
|
+
const year = new Date().getFullYear()
|
|
400
|
+
const author = this.inspector.problemLangYmls[language].author || 'Unknown'
|
|
401
|
+
const authorEmail = this.inspector.problemLangYmls[language].author_email || 'unknown email'
|
|
402
|
+
const translator = this.inspector.problemLangYmls[language].translator || ''
|
|
403
|
+
|
|
404
|
+
const root = `
|
|
405
|
+
|
|
406
|
+
\\documentclass[11pt]{article}
|
|
407
|
+
|
|
408
|
+
\\usepackage{judgeit}
|
|
409
|
+
\\usepackage{judgeit.${language}}
|
|
410
|
+
|
|
411
|
+
% TODO?
|
|
412
|
+
\\lstMakeShortInline@
|
|
413
|
+
|
|
414
|
+
% redefine commands to simplify text output
|
|
415
|
+
\\renewcommand{\\Sample}{}
|
|
416
|
+
\\renewcommand{\\Statement}{}
|
|
417
|
+
\\renewcommand{\\Problem}[1]{\\section{#1}}
|
|
418
|
+
\\renewcommand{\\ProblemId}[1]{DRAFT ${language}}
|
|
419
|
+
|
|
420
|
+
% redefine figure commands to avoid troubles with \\parpic
|
|
421
|
+
\\renewcommand{\\FigureL}[2]{\\includegraphics[#1]{#2}}
|
|
422
|
+
\\renewcommand{\\FigureC}[2]{\\includegraphics[#1]{#2}}
|
|
423
|
+
\\renewcommand{\\FigureR}[2]{\\includegraphics[#1]{#2}}
|
|
424
|
+
|
|
425
|
+
\\begin{document}
|
|
426
|
+
|
|
427
|
+
\\DoProblem{${language}}
|
|
428
|
+
|
|
429
|
+
\\subsection*{\\TxtAuthor}
|
|
430
|
+
${author} ${translator ? `(${language}: ${translator})` : ''}
|
|
431
|
+
|
|
432
|
+
\\subsection*{Draft}
|
|
433
|
+
Draft generated with \\textbf{new-jutge-toolkit}.
|
|
434
|
+
|
|
435
|
+
\\end{document}
|
|
436
|
+
|
|
437
|
+
`
|
|
438
|
+
|
|
439
|
+
// copy files to tmpDir
|
|
440
|
+
// TODO: only copy needed files
|
|
441
|
+
for await (const entry of glob('*', { cwd: this.inspector.directory })) {
|
|
442
|
+
if (
|
|
443
|
+
entry.startsWith(toolkitPrefix()) ||
|
|
444
|
+
entry.endsWith('.pdf') ||
|
|
445
|
+
entry.endsWith('.exe') ||
|
|
446
|
+
entry.endsWith('.html') ||
|
|
447
|
+
entry.endsWith('.md') ||
|
|
448
|
+
entry.endsWith('.txt') ||
|
|
449
|
+
(await isDirectoryInDir(this.inspector.directory, entry))
|
|
450
|
+
) {
|
|
451
|
+
continue
|
|
452
|
+
}
|
|
453
|
+
await cp(join(this.inspector.directory, entry), join(tmpDir, entry))
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// write root.tex
|
|
457
|
+
await writeText(join(tmpDir, 'root.tex'), root)
|
|
458
|
+
|
|
459
|
+
// tweak the tex file
|
|
460
|
+
const tex1 = await readText(join(tmpDir, `problem.${language}.tex`))
|
|
461
|
+
const tex2 = tex1
|
|
462
|
+
.replace(/\\begin{latexonly}[\s\S]*?\\end{latexonly}/g, '')
|
|
463
|
+
.replace(/\\begin{htmlonly}/g, '')
|
|
464
|
+
.replace(/\\end{htmlonly}/g, '')
|
|
465
|
+
.replace(/\\begin{minipage}/g, '')
|
|
466
|
+
.replace(/\\end{minipage}/g, '')
|
|
467
|
+
.replace(/\.eps}/g, '}')
|
|
468
|
+
await writeText(join(tmpDir, `problem.${language}.tex`), tex2)
|
|
469
|
+
|
|
470
|
+
// copy style files
|
|
471
|
+
await this.copyStyleFiles(tmpDir, language)
|
|
472
|
+
|
|
473
|
+
// convert .eps files to png
|
|
474
|
+
const files = (await Array.fromAsync(glob('*.eps', { cwd: tmpDir }))).sort()
|
|
475
|
+
for (const file of files) {
|
|
476
|
+
tui.command(`convert ${file} ${file.replace(/\.eps$/, '.png')}`)
|
|
477
|
+
await execa({ cwd: tmpDir })`convert ${file} ${file.replace(/\.eps$/, '.png')}`
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// create lua files
|
|
481
|
+
const luaSource = join(projectDir(), 'assets', 'lua', 'fixCodeBlocks.lua')
|
|
482
|
+
await cp(luaSource, join(tmpDir, 'fixCodeBlocks.lua'))
|
|
483
|
+
|
|
484
|
+
// txt
|
|
485
|
+
try {
|
|
486
|
+
tui.command('pandoc --quiet root.tex --to plain --output root.txt')
|
|
487
|
+
await execa({ cwd: tmpDir })`pandoc --quiet root.tex --to plain --output root.txt`
|
|
488
|
+
await cp(join(tmpDir, 'root.txt'), join(this.inspector.directory, `problem.${language}.txt`))
|
|
489
|
+
tui.success('Generated ' + tui.hyperlink(this.inspector.directory, `problem.${language}.txt`))
|
|
490
|
+
} catch (e) {
|
|
491
|
+
console.error('pandoc error', e)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// md
|
|
495
|
+
try {
|
|
496
|
+
tui.command(
|
|
497
|
+
'pandoc --quiet root.tex --to markdown --to markdown-header_attributes --lua-filter=fixCodeBlocks.lua --output root.md',
|
|
498
|
+
)
|
|
499
|
+
await execa({
|
|
500
|
+
cwd: tmpDir,
|
|
501
|
+
})`pandoc --quiet root.tex --to markdown --to markdown-header_attributes --lua-filter=fixCodeBlocks.lua --output root.md`
|
|
502
|
+
await cp(join(tmpDir, 'root.md'), join(this.inspector.directory, `problem.${language}.md`))
|
|
503
|
+
tui.success('Generated ' + tui.hyperlink(this.inspector.directory, `problem.${language}.md`))
|
|
504
|
+
} catch (e) {
|
|
505
|
+
console.error('pandoc error', e)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// html
|
|
509
|
+
try {
|
|
510
|
+
tui.command('pandoc --quiet root.tex --to html --mathml --embed-resources --output root.html')
|
|
511
|
+
await execa({
|
|
512
|
+
cwd: tmpDir,
|
|
513
|
+
})`pandoc --quiet root.tex --to html --mathml --embed-resources --output root.html`
|
|
514
|
+
await cp(join(tmpDir, 'root.html'), join(this.inspector.directory, `problem.${language}.html`))
|
|
515
|
+
tui.success('Generated ' + tui.hyperlink(this.inspector.directory, `problem.${language}.html`))
|
|
516
|
+
} catch (e) {
|
|
517
|
+
console.error('pandoc error', e)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
public async checkCandidate(program: string) {
|
|
522
|
+
await tui.section(`Checking ${program}`, async () => {
|
|
523
|
+
const extension = program.split('.').pop()!
|
|
524
|
+
const compiler = getCompilerByExtension(extension)
|
|
525
|
+
const newProgram = `${toolkitPrefix()}-${program}`
|
|
526
|
+
|
|
527
|
+
await tui.section(
|
|
528
|
+
`Copying ${tui.hyperlink(this.inspector.directory, program)} to ${tui.hyperlink(this.inspector.directory, newProgram)}`,
|
|
529
|
+
async () => {
|
|
530
|
+
await cp(join(this.inspector.directory, program), join(this.inspector.directory, newProgram))
|
|
531
|
+
},
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
await tui.section(
|
|
535
|
+
`Using compiler ${compiler.name()} to compile ${tui.hyperlink(this.inspector.directory, newProgram)}`,
|
|
536
|
+
async () => {
|
|
537
|
+
try {
|
|
538
|
+
let outputPath: string
|
|
539
|
+
if (this.inspector.handler.source_modifier === 'none') {
|
|
540
|
+
outputPath = await compiler.compileNormal(
|
|
541
|
+
this.inspector.handler,
|
|
542
|
+
this.inspector.directory,
|
|
543
|
+
newProgram,
|
|
544
|
+
)
|
|
545
|
+
} else if (this.inspector.handler.source_modifier === 'no_main') {
|
|
546
|
+
outputPath = await compiler.compileWithMain(
|
|
547
|
+
this.inspector.handler,
|
|
548
|
+
this.inspector.directory,
|
|
549
|
+
newProgram,
|
|
550
|
+
)
|
|
551
|
+
} else {
|
|
552
|
+
throw new Error(
|
|
553
|
+
`Unknown source modifier: ${this.inspector.handler.source_modifier as string}`,
|
|
554
|
+
)
|
|
555
|
+
}
|
|
556
|
+
if (!(await exists(join(this.inspector.directory, outputPath)))) {
|
|
557
|
+
throw new Error(`Compilation failed for ${newProgram}`)
|
|
558
|
+
}
|
|
559
|
+
tui.success(`Compiled ${newProgram} to ${outputPath}`)
|
|
560
|
+
} catch (error) {
|
|
561
|
+
throw new Error(`Compilation failed`)
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
const results: ExecutionResult[] = []
|
|
567
|
+
await tui.section('Executing testcases', async () => {
|
|
568
|
+
for (const testcase of this.inspector.testcases) {
|
|
569
|
+
results.push(
|
|
570
|
+
await this.runTestcase(
|
|
571
|
+
testcase,
|
|
572
|
+
`${testcase}.inp`,
|
|
573
|
+
`${toolkitPrefix()}-${testcase}.${extension}.out`,
|
|
574
|
+
compiler,
|
|
575
|
+
newProgram,
|
|
576
|
+
),
|
|
577
|
+
)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
console.log()
|
|
581
|
+
console.log(`testcase time status`)
|
|
582
|
+
let errors = 0
|
|
583
|
+
for (const result of results) {
|
|
584
|
+
const status = result.error
|
|
585
|
+
? 'EE'
|
|
586
|
+
: (await filesAreEqual(
|
|
587
|
+
join(this.inspector.directory, `${result.testcase}.cor`),
|
|
588
|
+
join(
|
|
589
|
+
this.inspector.directory,
|
|
590
|
+
`${toolkitPrefix()}-${result.testcase}.${extension}.out`,
|
|
591
|
+
),
|
|
592
|
+
))
|
|
593
|
+
? 'OK'
|
|
594
|
+
: 'WA'
|
|
595
|
+
const time = prettyMs(result.time)
|
|
596
|
+
console.log(
|
|
597
|
+
(status !== 'OK' ? chalk.red : chalk.green)(
|
|
598
|
+
`${result.testcase.padEnd(12)} ${time.padStart(10)} ${status.padStart(10)}`,
|
|
599
|
+
),
|
|
600
|
+
)
|
|
601
|
+
if (status !== 'OK') errors++
|
|
602
|
+
}
|
|
603
|
+
console.log()
|
|
604
|
+
|
|
605
|
+
if (errors) {
|
|
606
|
+
tui.error(`${errors} errors found for ${program}`)
|
|
607
|
+
} else {
|
|
608
|
+
tui.success(`All testcases passed successfully for ${program}`)
|
|
609
|
+
}
|
|
610
|
+
})
|
|
611
|
+
})
|
|
612
|
+
}
|
|
613
|
+
}
|
package/lib/settings.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import envPaths from 'env-paths'
|
|
2
|
+
import { exists, mkdir } from 'fs/promises'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { guessUserEmail, guessUserName, readYaml, writeYaml } from './utils'
|
|
5
|
+
import { Settings } from './types'
|
|
6
|
+
|
|
7
|
+
export const paths = envPaths('jutge', { suffix: 'toolkit' })
|
|
8
|
+
|
|
9
|
+
export function configPath() {
|
|
10
|
+
return join(paths.config, 'config.yml')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// console.log(`Configuration path: ${configPath()}`)
|
|
14
|
+
|
|
15
|
+
export async function initializePaths() {
|
|
16
|
+
await mkdir(paths.config, { recursive: true })
|
|
17
|
+
await mkdir(paths.data, { recursive: true })
|
|
18
|
+
await mkdir(paths.cache, { recursive: true })
|
|
19
|
+
await mkdir(paths.log, { recursive: true })
|
|
20
|
+
await mkdir(paths.temp, { recursive: true })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function saveSettings(settings: Settings) {
|
|
24
|
+
await initializePaths()
|
|
25
|
+
settings = Settings.parse(settings)
|
|
26
|
+
return await writeYaml(configPath(), settings)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function loadSettings(): Promise<Settings> {
|
|
30
|
+
const data = await readYaml(configPath())
|
|
31
|
+
return Settings.parse(data)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function loadSettingsAtStart(): Promise<Settings> {
|
|
35
|
+
if (!(await settingsExist())) {
|
|
36
|
+
const defaultSettings = Settings.parse({})
|
|
37
|
+
defaultSettings.name = (await guessUserName()) || 'John Doe'
|
|
38
|
+
defaultSettings.email = (await guessUserEmail()) || 'john.doe@example.com'
|
|
39
|
+
await saveSettings(defaultSettings)
|
|
40
|
+
return defaultSettings
|
|
41
|
+
} else {
|
|
42
|
+
return await loadSettings()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function settingsExist(): Promise<boolean> {
|
|
47
|
+
return await exists(configPath())
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// settings loaded at the start of the program
|
|
51
|
+
export const settings = await loadSettingsAtStart()
|