@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.
- package/.prettierignore +11 -0
- package/.prettierrc.json +9 -0
- package/.vscode/settings.json +26 -0
- package/README.md +22 -0
- package/assets/lua/fixCodeBlocks.lua +19 -0
- package/assets/lua/removeEnvs.lua +20 -0
- package/assets/lua/removeHtmlOnly.lua +11 -0
- package/assets/problems/graphics/japanese-flag.pbm/README.md +14 -0
- package/assets/problems/graphics/japanese-flag.pbm/award.png +0 -0
- package/assets/problems/graphics/japanese-flag.pbm/handler.yml +2 -0
- package/assets/problems/graphics/japanese-flag.pbm/problem.ca.tex +21 -0
- package/assets/problems/graphics/japanese-flag.pbm/problem.ca.yml +3 -0
- package/assets/problems/graphics/japanese-flag.pbm/sample-1.inp +1 -0
- package/assets/problems/graphics/japanese-flag.pbm/sample-2.inp +1 -0
- package/assets/problems/graphics/japanese-flag.pbm/solution.cc +25 -0
- package/assets/problems/graphics/japanese-flag.pbm/solution.py +11 -0
- package/assets/problems/graphics/tortuga.pbm/README.md +13 -0
- package/assets/problems/graphics/tortuga.pbm/award.png +0 -0
- package/assets/problems/graphics/tortuga.pbm/handler.yml +2 -0
- package/assets/problems/graphics/tortuga.pbm/problem.ca.tex +23 -0
- package/assets/problems/graphics/tortuga.pbm/problem.ca.yml +3 -0
- package/assets/problems/graphics/tortuga.pbm/sample.inp +0 -0
- package/assets/problems/graphics/tortuga.pbm/solution.py +11 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/README.md +15 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/award.html +1 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/award.png +0 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/campanar.eps +1113 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/campanar.png +0 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/generate.cc +10 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/handler.yml +2 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.ca.tex +59 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.ca.yml +3 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.en.tex +52 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.en.yml +4 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/sample.inp +7 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/slow.cc +29 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/solution.cc +48 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-1.inp +12 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-2.inp +100000 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-2.ops +1 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-b.inp +0 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/README.md +11 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/handler.yml +1 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/problem.ca.tex +17 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/problem.ca.yml +3 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/problem.en.tex +16 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/problem.en.yml +4 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/sample-1.inp +1 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/sample-2.inp +1 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/sample-3.inp +1 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/solution.c +18 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/solution.cc +13 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/solution.java +16 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/solution.py +5 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/test-1.inp +1 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/test-2.inp +1 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/test-3.inp +1 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/test-4.inp +1 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/test-5.inp +1 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/README.md +12 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/atzar.cc +85 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/award.png +0 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/generate-1.cc +26 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/generate-2.cc +26 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/generate-3.cc +26 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/generate-4.cc +26 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/handler.yml +1 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/problem.ca.tex +40 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/problem.ca.yml +3 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/problem.en.tex +40 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/problem.en.yml +4 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/random-1.inp +24 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/random-2.inp +27 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/random-3.inp +38 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/random-4.inp +50 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/sample-1.inp +9 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/sample-2.inp +6 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/sample-3.inp +7 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/solution.cc +38 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-1.inp +5 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-2.inp +6 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-3.inp +6 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-4.inp +9 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-5.inp +10 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-6.inp +9 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-7.inp +12 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-8.inp +3 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-9.inp +37 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-91.inp +52 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-92.inp +25 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-93.inp +3 -0
- package/assets/sty/judgeit.ca.sty +54 -0
- package/assets/sty/judgeit.de.sty +61 -0
- package/assets/sty/judgeit.en.sty +60 -0
- package/assets/sty/judgeit.es.sty +54 -0
- package/assets/sty/judgeit.fr.sty +59 -0
- package/assets/sty/judgeit.sty +307 -0
- package/assets/sty/picins.sty +579 -0
- package/assets.zip +0 -0
- package/eslint.config.mjs +31 -0
- package/lib/ai.ts +138 -0
- package/lib/assets.ts +31 -0
- package/lib/cleaner.ts +58 -0
- package/lib/compilers/_frompython.ts +388 -0
- package/lib/compilers/base.ts +97 -0
- package/lib/compilers/gcc.ts +47 -0
- package/lib/compilers/gxx.ts +47 -0
- package/lib/compilers/index.ts +61 -0
- package/lib/compilers/python3.ts +67 -0
- package/lib/data.ts +19 -0
- package/lib/doctor.ts +104 -0
- package/lib/generate.ts +333 -0
- package/lib/maker.ts +535 -0
- package/lib/settings.ts +42 -0
- package/lib/tui.ts +69 -0
- package/lib/utils.ts +83 -0
- package/package.json +56 -0
- package/problems/graphic.pbm/README.md +14 -0
- package/problems/graphic.pbm/award.png +0 -0
- package/problems/graphic.pbm/handler.yml +2 -0
- package/problems/graphic.pbm/problem.ca.html +13 -0
- package/problems/graphic.pbm/problem.ca.md +20 -0
- package/problems/graphic.pbm/problem.ca.tex +21 -0
- package/problems/graphic.pbm/problem.ca.txt +20 -0
- package/problems/graphic.pbm/problem.ca.yml +3 -0
- package/problems/graphic.pbm/sample-1.inp +1 -0
- package/problems/graphic.pbm/sample-2.inp +1 -0
- package/problems/graphic.pbm/solution.py +11 -0
- package/problems/maxim2.pbm/Main.java +13 -0
- package/problems/maxim2.pbm/distillation.yml +7 -0
- package/problems/maxim2.pbm/distilled-01.inp +1 -0
- package/problems/maxim2.pbm/distilled-02.inp +1 -0
- package/problems/maxim2.pbm/distilled-03.inp +1 -0
- package/problems/maxim2.pbm/distiller.yml +2 -0
- package/problems/maxim2.pbm/generate-inputs.py +9 -0
- package/problems/maxim2.pbm/handler.yml +2 -0
- package/problems/maxim2.pbm/ma-1.inp +1 -0
- package/problems/maxim2.pbm/ma-2.inp +1 -0
- package/problems/maxim2.pbm/ma-3.inp +1 -0
- package/problems/maxim2.pbm/ma-4.inp +1 -0
- package/problems/maxim2.pbm/ma-5.inp +1 -0
- package/problems/maxim2.pbm/per-doubles.inp +1 -0
- package/problems/maxim2.pbm/problem.ca.html +11 -0
- package/problems/maxim2.pbm/problem.ca.md +19 -0
- package/problems/maxim2.pbm/problem.ca.tex +17 -0
- package/problems/maxim2.pbm/problem.ca.txt +19 -0
- package/problems/maxim2.pbm/problem.ca.yml +3 -0
- package/problems/maxim2.pbm/problem.en.html +11 -0
- package/problems/maxim2.pbm/problem.en.md +19 -0
- package/problems/maxim2.pbm/problem.en.tex +16 -0
- package/problems/maxim2.pbm/problem.en.txt +19 -0
- package/problems/maxim2.pbm/problem.en.yml +4 -0
- package/problems/maxim2.pbm/sample-1.inp +1 -0
- package/problems/maxim2.pbm/sample-2.inp +1 -0
- package/problems/maxim2.pbm/sample-3.inp +1 -0
- package/problems/maxim2.pbm/solution.c +12 -0
- package/problems/maxim2.pbm/solution.cc +13 -0
- package/problems/maxim2.pbm/solution.java +13 -0
- package/problems/maxim2.pbm/solution.pas +9 -0
- package/problems/maxim2.pbm/solution.py +5 -0
- package/problems/maxim2.pbm/tags.yml +2 -0
- package/problems/maxim2.pbm/test_-1_-1.inp +1 -0
- package/problems/maxim2.pbm/test_-1_-2.inp +1 -0
- package/problems/maxim2.pbm/test_-1_0.inp +1 -0
- package/problems/maxim2.pbm/test_-1_1.inp +1 -0
- package/problems/maxim2.pbm/test_-2_-1.inp +1 -0
- package/problems/maxim2.pbm/test_-2_-2.inp +1 -0
- package/problems/maxim2.pbm/test_-2_0.inp +1 -0
- package/problems/maxim2.pbm/test_-2_1.inp +1 -0
- package/problems/maxim2.pbm/test_0_-1.inp +1 -0
- package/problems/maxim2.pbm/test_0_-2.inp +1 -0
- package/problems/maxim2.pbm/test_0_0.inp +1 -0
- package/problems/maxim2.pbm/test_0_1.inp +1 -0
- package/problems/maxim2.pbm/test_1_-1.inp +1 -0
- package/problems/maxim2.pbm/test_1_-2.inp +1 -0
- package/problems/maxim2.pbm/test_1_0.inp +1 -0
- package/problems/maxim2.pbm/test_1_1.inp +1 -0
- package/test.ts +3 -0
- package/toolkit/ai.ts +30 -0
- package/toolkit/clean.ts +19 -0
- package/toolkit/compilers.ts +29 -0
- package/toolkit/create-jutge-ai.ts +101 -0
- package/toolkit/create-template.ts +51 -0
- package/toolkit/create-wizard.ts +4 -0
- package/toolkit/create.ts +75 -0
- package/toolkit/doctor.ts +17 -0
- package/toolkit/index.ts +28 -0
- package/toolkit/init.ts +66 -0
- package/toolkit/make.ts +60 -0
- package/toolkit/verify.ts +19 -0
- package/tsconfig.json +38 -0
- 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
|
+
}
|
package/lib/settings.ts
ADDED
|
@@ -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
|
+
}
|