testownik-converter 1.0.2 → 1.2.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/dist/index.js ADDED
@@ -0,0 +1,256 @@
1
+ // src/cli/index.ts
2
+ import { Command } from "commander";
3
+
4
+ // package.json
5
+ var package_default = {
6
+ name: "testownik-converter",
7
+ version: "1.1.0",
8
+ description: "A command-line utility to convert quizzes into Solvro's Testownik format.",
9
+ type: "module",
10
+ author: "Konrad Guzek",
11
+ license: "MIT",
12
+ scripts: {
13
+ build: "tsup"
14
+ },
15
+ homepage: "https://github.com/kguzek/testownik-converter#readme",
16
+ bugs: {
17
+ url: "https://github.com/kguzek/testownik-converter/issues"
18
+ },
19
+ repository: {
20
+ type: "git",
21
+ url: "git+https://github.com/kguzek/testownik-converter.git"
22
+ },
23
+ bin: {
24
+ "testownik-converter": "bin/index.js"
25
+ },
26
+ files: [
27
+ "dist",
28
+ "package.json"
29
+ ],
30
+ dependencies: {
31
+ boxen: "^8.0.1",
32
+ chalk: "^5.6.2",
33
+ commander: "^14.0.3"
34
+ },
35
+ devDependencies: {
36
+ "@semantic-release/changelog": "^6.0.3",
37
+ "@semantic-release/git": "^10.0.1",
38
+ "@types/node": "^25.2.0",
39
+ "semantic-release": "^25.0.3",
40
+ tsup: "^8.5.1",
41
+ typescript: "^5.9.3"
42
+ },
43
+ publishConfig: {
44
+ provenance: true
45
+ },
46
+ packageManager: "pnpm@10.28.2"
47
+ };
48
+
49
+ // src/constants/index.ts
50
+ var AUTHOR_EMAIL = "konrad@guzek.uk";
51
+ var AUTHOR_NAME = "Konrad Guzek";
52
+ var AUTHOR_URL = "https://github.com/kguzek";
53
+ var REPO_NAME = "Testownik Converter";
54
+ var REPO_SLUG = "testownik-converter";
55
+ var REPO_URL = `${AUTHOR_URL}/${REPO_SLUG}`;
56
+ var TESTOWNIK_URL = "https://testownik.solvro.pl";
57
+ var VERSION = package_default.version;
58
+
59
+ // src/logging/logger.ts
60
+ import chalk from "chalk";
61
+ var formatMessage = (emoji, message) => `
62
+ ${emoji} ${chalk.dim("[")}${chalk.bgCyan.black(REPO_NAME)}${chalk.reset.dim("]")} ${message}`;
63
+ var logger = {
64
+ print: console.log,
65
+ info: (message) => console.info(formatMessage("\u{1F916}", chalk.cyan(message))),
66
+ warn: (message) => console.warn(formatMessage("\u26A0\uFE0F", chalk.yellow(message))),
67
+ error: (message) => console.error(formatMessage("\u274C", chalk.red(message)))
68
+ };
69
+
70
+ // src/logging/cards.ts
71
+ import boxen from "boxen";
72
+ import chalk2 from "chalk";
73
+ var cardIntro = boxen(
74
+ chalk2.white(`
75
+ Welcome to ${REPO_NAME} version ${VERSION}!
76
+
77
+ Author: ${AUTHOR_NAME}
78
+ GitHub: ${chalk2.underline(AUTHOR_URL)}
79
+ Email: ${AUTHOR_EMAIL}
80
+ `),
81
+ {
82
+ padding: 1,
83
+ margin: 1,
84
+ borderStyle: "round",
85
+ borderColor: "cyan",
86
+ textAlignment: "center"
87
+ }
88
+ );
89
+ var importQuizUrl = `${TESTOWNIK_URL}/import-quiz`;
90
+ var cardOutro = boxen(
91
+ chalk2.white(`
92
+ Thank you for using ${REPO_NAME}.
93
+
94
+ \u{1F4E5} Import your quiz \u{1F4E5}
95
+
96
+ ${chalk2.underline(importQuizUrl)}
97
+
98
+ \u2B50 Star me on GitHub! \u2B50
99
+
100
+ ${chalk2.underline(REPO_URL)}
101
+ `),
102
+ {
103
+ padding: 1,
104
+ margin: 1,
105
+ borderStyle: "round",
106
+ borderColor: "yellow",
107
+ textAlignment: "center"
108
+ }
109
+ );
110
+ var generateErrorCard = (text) => boxen(chalk2.white(text), {
111
+ padding: 1,
112
+ margin: 1,
113
+ borderStyle: "round",
114
+ borderColor: "red",
115
+ textAlignment: "center"
116
+ });
117
+ var cardError = generateErrorCard(`
118
+
119
+ An unexpected error occurred during the program's execution.
120
+ If the problem persists, please report it on GitHub:
121
+
122
+ ${chalk2.underline(REPO_URL + "/issues/new")}
123
+ `);
124
+
125
+ // src/adapters/base-adapter.ts
126
+ import { parse } from "path";
127
+ import { readFileSync, writeFileSync } from "fs";
128
+ var BaseAdapter = class {
129
+ inputContent;
130
+ options;
131
+ constructor(options) {
132
+ const content = readFileSync(options.inputFilename, "utf8");
133
+ this.inputContent = content;
134
+ this.options = options;
135
+ }
136
+ generateOutputFilename() {
137
+ const inputFile = parse(this.options.inputFilename);
138
+ const subExtension = inputFile.ext === ".json" ? ".converted" : "";
139
+ return `${inputFile.name}${subExtension}.json`;
140
+ }
141
+ createQuiz() {
142
+ const questions = this.convertQuestions();
143
+ if (questions.length === 0) {
144
+ throw new Error("Could not find any questions in the input file.");
145
+ }
146
+ logger.info(
147
+ `Successfully imported ${questions.length} question${questions.length === 1 ? "" : "s"}!`
148
+ );
149
+ const quiz = {
150
+ title: this.options.quizTitle,
151
+ ...this.options.quizDescription == null ? {} : { description: this.options.quizDescription },
152
+ questions
153
+ };
154
+ return quiz;
155
+ }
156
+ writeOutput() {
157
+ const quiz = this.createQuiz();
158
+ const data = JSON.stringify(quiz, null, 2);
159
+ const filename = this.options.outputFilename ?? this.generateOutputFilename();
160
+ writeFileSync(filename, data, "utf8");
161
+ logger.info(`Converted quiz written to ${filename}.`);
162
+ }
163
+ };
164
+
165
+ // src/adapters/ccna-adapter.ts
166
+ var QUESTION_REGEX = /^(?<number>\d+)\.\s+(?<question>[\s\S]+?)\n^-\s+(?<answers>.+(?:\r?\n.+)+)/gm;
167
+ var ANSWER_REGEX = /^(?<correct>\[✓\]\s+)?(?<answer>[\s\S]+)/m;
168
+ var CcnaAdapter = class extends BaseAdapter {
169
+ convertQuestions() {
170
+ const questions = [];
171
+ for (const questionMatch of this.inputContent.matchAll(QUESTION_REGEX)) {
172
+ if (questionMatch.groups == null) {
173
+ continue;
174
+ }
175
+ const questionText = questionMatch.groups.question?.trim();
176
+ if (!questionText) {
177
+ logger.warn("Skipping question with no text");
178
+ continue;
179
+ }
180
+ const answersText = questionMatch.groups.answers?.trim();
181
+ if (!answersText) {
182
+ logger.warn(`Skipping question with no answers: '${questionText}'`);
183
+ continue;
184
+ }
185
+ const answers = [];
186
+ for (const fullAnswerText of answersText.split(/\r?\n-\s+/)) {
187
+ const answerMatch = fullAnswerText.match(ANSWER_REGEX);
188
+ if (answerMatch?.groups == null) {
189
+ logger.warn(`Skiping invalid answer: ${fullAnswerText}`);
190
+ continue;
191
+ }
192
+ const isCorrect = answerMatch?.groups?.correct != null;
193
+ const answerText = answerMatch.groups?.answer?.trim();
194
+ if (!answerText) {
195
+ logger.warn(
196
+ `Skipping answer with no text in question: ${questionText}`
197
+ );
198
+ continue;
199
+ }
200
+ const answer = {
201
+ text: answerText,
202
+ is_correct: isCorrect
203
+ };
204
+ answers.push(answer);
205
+ }
206
+ if (answers.length === 0) {
207
+ continue;
208
+ }
209
+ const question = {
210
+ text: questionText,
211
+ answers,
212
+ multiple: answers.filter((answer) => answer.is_correct).length > 1
213
+ };
214
+ if (questionMatch.groups.number != null) {
215
+ question.order = Number(questionMatch.groups.number);
216
+ }
217
+ questions.push(question);
218
+ }
219
+ return questions;
220
+ }
221
+ };
222
+
223
+ // src/cli/index.ts
224
+ var program = new Command();
225
+ program.name("testownik-converter").description(
226
+ "A command-line converter of quizzes into KN Solvro's Testownik format."
227
+ ).option("-t, --title <string>", "quiz title", "Testownik Quiz").option(
228
+ "-d, --description <string>",
229
+ "quiz description",
230
+ `Generated by ${REPO_NAME}: ${REPO_URL}`
231
+ ).option("-o, --output <filename>", "output JSON filename").version(VERSION);
232
+ program.command("ccna").argument("<filename>", "input text filename").description("Parses questions from Cisco's CCNA exam format.").action((inputFilename) => {
233
+ logger.print(cardIntro);
234
+ try {
235
+ const options = program.opts();
236
+ const outputFilename = options.output;
237
+ const quizTitle = options.title;
238
+ const quizDescription = options.description;
239
+ const adapter = new CcnaAdapter({
240
+ inputFilename,
241
+ outputFilename,
242
+ quizTitle,
243
+ quizDescription
244
+ });
245
+ adapter.writeOutput();
246
+ logger.print(cardOutro);
247
+ } catch (error) {
248
+ logger.error(
249
+ error instanceof Error ? error.message : `Unknown error: ${error}`
250
+ );
251
+ logger.print(cardError);
252
+ process.exitCode = 1;
253
+ }
254
+ });
255
+ program.parse(process.argv);
256
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/index.ts","../package.json","../src/constants/index.ts","../src/logging/logger.ts","../src/logging/cards.ts","../src/adapters/base-adapter.ts","../src/adapters/ccna-adapter.ts"],"sourcesContent":["import { Command } from \"commander\";\n\nimport { CcnaAdapter } from \"@/adapters/ccna-adapter\";\nimport { REPO_NAME, REPO_URL, VERSION } from \"@/constants\";\nimport { cardIntro, cardOutro, cardError, logger } from \"@/logging\";\n\nconst program = new Command();\nprogram\n .name(\"testownik-converter\")\n .description(\n \"A command-line converter of quizzes into KN Solvro's Testownik format.\",\n )\n .option(\"-t, --title <string>\", \"quiz title\", \"Testownik Quiz\")\n .option(\n \"-d, --description <string>\",\n \"quiz description\",\n `Generated by ${REPO_NAME}: ${REPO_URL}`,\n )\n .option(\"-o, --output <filename>\", \"output JSON filename\")\n .version(VERSION);\n\nprogram\n .command(\"ccna\")\n .argument(\"<filename>\", \"input text filename\")\n .description(\"Parses questions from Cisco's CCNA exam format.\")\n .action((inputFilename: string) => {\n logger.print(cardIntro);\n try {\n const options = program.opts();\n\n const outputFilename: string | undefined = options.output;\n const quizTitle: string = options.title;\n const quizDescription: string | undefined = options.description;\n\n const adapter = new CcnaAdapter({\n inputFilename,\n outputFilename,\n quizTitle,\n quizDescription,\n });\n\n adapter.writeOutput();\n\n logger.print(cardOutro);\n } catch (error) {\n logger.error(\n error instanceof Error ? error.message : `Unknown error: ${error}`,\n );\n logger.print(cardError);\n process.exitCode = 1;\n }\n });\n\nprogram.parse(process.argv);\n","{\n \"name\": \"testownik-converter\",\n \"version\": \"1.1.0\",\n \"description\": \"A command-line utility to convert quizzes into Solvro's Testownik format.\",\n \"type\": \"module\",\n \"author\": \"Konrad Guzek\",\n \"license\": \"MIT\",\n \"scripts\": {\n \"build\": \"tsup\"\n },\n \"homepage\": \"https://github.com/kguzek/testownik-converter#readme\",\n \"bugs\": {\n \"url\": \"https://github.com/kguzek/testownik-converter/issues\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/kguzek/testownik-converter.git\"\n },\n \"bin\": {\n \"testownik-converter\": \"bin/index.js\"\n },\n \"files\": [\n \"dist\",\n \"package.json\"\n ],\n \"dependencies\": {\n \"boxen\": \"^8.0.1\",\n \"chalk\": \"^5.6.2\",\n \"commander\": \"^14.0.3\"\n },\n \"devDependencies\": {\n \"@semantic-release/changelog\": \"^6.0.3\",\n \"@semantic-release/git\": \"^10.0.1\",\n \"@types/node\": \"^25.2.0\",\n \"semantic-release\": \"^25.0.3\",\n \"tsup\": \"^8.5.1\",\n \"typescript\": \"^5.9.3\"\n },\n \"publishConfig\": {\n \"provenance\": true\n },\n \"packageManager\": \"pnpm@10.28.2\"\n}\n","export const AUTHOR_EMAIL = \"konrad@guzek.uk\";\nexport const AUTHOR_NAME = \"Konrad Guzek\";\nexport const AUTHOR_URL = \"https://github.com/kguzek\";\nimport pkg from \"../../package.json\";\n\nexport const REPO_NAME = \"Testownik Converter\";\nexport const REPO_SLUG = \"testownik-converter\";\nexport const REPO_URL = `${AUTHOR_URL}/${REPO_SLUG}`;\n\nexport const TESTOWNIK_URL = \"https://testownik.solvro.pl\";\n\nexport const VERSION = pkg.version;\n","import { REPO_NAME } from \"@/constants\";\n\nimport chalk from \"chalk\";\n\nconst formatMessage = (emoji: string, message: string) =>\n `\\n${emoji} ${chalk.dim(\"[\")}${chalk.bgCyan.black(REPO_NAME)}${chalk.reset.dim(\"]\")} ${message}`;\n\nexport const logger = {\n print: console.log,\n info: (message: string) =>\n console.info(formatMessage(\"🤖\", chalk.cyan(message))),\n warn: (message: string) =>\n console.warn(formatMessage(\"⚠️\", chalk.yellow(message))),\n error: (message: string) =>\n console.error(formatMessage(\"❌\", chalk.red(message))),\n};\n","import boxen from \"boxen\";\nimport chalk from \"chalk\";\n\nimport {\n AUTHOR_EMAIL,\n AUTHOR_NAME,\n AUTHOR_URL,\n REPO_NAME,\n REPO_URL,\n TESTOWNIK_URL,\n VERSION,\n} from \"@/constants\";\n\nexport const cardIntro = boxen(\n chalk.white(`\nWelcome to ${REPO_NAME} version ${VERSION}!\n\nAuthor: ${AUTHOR_NAME}\nGitHub: ${chalk.underline(AUTHOR_URL)}\nEmail: ${AUTHOR_EMAIL}\n`),\n {\n padding: 1,\n margin: 1,\n borderStyle: \"round\",\n borderColor: \"cyan\",\n textAlignment: \"center\",\n },\n);\n\nconst importQuizUrl = `${TESTOWNIK_URL}/import-quiz`;\n\nexport const cardOutro = boxen(\n chalk.white(`\nThank you for using ${REPO_NAME}.\n\n📥 Import your quiz 📥\n\n${chalk.underline(importQuizUrl)}\n\n⭐ Star me on GitHub! ⭐\n \n${chalk.underline(REPO_URL)}\n`),\n {\n padding: 1,\n margin: 1,\n borderStyle: \"round\",\n borderColor: \"yellow\",\n textAlignment: \"center\",\n },\n);\n\nconst generateErrorCard = (text: string) =>\n boxen(chalk.white(text), {\n padding: 1,\n margin: 1,\n borderStyle: \"round\",\n borderColor: \"red\",\n textAlignment: \"center\",\n });\n\nexport const cardError = generateErrorCard(`\n\nAn unexpected error occurred during the program's execution.\nIf the problem persists, please report it on GitHub:\n\n${chalk.underline(REPO_URL + \"/issues/new\")}\n`);\n","import { parse } from \"node:path\";\n\nimport { readFileSync, writeFileSync } from \"node:fs\";\n\nimport type { AdapterOptions, TestownikQuestion, TestownikQuiz } from \"@/types\";\nimport { logger } from \"@/logging\";\n\nexport abstract class BaseAdapter {\n protected inputContent!: string;\n protected options!: AdapterOptions;\n\n constructor(options: AdapterOptions) {\n const content = readFileSync(options.inputFilename, \"utf8\");\n\n this.inputContent = content;\n this.options = options;\n }\n\n protected abstract convertQuestions(): TestownikQuestion[];\n\n protected generateOutputFilename(): string {\n const inputFile = parse(this.options.inputFilename);\n const subExtension = inputFile.ext === \".json\" ? \".converted\" : \"\";\n return `${inputFile.name}${subExtension}.json`;\n }\n\n private createQuiz(): TestownikQuiz {\n const questions = this.convertQuestions();\n if (questions.length === 0) {\n throw new Error(\"Could not find any questions in the input file.\");\n }\n logger.info(\n `Successfully imported ${questions.length} question${questions.length === 1 ? \"\" : \"s\"}!`,\n );\n const quiz: TestownikQuiz = {\n title: this.options.quizTitle,\n ...(this.options.quizDescription == null\n ? {}\n : { description: this.options.quizDescription }),\n questions,\n };\n return quiz;\n }\n\n writeOutput(): void {\n const quiz = this.createQuiz();\n const data = JSON.stringify(quiz, null, 2);\n const filename: string =\n this.options.outputFilename ?? this.generateOutputFilename();\n writeFileSync(filename, data, \"utf8\");\n logger.info(`Converted quiz written to ${filename}.`);\n }\n}\n","import type { TestownikQuestion, TestownikAnswer } from \"@/types\";\nimport { logger } from \"@/logging\";\n\nimport { BaseAdapter } from \"./base-adapter\";\n\nconst QUESTION_REGEX =\n /^(?<number>\\d+)\\.\\s+(?<question>[\\s\\S]+?)\\n^-\\s+(?<answers>.+(?:\\r?\\n.+)+)/gm;\n\nconst ANSWER_REGEX = /^(?<correct>\\[✓\\]\\s+)?(?<answer>[\\s\\S]+)/m;\n\nexport class CcnaAdapter extends BaseAdapter {\n protected convertQuestions() {\n const questions: TestownikQuestion[] = [];\n for (const questionMatch of this.inputContent.matchAll(QUESTION_REGEX)) {\n if (questionMatch.groups == null) {\n continue;\n }\n const questionText = questionMatch.groups.question?.trim();\n if (!questionText) {\n logger.warn(\"Skipping question with no text\");\n continue;\n }\n\n const answersText = questionMatch.groups.answers?.trim();\n if (!answersText) {\n logger.warn(`Skipping question with no answers: '${questionText}'`);\n continue;\n }\n\n const answers: TestownikAnswer[] = [];\n for (const fullAnswerText of answersText.split(/\\r?\\n-\\s+/)) {\n const answerMatch = fullAnswerText.match(ANSWER_REGEX);\n if (answerMatch?.groups == null) {\n logger.warn(`Skiping invalid answer: ${fullAnswerText}`);\n continue;\n }\n const isCorrect = answerMatch?.groups?.correct != null;\n const answerText = answerMatch.groups?.answer?.trim();\n if (!answerText) {\n logger.warn(\n `Skipping answer with no text in question: ${questionText}`,\n );\n continue;\n }\n const answer: TestownikAnswer = {\n text: answerText,\n is_correct: isCorrect,\n };\n answers.push(answer);\n }\n if (answers.length === 0) {\n continue;\n }\n const question: TestownikQuestion = {\n text: questionText,\n answers,\n multiple: answers.filter((answer) => answer.is_correct).length > 1,\n };\n if (questionMatch.groups.number != null) {\n question.order = Number(questionMatch.groups.number);\n }\n questions.push(question);\n }\n\n return questions;\n }\n}\n"],"mappings":"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","names":["chalk","chalk"]}
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "testownik-converter",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "A command-line utility to convert quizzes into Solvro's Testownik format.",
5
5
  "type": "module",
6
6
  "author": "Konrad Guzek",
7
7
  "license": "MIT",
8
+ "scripts": {
9
+ "build": "tsup"
10
+ },
8
11
  "homepage": "https://github.com/kguzek/testownik-converter#readme",
9
12
  "bugs": {
10
13
  "url": "https://github.com/kguzek/testownik-converter/issues"
@@ -16,18 +19,25 @@
16
19
  "bin": {
17
20
  "testownik-converter": "bin/index.js"
18
21
  },
19
- "scripts": {
20
- "build": "tsup"
21
- },
22
+ "files": [
23
+ "dist",
24
+ "package.json"
25
+ ],
22
26
  "dependencies": {
23
27
  "boxen": "^8.0.1",
24
28
  "chalk": "^5.6.2",
25
29
  "commander": "^14.0.3"
26
30
  },
27
31
  "devDependencies": {
32
+ "@semantic-release/changelog": "^6.0.3",
33
+ "@semantic-release/git": "^10.0.1",
28
34
  "@types/node": "^25.2.0",
35
+ "semantic-release": "^25.0.3",
29
36
  "tsup": "^8.5.1",
30
37
  "typescript": "^5.9.3"
31
38
  },
39
+ "publishConfig": {
40
+ "provenance": true
41
+ },
32
42
  "packageManager": "pnpm@10.28.2"
33
43
  }
@@ -1,25 +0,0 @@
1
- name: Publish to npm registry
2
-
3
- permissions:
4
- id-token: write # Required for OIDC
5
- contents: read
6
-
7
- on:
8
- push:
9
- branches: ["main"]
10
- workflow_dispatch:
11
-
12
- jobs:
13
- publish:
14
- runs-on: ubuntu-latest
15
- steps:
16
- - uses: actions/checkout@v4
17
- - uses: actions/setup-node@v4
18
- with:
19
- node-version: 22
20
- registry-url: https://registry.npmjs.org/
21
- - uses: pnpm/action-setup@v4
22
- - run: pnpm install --frozen-lockfile
23
- - run: pnpm run build
24
- - run: npm install --global npm@latest
25
- - run: npm publish --provenance
@@ -1,44 +0,0 @@
1
- import { readFileSync, writeFileSync } from "node:fs";
2
-
3
- import type { AdapterOptions, TestownikQuestion, TestownikQuiz } from "@/types";
4
- import { logger } from "@/logging";
5
-
6
- export abstract class BaseAdapter {
7
- protected inputContent!: string;
8
- protected options!: AdapterOptions;
9
-
10
- constructor(options: AdapterOptions) {
11
- const content = readFileSync(options.inputFilename, "utf8");
12
-
13
- this.inputContent = content;
14
- this.options = options;
15
- }
16
-
17
- protected abstract convertQuestions(): TestownikQuestion[];
18
-
19
- private createQuiz(): TestownikQuiz {
20
- const questions = this.convertQuestions();
21
- if (questions.length === 0) {
22
- throw new Error("Could not find any questions in the input file.");
23
- }
24
- logger.info(
25
- `Successfully imported ${questions.length} question${questions.length === 1 ? "" : "s"}!`,
26
- );
27
- const quiz: TestownikQuiz = {
28
- title: this.options.quizTitle,
29
- ...(this.options.quizDescription == null
30
- ? {}
31
- : { description: this.options.quizDescription }),
32
- questions,
33
- };
34
- return quiz;
35
- }
36
-
37
- writeOutput(): void {
38
- const quiz = this.createQuiz();
39
- const data = JSON.stringify(quiz, null, 2);
40
- const filename = this.options.outputFilename;
41
- writeFileSync(filename, data, "utf8");
42
- logger.info(`Converted quiz written to ${filename}.`);
43
- }
44
- }
@@ -1,74 +0,0 @@
1
- import type { TestownikQuestion, TestownikAnswer } from "@/types";
2
-
3
- import { BaseAdapter } from "./base-adapter";
4
- import { logger } from "@/logging";
5
-
6
- const QUESTION_REGEX = new RegExp(
7
- String.raw`^(?:(?<number>\d+)\.\s+)(?<question>[\s\S]+?)$\n(?<answers>(?:^- .+(?:\n(?!- ).+)*\n?)+)`,
8
- "gm",
9
- );
10
-
11
- const ANSWER_REGEX = new RegExp(
12
- String.raw`^- (?<correct>\[✓\] )?(?<answer>.+)$`,
13
- "gm",
14
- );
15
-
16
- export class CcnaAdapter extends BaseAdapter {
17
- convertQuestions() {
18
- QUESTION_REGEX.lastIndex = 0;
19
- const questions: TestownikQuestion[] = [];
20
- let questionMatch: RegExpExecArray | null;
21
- while (
22
- (questionMatch = QUESTION_REGEX.exec(this.inputContent)) != null &&
23
- questionMatch.groups != null
24
- ) {
25
- const answers: TestownikAnswer[] = [];
26
- ANSWER_REGEX.lastIndex = 0;
27
-
28
- const questionText = questionMatch.groups.question?.trim();
29
- if (!questionText) {
30
- logger.warn("Skipping question with no text");
31
- continue;
32
- }
33
-
34
- const answersText = questionMatch.groups.answers;
35
- if (!answersText) {
36
- logger.warn(`Skipping question with no answers: '${questionText}'`);
37
- continue;
38
- }
39
-
40
- let answerMatch: RegExpExecArray | null;
41
- while (
42
- (answerMatch = ANSWER_REGEX.exec(answersText)) != null &&
43
- answerMatch.groups != null
44
- ) {
45
- const answerText = answerMatch.groups.answer?.trim();
46
- if (answerText == null) {
47
- logger.warn(
48
- `Skipping answer with no text in question: ${questionText}`,
49
- );
50
- continue;
51
- }
52
- const answer: TestownikAnswer = {
53
- text: answerText,
54
- is_correct: answerMatch.groups.correct != null,
55
- };
56
- answers.push(answer);
57
- }
58
- if (answers.length === 0) {
59
- continue;
60
- }
61
- const question: TestownikQuestion = {
62
- text: questionText,
63
- answers,
64
- multiple: answers.filter((answer) => answer.is_correct).length > 1,
65
- };
66
- if (questionMatch.groups.number != null) {
67
- question.order = Number(questionMatch.groups.number);
68
- }
69
- questions.push(question);
70
- }
71
-
72
- return questions;
73
- }
74
- }
package/src/cli/index.ts DELETED
@@ -1,54 +0,0 @@
1
- import { Command } from "commander";
2
-
3
- import { CcnaAdapter } from "@/adapters/ccna-adapter";
4
- import { REPO_NAME, REPO_URL, VERSION } from "@/constants";
5
- import { cardIntro, cardOutro, cardError, logger } from "@/logging";
6
-
7
- const program = new Command();
8
- program
9
- .name("testownik-converter")
10
- .description(
11
- "A command-line converter of quizzes into KN Solvro's Testownik format.",
12
- )
13
- .option("-t, --title <string>", "quiz title", "Testownik Quiz")
14
- .option(
15
- "-d, --description <string>",
16
- "quiz description",
17
- `Generated by ${REPO_NAME}: ${REPO_URL}`,
18
- )
19
- .option("-o, --output <filename>", "output JSON filename", "testownik.json")
20
- .version(VERSION);
21
-
22
- program
23
- .command("ccna")
24
- .argument("<filename>", "input text filename")
25
- .description("Parses questions from Cisco's CCNA exam format.")
26
- .action((inputFilename: string) => {
27
- logger.print(cardIntro);
28
- try {
29
- const options = program.opts();
30
-
31
- const outputFilename: string = options.output;
32
- const quizTitle: string = options.title;
33
- const quizDescription: string | undefined = options.description;
34
-
35
- const adapter = new CcnaAdapter({
36
- inputFilename,
37
- outputFilename,
38
- quizTitle,
39
- quizDescription,
40
- });
41
-
42
- adapter.writeOutput();
43
-
44
- logger.print(cardOutro);
45
- } catch (error) {
46
- logger.error(
47
- error instanceof Error ? error.message : `Unknown error: ${error}`,
48
- );
49
- logger.print(cardError);
50
- process.exitCode = 1;
51
- }
52
- });
53
-
54
- program.parse(process.argv);
@@ -1,9 +0,0 @@
1
- export const AUTHOR_EMAIL = "konrad@guzek.uk";
2
- export const AUTHOR_NAME = "Konrad Guzek";
3
- export const AUTHOR_URL = "https://github.com/kguzek";
4
-
5
- export const REPO_NAME = "Testownik Converter";
6
- export const REPO_SLUG = "testownik-converter";
7
- export const REPO_URL = `${AUTHOR_URL}/${REPO_SLUG}`;
8
-
9
- export const VERSION = process.env.npm_package_version || "1.0.1";
@@ -1,62 +0,0 @@
1
- import boxen from "boxen";
2
- import chalk from "chalk";
3
-
4
- import {
5
- AUTHOR_EMAIL,
6
- AUTHOR_NAME,
7
- AUTHOR_URL,
8
- REPO_NAME,
9
- REPO_URL,
10
- VERSION,
11
- } from "@/constants";
12
-
13
- export const cardIntro = boxen(
14
- chalk.white(`
15
- Welcome to ${REPO_NAME} version ${VERSION}!
16
-
17
- Author: ${AUTHOR_NAME}
18
- GitHub: ${chalk.underline(AUTHOR_URL)}
19
- Email: ${AUTHOR_EMAIL}
20
- `),
21
- {
22
- padding: 1,
23
- margin: 1,
24
- borderStyle: "round",
25
- borderColor: "cyan",
26
- textAlignment: "center",
27
- },
28
- );
29
-
30
- export const cardOutro = boxen(
31
- chalk.white(`
32
- Thank you for using ${REPO_NAME}.
33
-
34
- ⭐ Star me on GitHub! ⭐
35
-
36
- ${chalk.underline(REPO_URL)}
37
- `),
38
- {
39
- padding: 1,
40
- margin: 1,
41
- borderStyle: "round",
42
- borderColor: "yellow",
43
- textAlignment: "center",
44
- },
45
- );
46
-
47
- const generateErrorCard = (text: string) =>
48
- boxen(chalk.white(text), {
49
- padding: 1,
50
- margin: 1,
51
- borderStyle: "round",
52
- borderColor: "red",
53
- textAlignment: "center",
54
- });
55
-
56
- export const cardError = generateErrorCard(`
57
-
58
- An unexpected error occurred during the program's execution.
59
- If the problem persists, please report it on GitHub:
60
-
61
- ${chalk.underline(REPO_URL + "/issues/new")}
62
- `);
@@ -1,3 +0,0 @@
1
- export * from "./logger";
2
-
3
- export * from "./cards";
@@ -1,16 +0,0 @@
1
- import { REPO_NAME } from "@/constants";
2
-
3
- import chalk from "chalk";
4
-
5
- const formatMessage = (emoji: string, message: string) =>
6
- `\n${emoji} ${chalk.dim("[")}${chalk.bgCyan.black(REPO_NAME)}${chalk.reset.dim("]")} ${message}`;
7
-
8
- export const logger = {
9
- print: console.log,
10
- info: (message: string) =>
11
- console.info(formatMessage("🤖", chalk.cyan(message))),
12
- warn: (message: string) =>
13
- console.warn(formatMessage("⚠️", chalk.yellow(message))),
14
- error: (message: string) =>
15
- console.error(formatMessage("❌", chalk.red(message))),
16
- };
@@ -1,6 +0,0 @@
1
- export interface AdapterOptions {
2
- inputFilename: string;
3
- outputFilename: string;
4
- quizTitle: string;
5
- quizDescription: string | undefined;
6
- }
@@ -1,3 +0,0 @@
1
- export type * from "./adapters";
2
-
3
- export type * from "./testownik";
@@ -1,21 +0,0 @@
1
- interface QuizPart {
2
- text: string;
3
- order?: number;
4
- image_url?: string;
5
- }
6
-
7
- export interface TestownikAnswer extends QuizPart {
8
- is_correct: boolean;
9
- }
10
-
11
- export interface TestownikQuestion extends QuizPart {
12
- answers: TestownikAnswer[];
13
- multiple?: boolean; // default: false
14
- explanation?: string | null;
15
- }
16
-
17
- export interface TestownikQuiz {
18
- title: string;
19
- description?: string;
20
- questions: TestownikQuestion[];
21
- }
package/tsconfig.json DELETED
@@ -1,46 +0,0 @@
1
- {
2
- // Visit https://aka.ms/tsconfig to read more about this file
3
- "compilerOptions": {
4
- // File Layout
5
- "rootDir": "./src",
6
- "outDir": "./dist",
7
-
8
- // Environment Settings
9
- // See also https://aka.ms/tsconfig/module
10
- "module": "esnext",
11
- "target": "esnext",
12
- "lib": ["esnext"],
13
- "types": ["node"],
14
- "moduleResolution": "bundler",
15
-
16
- // Other Outputs
17
- "sourceMap": true,
18
- "declaration": true,
19
- "declarationMap": true,
20
-
21
- // Stricter Typechecking Options
22
- "noUncheckedIndexedAccess": true,
23
- "exactOptionalPropertyTypes": true,
24
-
25
- // Style Options
26
- // "noImplicitReturns": true,
27
- // "noImplicitOverride": true,
28
- // "noUnusedLocals": true,
29
- // "noUnusedParameters": true,
30
- // "noFallthroughCasesInSwitch": true,
31
- // "noPropertyAccessFromIndexSignature": true,
32
-
33
- // Recommended Options
34
- "strict": true,
35
- "jsx": "react-jsx",
36
- "verbatimModuleSyntax": true,
37
- "isolatedModules": true,
38
- "noUncheckedSideEffectImports": true,
39
- "moduleDetection": "force",
40
- "skipLibCheck": true,
41
-
42
- "paths": {
43
- "@/*": ["./src/*"]
44
- },
45
- }
46
- }
package/tsup.config.ts DELETED
@@ -1,13 +0,0 @@
1
- import { defineConfig } from "tsup";
2
-
3
- export default defineConfig([
4
- {
5
- entry: ["src/cli/index.ts"],
6
- clean: true,
7
- shims: true,
8
- format: "esm",
9
- splitting: false,
10
- sourcemap: true,
11
- outDir: "dist",
12
- },
13
- ]);