@jpetit/toolkit 3.0.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.
Files changed (192) hide show
  1. package/.prettierignore +11 -0
  2. package/.prettierrc.json +9 -0
  3. package/.vscode/settings.json +26 -0
  4. package/README.md +22 -0
  5. package/assets/lua/fixCodeBlocks.lua +19 -0
  6. package/assets/lua/removeEnvs.lua +20 -0
  7. package/assets/lua/removeHtmlOnly.lua +11 -0
  8. package/assets/problems/graphics/japanese-flag.pbm/README.md +14 -0
  9. package/assets/problems/graphics/japanese-flag.pbm/award.png +0 -0
  10. package/assets/problems/graphics/japanese-flag.pbm/handler.yml +2 -0
  11. package/assets/problems/graphics/japanese-flag.pbm/problem.ca.tex +21 -0
  12. package/assets/problems/graphics/japanese-flag.pbm/problem.ca.yml +3 -0
  13. package/assets/problems/graphics/japanese-flag.pbm/sample-1.inp +1 -0
  14. package/assets/problems/graphics/japanese-flag.pbm/sample-2.inp +1 -0
  15. package/assets/problems/graphics/japanese-flag.pbm/solution.cc +25 -0
  16. package/assets/problems/graphics/japanese-flag.pbm/solution.py +11 -0
  17. package/assets/problems/graphics/tortuga.pbm/README.md +13 -0
  18. package/assets/problems/graphics/tortuga.pbm/award.png +0 -0
  19. package/assets/problems/graphics/tortuga.pbm/handler.yml +2 -0
  20. package/assets/problems/graphics/tortuga.pbm/problem.ca.tex +23 -0
  21. package/assets/problems/graphics/tortuga.pbm/problem.ca.yml +3 -0
  22. package/assets/problems/graphics/tortuga.pbm/sample.inp +0 -0
  23. package/assets/problems/graphics/tortuga.pbm/solution.py +11 -0
  24. package/assets/problems/standard/campanar-de-la-torrassa.pbm/README.md +15 -0
  25. package/assets/problems/standard/campanar-de-la-torrassa.pbm/award.html +1 -0
  26. package/assets/problems/standard/campanar-de-la-torrassa.pbm/award.png +0 -0
  27. package/assets/problems/standard/campanar-de-la-torrassa.pbm/campanar.eps +1113 -0
  28. package/assets/problems/standard/campanar-de-la-torrassa.pbm/campanar.png +0 -0
  29. package/assets/problems/standard/campanar-de-la-torrassa.pbm/generate.cc +10 -0
  30. package/assets/problems/standard/campanar-de-la-torrassa.pbm/handler.yml +2 -0
  31. package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.ca.tex +59 -0
  32. package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.ca.yml +3 -0
  33. package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.en.tex +52 -0
  34. package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.en.yml +4 -0
  35. package/assets/problems/standard/campanar-de-la-torrassa.pbm/sample.inp +7 -0
  36. package/assets/problems/standard/campanar-de-la-torrassa.pbm/slow.cc +29 -0
  37. package/assets/problems/standard/campanar-de-la-torrassa.pbm/solution.cc +48 -0
  38. package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-1.inp +12 -0
  39. package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-2.inp +100000 -0
  40. package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-2.ops +1 -0
  41. package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-b.inp +0 -0
  42. package/assets/problems/standard/maximum-of-2-integers.pbm/README.md +11 -0
  43. package/assets/problems/standard/maximum-of-2-integers.pbm/handler.yml +1 -0
  44. package/assets/problems/standard/maximum-of-2-integers.pbm/problem.ca.tex +17 -0
  45. package/assets/problems/standard/maximum-of-2-integers.pbm/problem.ca.yml +3 -0
  46. package/assets/problems/standard/maximum-of-2-integers.pbm/problem.en.tex +16 -0
  47. package/assets/problems/standard/maximum-of-2-integers.pbm/problem.en.yml +4 -0
  48. package/assets/problems/standard/maximum-of-2-integers.pbm/sample-1.inp +1 -0
  49. package/assets/problems/standard/maximum-of-2-integers.pbm/sample-2.inp +1 -0
  50. package/assets/problems/standard/maximum-of-2-integers.pbm/sample-3.inp +1 -0
  51. package/assets/problems/standard/maximum-of-2-integers.pbm/solution.c +18 -0
  52. package/assets/problems/standard/maximum-of-2-integers.pbm/solution.cc +13 -0
  53. package/assets/problems/standard/maximum-of-2-integers.pbm/solution.java +16 -0
  54. package/assets/problems/standard/maximum-of-2-integers.pbm/solution.py +5 -0
  55. package/assets/problems/standard/maximum-of-2-integers.pbm/test-1.inp +1 -0
  56. package/assets/problems/standard/maximum-of-2-integers.pbm/test-2.inp +1 -0
  57. package/assets/problems/standard/maximum-of-2-integers.pbm/test-3.inp +1 -0
  58. package/assets/problems/standard/maximum-of-2-integers.pbm/test-4.inp +1 -0
  59. package/assets/problems/standard/maximum-of-2-integers.pbm/test-5.inp +1 -0
  60. package/assets/problems/standard/treasures-in-a-map.pbm/README.md +12 -0
  61. package/assets/problems/standard/treasures-in-a-map.pbm/atzar.cc +85 -0
  62. package/assets/problems/standard/treasures-in-a-map.pbm/award.png +0 -0
  63. package/assets/problems/standard/treasures-in-a-map.pbm/generate-1.cc +26 -0
  64. package/assets/problems/standard/treasures-in-a-map.pbm/generate-2.cc +26 -0
  65. package/assets/problems/standard/treasures-in-a-map.pbm/generate-3.cc +26 -0
  66. package/assets/problems/standard/treasures-in-a-map.pbm/generate-4.cc +26 -0
  67. package/assets/problems/standard/treasures-in-a-map.pbm/handler.yml +1 -0
  68. package/assets/problems/standard/treasures-in-a-map.pbm/problem.ca.tex +40 -0
  69. package/assets/problems/standard/treasures-in-a-map.pbm/problem.ca.yml +3 -0
  70. package/assets/problems/standard/treasures-in-a-map.pbm/problem.en.tex +40 -0
  71. package/assets/problems/standard/treasures-in-a-map.pbm/problem.en.yml +4 -0
  72. package/assets/problems/standard/treasures-in-a-map.pbm/random-1.inp +24 -0
  73. package/assets/problems/standard/treasures-in-a-map.pbm/random-2.inp +27 -0
  74. package/assets/problems/standard/treasures-in-a-map.pbm/random-3.inp +38 -0
  75. package/assets/problems/standard/treasures-in-a-map.pbm/random-4.inp +50 -0
  76. package/assets/problems/standard/treasures-in-a-map.pbm/sample-1.inp +9 -0
  77. package/assets/problems/standard/treasures-in-a-map.pbm/sample-2.inp +6 -0
  78. package/assets/problems/standard/treasures-in-a-map.pbm/sample-3.inp +7 -0
  79. package/assets/problems/standard/treasures-in-a-map.pbm/solution.cc +38 -0
  80. package/assets/problems/standard/treasures-in-a-map.pbm/test-1.inp +5 -0
  81. package/assets/problems/standard/treasures-in-a-map.pbm/test-2.inp +6 -0
  82. package/assets/problems/standard/treasures-in-a-map.pbm/test-3.inp +6 -0
  83. package/assets/problems/standard/treasures-in-a-map.pbm/test-4.inp +9 -0
  84. package/assets/problems/standard/treasures-in-a-map.pbm/test-5.inp +10 -0
  85. package/assets/problems/standard/treasures-in-a-map.pbm/test-6.inp +9 -0
  86. package/assets/problems/standard/treasures-in-a-map.pbm/test-7.inp +12 -0
  87. package/assets/problems/standard/treasures-in-a-map.pbm/test-8.inp +3 -0
  88. package/assets/problems/standard/treasures-in-a-map.pbm/test-9.inp +37 -0
  89. package/assets/problems/standard/treasures-in-a-map.pbm/test-91.inp +52 -0
  90. package/assets/problems/standard/treasures-in-a-map.pbm/test-92.inp +25 -0
  91. package/assets/problems/standard/treasures-in-a-map.pbm/test-93.inp +3 -0
  92. package/assets/sty/judgeit.ca.sty +54 -0
  93. package/assets/sty/judgeit.de.sty +61 -0
  94. package/assets/sty/judgeit.en.sty +60 -0
  95. package/assets/sty/judgeit.es.sty +54 -0
  96. package/assets/sty/judgeit.fr.sty +59 -0
  97. package/assets/sty/judgeit.sty +307 -0
  98. package/assets/sty/picins.sty +579 -0
  99. package/assets.zip +0 -0
  100. package/eslint.config.mjs +31 -0
  101. package/lib/ai.ts +138 -0
  102. package/lib/assets.ts +31 -0
  103. package/lib/cleaner.ts +58 -0
  104. package/lib/compilers/_frompython.ts +388 -0
  105. package/lib/compilers/base.ts +97 -0
  106. package/lib/compilers/gcc.ts +47 -0
  107. package/lib/compilers/gxx.ts +47 -0
  108. package/lib/compilers/index.ts +61 -0
  109. package/lib/compilers/python3.ts +67 -0
  110. package/lib/data.ts +19 -0
  111. package/lib/doctor.ts +104 -0
  112. package/lib/generate.ts +333 -0
  113. package/lib/maker.ts +535 -0
  114. package/lib/settings.ts +42 -0
  115. package/lib/tui.ts +69 -0
  116. package/lib/utils.ts +83 -0
  117. package/package.json +56 -0
  118. package/problems/graphic.pbm/README.md +14 -0
  119. package/problems/graphic.pbm/award.png +0 -0
  120. package/problems/graphic.pbm/handler.yml +2 -0
  121. package/problems/graphic.pbm/problem.ca.html +13 -0
  122. package/problems/graphic.pbm/problem.ca.md +20 -0
  123. package/problems/graphic.pbm/problem.ca.tex +21 -0
  124. package/problems/graphic.pbm/problem.ca.txt +20 -0
  125. package/problems/graphic.pbm/problem.ca.yml +3 -0
  126. package/problems/graphic.pbm/sample-1.inp +1 -0
  127. package/problems/graphic.pbm/sample-2.inp +1 -0
  128. package/problems/graphic.pbm/solution.py +11 -0
  129. package/problems/maxim2.pbm/Main.java +13 -0
  130. package/problems/maxim2.pbm/distillation.yml +7 -0
  131. package/problems/maxim2.pbm/distilled-01.inp +1 -0
  132. package/problems/maxim2.pbm/distilled-02.inp +1 -0
  133. package/problems/maxim2.pbm/distilled-03.inp +1 -0
  134. package/problems/maxim2.pbm/distiller.yml +2 -0
  135. package/problems/maxim2.pbm/generate-inputs.py +9 -0
  136. package/problems/maxim2.pbm/handler.yml +2 -0
  137. package/problems/maxim2.pbm/ma-1.inp +1 -0
  138. package/problems/maxim2.pbm/ma-2.inp +1 -0
  139. package/problems/maxim2.pbm/ma-3.inp +1 -0
  140. package/problems/maxim2.pbm/ma-4.inp +1 -0
  141. package/problems/maxim2.pbm/ma-5.inp +1 -0
  142. package/problems/maxim2.pbm/per-doubles.inp +1 -0
  143. package/problems/maxim2.pbm/problem.ca.html +11 -0
  144. package/problems/maxim2.pbm/problem.ca.md +19 -0
  145. package/problems/maxim2.pbm/problem.ca.tex +17 -0
  146. package/problems/maxim2.pbm/problem.ca.txt +19 -0
  147. package/problems/maxim2.pbm/problem.ca.yml +3 -0
  148. package/problems/maxim2.pbm/problem.en.html +11 -0
  149. package/problems/maxim2.pbm/problem.en.md +19 -0
  150. package/problems/maxim2.pbm/problem.en.tex +16 -0
  151. package/problems/maxim2.pbm/problem.en.txt +19 -0
  152. package/problems/maxim2.pbm/problem.en.yml +4 -0
  153. package/problems/maxim2.pbm/sample-1.inp +1 -0
  154. package/problems/maxim2.pbm/sample-2.inp +1 -0
  155. package/problems/maxim2.pbm/sample-3.inp +1 -0
  156. package/problems/maxim2.pbm/solution.c +12 -0
  157. package/problems/maxim2.pbm/solution.cc +13 -0
  158. package/problems/maxim2.pbm/solution.java +13 -0
  159. package/problems/maxim2.pbm/solution.pas +9 -0
  160. package/problems/maxim2.pbm/solution.py +5 -0
  161. package/problems/maxim2.pbm/tags.yml +2 -0
  162. package/problems/maxim2.pbm/test_-1_-1.inp +1 -0
  163. package/problems/maxim2.pbm/test_-1_-2.inp +1 -0
  164. package/problems/maxim2.pbm/test_-1_0.inp +1 -0
  165. package/problems/maxim2.pbm/test_-1_1.inp +1 -0
  166. package/problems/maxim2.pbm/test_-2_-1.inp +1 -0
  167. package/problems/maxim2.pbm/test_-2_-2.inp +1 -0
  168. package/problems/maxim2.pbm/test_-2_0.inp +1 -0
  169. package/problems/maxim2.pbm/test_-2_1.inp +1 -0
  170. package/problems/maxim2.pbm/test_0_-1.inp +1 -0
  171. package/problems/maxim2.pbm/test_0_-2.inp +1 -0
  172. package/problems/maxim2.pbm/test_0_0.inp +1 -0
  173. package/problems/maxim2.pbm/test_0_1.inp +1 -0
  174. package/problems/maxim2.pbm/test_1_-1.inp +1 -0
  175. package/problems/maxim2.pbm/test_1_-2.inp +1 -0
  176. package/problems/maxim2.pbm/test_1_0.inp +1 -0
  177. package/problems/maxim2.pbm/test_1_1.inp +1 -0
  178. package/test.ts +3 -0
  179. package/toolkit/ai.ts +30 -0
  180. package/toolkit/clean.ts +19 -0
  181. package/toolkit/compilers.ts +29 -0
  182. package/toolkit/create-jutge-ai.ts +101 -0
  183. package/toolkit/create-template.ts +51 -0
  184. package/toolkit/create-wizard.ts +4 -0
  185. package/toolkit/create.ts +75 -0
  186. package/toolkit/doctor.ts +17 -0
  187. package/toolkit/index.ts +28 -0
  188. package/toolkit/init.ts +66 -0
  189. package/toolkit/make.ts +60 -0
  190. package/toolkit/verify.ts +19 -0
  191. package/tsconfig.json +38 -0
  192. package/types/zip.d.ts +4 -0
package/lib/maker.ts ADDED
@@ -0,0 +1,535 @@
1
+ import { imageSizeFromFile } from 'image-size/fromFile'
2
+ import { paths } from '@/lib/settings'
3
+ import * as tui from '@/lib/tui.js'
4
+ import { filesAreEqual, fileSize, readYaml } from '@/lib/utils'
5
+ import chalk from 'chalk'
6
+ import { cp, exists, mkdir, mkdtemp, rename, rm } from 'fs/promises'
7
+ import { tmpdir } from 'os'
8
+ import { join, normalize } from 'path'
9
+ import prettyBytes from 'pretty-bytes'
10
+ import prettyMs from 'pretty-ms'
11
+ import z from 'zod'
12
+ import { getCompilerByExtension } from './compilers'
13
+ import type { Compiler } from './compilers/base'
14
+ import { languageNames, proglangExtensions } from './data'
15
+ import { file } from 'bun'
16
+
17
+ export const ZHandler = z.object({
18
+ handler: z.enum(['std', 'graphic']).default('std'),
19
+ solution: z.string().default('C++'),
20
+ source_modifier: z.string().default('none'),
21
+ })
22
+
23
+ export type Handler = z.infer<typeof ZHandler>
24
+
25
+ export interface MakerOptions {
26
+ verbose?: boolean
27
+ directory: string
28
+ }
29
+
30
+ export async function newMaker(options: MakerOptions): Promise<Maker> {
31
+ const maker = new Maker(options)
32
+ await maker.makeInspection()
33
+ return maker
34
+ }
35
+
36
+ type ExecutionResult = {
37
+ testcase: string
38
+ time: number
39
+ inputSize: number
40
+ outputSize: number
41
+ error: boolean
42
+ }
43
+
44
+ export class Maker {
45
+ verbose: boolean
46
+ directory: string
47
+ // @ts-expect-error: will be initialized later
48
+ handler: Handler
49
+ languages: string[] = []
50
+ problemYmls: Record<string, any> = {} // language -> content
51
+ solutions: string[] = []
52
+ goldenSolution: string | null = null
53
+ testcases: string[] = [] // testcase names without extensions
54
+
55
+ public constructor(options: MakerOptions) {
56
+ this.verbose = options.verbose || false
57
+ this.directory = normalize(join(options.directory, '.')) // normalize path and ensure no trailing slash
58
+ }
59
+
60
+ private async loadHandler() {
61
+ await tui.section('Loading handler.yml', async () => {
62
+ const data = await readYaml(`${this.directory}/handler.yml`)
63
+ try {
64
+ this.handler = ZHandler.parse(data)
65
+ } catch (e) {
66
+ tui.error(`Invalid handler.yml format`)
67
+ console.dir(e)
68
+ process.exit(1)
69
+ }
70
+ tui.print(Bun.YAML.stringify(this.handler, null, 2))
71
+ })
72
+ }
73
+
74
+ private async loadLanguages() {
75
+ await tui.section('Loading languages', async () => {
76
+ const glob = new Bun.Glob('problem.*.yml')
77
+ const files = await Array.fromAsync(glob.scan(this.directory))
78
+ const languages = files
79
+ .map((file) => {
80
+ const match = file.match(/problem\.(.+)\.yml/)
81
+ return match ? match[1] : null
82
+ })
83
+ .filter((language) => language && language in languageNames)
84
+ this.languages = languages as string[]
85
+ tui.print(Bun.YAML.stringify(this.languages, null, 2))
86
+
87
+ for (const language of this.languages) {
88
+ await tui.section(`Loading problem.${language}.yml`, async () => {
89
+ this.problemYmls[language] = await readYaml(`${this.directory}/problem.${language}.yml`)
90
+ tui.print(Bun.YAML.stringify(this.problemYmls[language], null, 2))
91
+ })
92
+ }
93
+ })
94
+ }
95
+
96
+ private async loadSolutions() {
97
+ await tui.section('Loading solutions', async () => {
98
+ const glob = new Bun.Glob('solution.{py,cc}')
99
+ const files = await Array.fromAsync(glob.scan(this.directory))
100
+ this.solutions = files
101
+ tui.print(Bun.YAML.stringify(this.solutions, null, 2))
102
+ if (this.solutions.length === 0) throw new Error('No solutions found')
103
+ })
104
+ }
105
+
106
+ private async loadGoldenSolution() {
107
+ await tui.section('Loading golden solution', async () => {
108
+ const solutionProglang = this.handler.solution
109
+ const extension = proglangExtensions[solutionProglang]
110
+ if (!extension) {
111
+ tui.error(`Unknown programming language '${solutionProglang}' for solution`)
112
+ process.exit(1)
113
+ }
114
+ const goldenSolutionPath = join(this.directory, `solution.${extension}`)
115
+ const fileExists = await exists(goldenSolutionPath)
116
+ if (!fileExists) {
117
+ tui.error(`Golden solution file '${goldenSolutionPath}' not found`)
118
+ process.exit(1)
119
+ }
120
+ this.goldenSolution = `solution.${extension}`
121
+ tui.print(this.goldenSolution)
122
+ })
123
+ }
124
+
125
+ private async loadTestcases() {
126
+ await tui.section('Loading testcases', async () => {
127
+ const glob = new Bun.Glob('*.inp')
128
+ const files = await Array.fromAsync(glob.scan(this.directory))
129
+ this.testcases = files.map((file) => file.replace('.inp', '')).sort()
130
+ tui.print(Bun.YAML.stringify(this.testcases, null, 2))
131
+ })
132
+ }
133
+
134
+ public async makeInspection() {
135
+ await tui.section('Inspecting problem', async () => {
136
+ await this.loadHandler()
137
+ await this.loadLanguages()
138
+ await this.loadSolutions()
139
+ await this.loadGoldenSolution()
140
+ await this.loadTestcases()
141
+ })
142
+ }
143
+
144
+ public async makeProblem() {
145
+ await this.makeExecutables()
146
+ await this.makeCorrects()
147
+ await this.makePdfs()
148
+ await this.makeTexts()
149
+ }
150
+
151
+ public async makeExecutable(program: string) {
152
+ await tui.section(`Compiling ${program}`, async () => {
153
+ const extension = program.split('.').pop()!
154
+ const compiler = getCompilerByExtension(extension)
155
+ try {
156
+ await compiler.compile(this.directory, program)
157
+ } catch (error) {
158
+ tui.error(`Compilation failed`)
159
+ console.error(error)
160
+ process.exit(1)
161
+ }
162
+ })
163
+ }
164
+
165
+ public async makeExecutables() {
166
+ await tui.section('Compiling solutions', async () => {
167
+ for (const solution of this.solutions) {
168
+ await this.makeExecutable(solution)
169
+ }
170
+ })
171
+ }
172
+
173
+ async makeCorrect(testcase: string, compiler: Compiler): Promise<ExecutionResult> {
174
+ return await this.runTestcase(testcase, `${testcase}.inp`, `${testcase}.cor`, compiler)
175
+ }
176
+
177
+ async runTestcase(testcase: string, input: string, output: string, compiler: Compiler): Promise<ExecutionResult> {
178
+ let error = false
179
+ const start = Date.now()
180
+ try {
181
+ await compiler.execute(this.directory, input, output)
182
+ } catch (e) {
183
+ tui.error(`Execution failed for testcase '${testcase}'`)
184
+ error = true
185
+ }
186
+ const end = Date.now()
187
+ const time = end - start
188
+
189
+ if (this.handler.handler === 'graphic') {
190
+ await rename(join(this.directory, 'output.png'), join(this.directory, output))
191
+ }
192
+
193
+ const inputSize = await fileSize(`${this.directory}/${input}`)
194
+ const outputSize = await fileSize(`${this.directory}/${output}`)
195
+
196
+ return { testcase, error, time, inputSize, outputSize }
197
+ }
198
+
199
+ public async makeCorrects() {
200
+ await tui.section('Making corrects', async () => {
201
+ const extension = this.goldenSolution!.split('.').pop()!
202
+ const compiler = getCompilerByExtension(extension)
203
+ const results: ExecutionResult[] = []
204
+ await tui.section(`Using ${compiler.id()} on ${this.goldenSolution} to make corrects`, async () => {
205
+ for (const testcase of this.testcases) {
206
+ results.push(await this.makeCorrect(testcase, compiler))
207
+ }
208
+
209
+ console.log()
210
+ console.log(`testcase time input output`)
211
+ for (const result of results) {
212
+ const time = prettyMs(result.time)
213
+ const inputSize = prettyBytes(result.inputSize).replace(' ', '')
214
+ const outputSize = prettyBytes(result.outputSize).replace(' ', '')
215
+ console.log(
216
+ (result.error ? chalk.red : chalk.green)(
217
+ `${result.testcase.padEnd(12)} ${time.padStart(10)} ${inputSize.padStart(10)} ${outputSize.padStart(
218
+ 10,
219
+ )}`,
220
+ ),
221
+ )
222
+ }
223
+ console.log()
224
+
225
+ const errors = results.filter((result) => result.error).length
226
+ if (errors > 0) {
227
+ tui.error(`${errors} errors occurred while making correct answers`)
228
+ process.exit(1)
229
+ }
230
+ })
231
+ })
232
+ }
233
+
234
+ public async makePdfs() {
235
+ await tui.section('Making PDF statements', async () => {
236
+ await tui.section('Creating temporary directory', async () => {})
237
+ const tmpDirBase = await mkdtemp(join(tmpdir(), `pdf-`))
238
+ tui.command(tmpDirBase)
239
+ try {
240
+ for (const language of this.languages) {
241
+ await tui.section(`Making PDF statement for ${language}`, async () => {
242
+ await this.makePdf(tmpDirBase, language)
243
+ })
244
+ }
245
+ } finally {
246
+ // TODO: await rm(tmpDirBase, { recursive: true, force: true })
247
+ }
248
+ })
249
+ }
250
+
251
+ async makeSamples(tmpDir: string, language: string): Promise<[string, string]> {
252
+ const graphic = this.handler.handler === 'graphic'
253
+ const samples1c: string[] = []
254
+ const samples2c: string[] = []
255
+ let index = 1
256
+ for (const testcase of this.testcases) {
257
+ if (testcase.startsWith('sample')) {
258
+ let size = ''
259
+ if (graphic) {
260
+ await cp(join(this.directory, `${testcase}.cor`), join(tmpDir, `${testcase}.cor.png`))
261
+ const dimensions = await imageSizeFromFile(join(tmpDir, `${testcase}.cor.png`))
262
+ size = `(${dimensions.width}$\\times$${dimensions.height})`
263
+ }
264
+ samples1c.push(`\n\\SampleOneColInputOutput[${size}]{${testcase}}{${index}}\n`)
265
+ samples2c.push(`\n\\SampleTwoColInputOutput[${size}]{${testcase}}{${index}}\n`)
266
+ index++
267
+ }
268
+ }
269
+ return [samples1c.join('\n'), samples2c.join('\n')]
270
+ }
271
+
272
+ async makePdf(tmpDirBase: string, language: string) {
273
+ const tmpDir = join(tmpDirBase, language)
274
+ await mkdir(tmpDir)
275
+
276
+ const date = new Date().toISOString().substring(0, 10)
277
+ const year = new Date().getFullYear()
278
+ const author = this.problemYmls[language].author || 'Unknown'
279
+ const authorEmail = this.problemYmls[language].author_email || 'unknown email'
280
+ const translator = this.problemYmls[language].translator || ''
281
+
282
+ const [samples1c, samples2c] = await this.makeSamples(tmpDir, language)
283
+
284
+ const root = `
285
+
286
+ \\documentclass[11pt]{article}
287
+
288
+ \\usepackage{judgeit}
289
+ \\usepackage{judgeit.${language}}
290
+
291
+ % TODO?
292
+ \\lstMakeShortInline@
293
+
294
+ \\begin{document}
295
+
296
+ \\providecommand{\\SampleOneCol}{${samples1c}}
297
+ \\providecommand{\\SampleTwoCol}{${samples2c}}
298
+ \\ProblemId{{DRAFT ${language}}}
299
+ \\DoProblem{${language}}
300
+
301
+ \\ProblemInformation
302
+ \\Author: ${author}\\\\ ${translator ? `(${language}: ${translator})` : ''}
303
+ \\Generation: ${date}
304
+
305
+ \\subsection*{Draft}
306
+ Draft generated with \\textbf{new-jutge-toolkit}.
307
+
308
+ \\end{document}
309
+
310
+ `
311
+
312
+ // copy files to tmpDir
313
+ await cp(this.directory, tmpDir, { recursive: true })
314
+
315
+ // write root.tex
316
+ await Bun.write(join(tmpDir, 'root.tex'), root)
317
+
318
+ // tweak the tex file
319
+ const tex1 = await Bun.file(join(tmpDir, `problem.${language}.tex`)).text()
320
+ const tex2 = tex1
321
+ .replace(/\\begin{htmlonly}[\s\S]*?\\end{htmlonly}/g, '')
322
+ .replace(/\\begin{latexonly}/g, '')
323
+ .replace(/\\end{latexonly}/g, '')
324
+ .replace(/\.eps}/g, '}')
325
+ await Bun.write(join(tmpDir, `problem.${language}.tex`), tex2)
326
+
327
+ // copy style files
328
+ await this.copyStyleFiles(tmpDir, language)
329
+
330
+ // latex
331
+ try {
332
+ tui.command('pdflatex -interaction=nonstopmode -file-line-error root.tex')
333
+ await Bun.$`pdflatex -interaction=nonstopmode -file-line-error root.tex`.cwd(tmpDir).text()
334
+ await cp(join(tmpDir, 'root.pdf'), join(this.directory, `problem.${language}.pdf`))
335
+ tui.success(`Generated problem.${language}.pdf`)
336
+ } catch (e) {
337
+ console.error('pdflatex error', e)
338
+ }
339
+ }
340
+
341
+ async copyStyleFiles(tmpDir: string, language: string) {
342
+ const cpSty = async (path: string) => {
343
+ const source = join(paths.data, 'assets', 'sty', path)
344
+ await Bun.write(join(tmpDir, path), Bun.file(source))
345
+ }
346
+ await cpSty('picins.sty')
347
+ await cpSty('judgeit.sty')
348
+ if (language === 'ca') await cpSty('judgeit.ca.sty')
349
+ if (language === 'es') await cpSty('judgeit.es.sty')
350
+ if (language === 'en') await cpSty('judgeit.en.sty')
351
+ if (language === 'fr') await cpSty('judgeit.fr.sty')
352
+ if (language === 'de') await cpSty('judgeit.de.sty')
353
+ }
354
+
355
+ public async makeTexts() {
356
+ await tui.section('Making text statements', async () => {
357
+ await tui.section('Creating temporary directory', async () => {})
358
+ const tmpDirBase = await mkdtemp(join(tmpdir(), `text-`))
359
+ tui.command(tmpDirBase)
360
+ try {
361
+ for (const language of this.languages) {
362
+ await tui.section(`Making text statements for ${language}`, async () => {
363
+ await this.makeText(tmpDirBase, language)
364
+ })
365
+ }
366
+ } finally {
367
+ await rm(tmpDirBase, { recursive: true, force: true })
368
+ }
369
+ })
370
+ }
371
+
372
+ async makeText(tmpDirBase: string, language: string) {
373
+ const tmpDir = join(tmpDirBase, language)
374
+ await mkdir(tmpDir)
375
+
376
+ const date = new Date().toISOString().substring(0, 10)
377
+ const year = new Date().getFullYear()
378
+ const author = this.problemYmls[language].author || 'Unknown'
379
+ const authorEmail = this.problemYmls[language].author_email || 'unknown email'
380
+ const translator = this.problemYmls[language].translator || ''
381
+
382
+ const root = `
383
+
384
+ \\documentclass[11pt]{article}
385
+
386
+ \\usepackage{judgeit}
387
+ \\usepackage{judgeit.${language}}
388
+
389
+ % TODO?
390
+ \\lstMakeShortInline@
391
+
392
+ % redefine commands to simplify text output
393
+ \\renewcommand{\\Sample}{}
394
+ \\renewcommand{\\Statement}{}
395
+ \\renewcommand{\\Problem}[1]{\\section{#1}}
396
+ \\renewcommand{\\ProblemId}[1]{DRAFT ${language}}
397
+
398
+ % redefine figure commands to avoid troubles with \\parpic
399
+ \\renewcommand{\\FigureL}[2]{\\includegraphics[#1]{#2}}
400
+ \\renewcommand{\\FigureC}[2]{\\includegraphics[#1]{#2}}
401
+ \\renewcommand{\\FigureR}[2]{\\includegraphics[#1]{#2}}
402
+
403
+ \\begin{document}
404
+
405
+ \\DoProblem{${language}}
406
+
407
+ \\subsection*{\\TxtAuthor}
408
+ ${author} ${translator ? `(${language}: ${translator})` : ''}
409
+
410
+ \\subsection*{Draft}
411
+ Draft generated with \\textbf{new-jutge-toolkit}.
412
+
413
+ \\end{document}
414
+
415
+ `
416
+
417
+ // copy files to tmpDir
418
+ await cp(this.directory, tmpDir, { recursive: true })
419
+
420
+ // write root.tex
421
+ await Bun.write(join(tmpDir, 'root.tex'), root)
422
+
423
+ // tweak the tex file
424
+ const tex1 = await Bun.file(join(tmpDir, `problem.${language}.tex`)).text()
425
+ const tex2 = tex1
426
+ .replace(/\\begin{latexonly}[\s\S]*?\\end{latexonly}/g, '')
427
+ .replace(/\\begin{htmlonly}/g, '')
428
+ .replace(/\\end{htmlonly}/g, '')
429
+ .replace(/\\begin{minipage}/g, '')
430
+ .replace(/\\end{minipage}/g, '')
431
+ .replace(/\.eps}/g, '}')
432
+ await Bun.write(join(tmpDir, `problem.${language}.tex`), tex2)
433
+
434
+ // copy style files
435
+ await this.copyStyleFiles(tmpDir, language)
436
+
437
+ // convert .eps files to png
438
+ const glob2 = new Bun.Glob('*.{eps}')
439
+ for await (const file of glob2.scan(tmpDir)) {
440
+ tui.command(`convert ${file} ${file.replace(/\.eps$/, '.png')}`)
441
+ await Bun.$`convert ${file} ${file.replace(/\.eps$/, '.png')}`.cwd(tmpDir)
442
+ }
443
+
444
+ // create lua files
445
+ const luaSource = join(paths.data, 'assets', 'lua', 'fixCodeBlocks.lua')
446
+ await Bun.write(join(tmpDir, 'fixCodeBlocks.lua'), Bun.file(luaSource))
447
+
448
+ // txt
449
+ try {
450
+ tui.command('pandoc --quiet root.tex --to plain --output root.txt')
451
+ await Bun.$`pandoc --quiet root.tex --to plain --output root.txt`.cwd(tmpDir)
452
+ await cp(join(tmpDir, 'root.txt'), join(this.directory, `problem.${language}.txt`))
453
+ tui.success(`Generated problem.${language}.txt`)
454
+ } catch (e) {
455
+ console.error('pandoc error', e)
456
+ }
457
+
458
+ // md
459
+ try {
460
+ tui.command(
461
+ 'pandoc --quiet root.tex --to markdown --to markdown-header_attributes --lua-filter=fixCodeBlocks.lua --output root.md',
462
+ )
463
+ await Bun.$`pandoc --quiet root.tex --to markdown --to markdown-header_attributes --lua-filter=fixCodeBlocks.lua --output root.md`.cwd(
464
+ tmpDir,
465
+ )
466
+ await cp(join(tmpDir, 'root.md'), join(this.directory, `problem.${language}.md`))
467
+ tui.success(`Generated problem.${language}.md`)
468
+ } catch (e) {
469
+ console.error('pandoc error', e)
470
+ }
471
+
472
+ // md
473
+ try {
474
+ tui.command('pandoc --quiet root.tex --to html --mathml --embed-resources --output root.html')
475
+ await Bun.$`pandoc --quiet root.tex --to html --mathml --embed-resources --output root.html`.cwd(tmpDir)
476
+ await cp(join(tmpDir, 'root.html'), join(this.directory, `problem.${language}.html`))
477
+ tui.success(`Generated problem.${language}.html`)
478
+ } catch (e) {
479
+ console.error('pandoc error', e)
480
+ }
481
+ }
482
+
483
+ public async verifyCandidate(program: string) {
484
+ await tui.section(`Verifying candidate ${program}`, async () => {
485
+ const extension = program.split('.').pop()!
486
+ const compiler = getCompilerByExtension(extension)
487
+
488
+ await tui.section(`Using compiler '${compiler.name()}' to compile '${program}'`, async () => {
489
+ try {
490
+ await compiler.compile(this.directory, program)
491
+ } catch (error) {
492
+ tui.error(`Compilation failed`)
493
+ process.exit(1)
494
+ }
495
+ })
496
+
497
+ const results: ExecutionResult[] = []
498
+ await tui.section('Executing candidate on testcases', async () => {
499
+ for (const testcase of this.testcases) {
500
+ results.push(
501
+ await this.runTestcase(testcase, `${testcase}.inp`, `${testcase}.${extension}.out`, compiler),
502
+ )
503
+ }
504
+
505
+ console.log()
506
+ console.log(`testcase time status`)
507
+ let errors = 0
508
+ for (const result of results) {
509
+ const status = result.error
510
+ ? 'EE'
511
+ : (await filesAreEqual(
512
+ `${this.directory}/${result.testcase}.cor`,
513
+ `${this.directory}/${result.testcase}.${extension}.out`,
514
+ ))
515
+ ? 'OK'
516
+ : 'WA'
517
+ const time = prettyMs(result.time)
518
+ console.log(
519
+ (status !== 'OK' ? chalk.red : chalk.green)(
520
+ `${result.testcase.padEnd(12)} ${time.padStart(10)} ${status.padStart(10)}`,
521
+ ),
522
+ )
523
+ if (status !== 'OK') errors++
524
+ }
525
+ console.log()
526
+
527
+ if (errors) {
528
+ tui.error(`${errors} errors found for candidate '${program}'`)
529
+ } else {
530
+ tui.success(`All testcases passed successfully for candidate '${program}'`)
531
+ }
532
+ })
533
+ })
534
+ }
535
+ }
@@ -0,0 +1,42 @@
1
+ import { exists } from 'fs/promises'
2
+ import envPaths from 'env-paths'
3
+ import { mkdir } from 'fs/promises'
4
+ import { join } from 'path'
5
+ import { z } from 'zod'
6
+
7
+ const ZSettings = z.object({
8
+ name: z.string().min(1).default('John Doe'),
9
+ email: z.string().email().default('john.doe@example.com'),
10
+ notifications: z.boolean().default(true),
11
+ })
12
+
13
+ export type Settings = z.infer<typeof ZSettings>
14
+
15
+ export const paths = envPaths('jutge', { suffix: 'toolkit' })
16
+
17
+ function configPath() {
18
+ return join(paths.config, 'config.json')
19
+ }
20
+
21
+ export async function initializePaths() {
22
+ await mkdir(paths.config, { recursive: true })
23
+ await mkdir(paths.data, { recursive: true })
24
+ await mkdir(paths.cache, { recursive: true })
25
+ await mkdir(paths.log, { recursive: true })
26
+ await mkdir(paths.temp, { recursive: true })
27
+ }
28
+
29
+ export async function saveSettings(settings: Settings) {
30
+ await initializePaths()
31
+ return await Bun.write(configPath(), JSON.stringify(settings, null, 4))
32
+ }
33
+
34
+ export async function loadSettings(): Promise<Settings> {
35
+ const data = await Bun.file(configPath()).text()
36
+ const parsed = JSON.parse(data)
37
+ return ZSettings.parse(parsed)
38
+ }
39
+
40
+ export async function settingsExist(): Promise<boolean> {
41
+ return await exists(configPath())
42
+ }
package/lib/tui.ts ADDED
@@ -0,0 +1,69 @@
1
+ import boxen from 'boxen'
2
+ import chalk from 'chalk'
3
+ import { marked } from 'marked'
4
+ import { markedTerminal } from 'marked-terminal'
5
+
6
+ let indentation = 0
7
+
8
+ const symbols = ['', '▶', '◆', '●', '■']
9
+
10
+ export function sectionStart(text: string) {
11
+ if (indentation === 0) {
12
+ console.log(chalk.blue(boxen(text, { padding: { left: 1, right: 1 }, width: process.stdout.columns })))
13
+ } else {
14
+ console.log(chalk.blue(`${symbols[indentation]} ${text}`))
15
+ }
16
+ ++indentation
17
+ console.group()
18
+ }
19
+
20
+ export function sectionEnd() {
21
+ --indentation
22
+ console.groupEnd()
23
+ }
24
+
25
+ export async function section<T>(text: string, fn: () => Promise<T>): Promise<T> {
26
+ sectionStart(text)
27
+ const result = await fn()
28
+ sectionEnd()
29
+ return result
30
+ }
31
+
32
+ export function title(text: string) {
33
+ console.log(chalk.blue.bold(boxen(text, { padding: { left: 1, right: 1 }, width: process.stdout.columns })))
34
+ console.log()
35
+ }
36
+
37
+ export function command(text: string) {
38
+ console.log(chalk.magenta(`❯ ${text}`))
39
+ }
40
+
41
+ export function warning(text: string) {
42
+ console.log(chalk.yellow(`Warning: ${text}`))
43
+ }
44
+
45
+ export function error(text: string) {
46
+ console.log(chalk.red(`Error: ${text}`))
47
+ }
48
+
49
+ export function success(text: string) {
50
+ console.log(chalk.green(`Success: ${text}`))
51
+ }
52
+
53
+ export function action(text: string) {
54
+ console.log(chalk.blue(`${text}...`))
55
+ }
56
+
57
+ export function print(text: string): void {
58
+ const lines = text.split('\n')
59
+ for (const line of lines) {
60
+ console.log(line)
61
+ }
62
+ }
63
+
64
+ export async function markdown(content: string): Promise<void> {
65
+ // @ts-expect-error: i don't know why types are not working here but seems to work at runtime
66
+ marked.use(markedTerminal())
67
+ const output = await marked.parse(content)
68
+ console.log(output)
69
+ }