testownik-converter 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "../dist/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testownik-converter",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "A command-line utility to convert quizzes into Solvro's Testownik format.",
5
5
  "type": "module",
6
6
  "author": "Konrad Guzek",
@@ -14,9 +14,14 @@
14
14
  "url": "git+https://github.com/kguzek/testownik-converter.git"
15
15
  },
16
16
  "bin": {
17
- "testownik-converter": "node dist/index.js"
17
+ "testownik-converter": "bin/index.js"
18
+ },
19
+ "scripts": {
20
+ "build": "tsup"
18
21
  },
19
22
  "dependencies": {
23
+ "boxen": "^8.0.1",
24
+ "chalk": "^5.6.2",
20
25
  "commander": "^14.0.3"
21
26
  },
22
27
  "devDependencies": {
@@ -24,7 +29,5 @@
24
29
  "tsup": "^8.5.1",
25
30
  "typescript": "^5.9.3"
26
31
  },
27
- "scripts": {
28
- "build": "tsup"
29
- }
30
- }
32
+ "packageManager": "pnpm@10.28.2"
33
+ }
@@ -1,6 +1,8 @@
1
- import type { AdapterOptions, TestownikQuestion, TestownikQuiz } from "@/types";
2
1
  import { readFileSync, writeFileSync } from "node:fs";
3
2
 
3
+ import type { AdapterOptions, TestownikQuestion, TestownikQuiz } from "@/types";
4
+ import { logger } from "@/logging";
5
+
4
6
  export abstract class BaseAdapter {
5
7
  protected inputContent!: string;
6
8
  protected options!: AdapterOptions;
@@ -16,6 +18,12 @@ export abstract class BaseAdapter {
16
18
 
17
19
  private createQuiz(): TestownikQuiz {
18
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
+ );
19
27
  const quiz: TestownikQuiz = {
20
28
  title: this.options.quizTitle,
21
29
  ...(this.options.quizDescription == null
@@ -29,6 +37,8 @@ export abstract class BaseAdapter {
29
37
  writeOutput(): void {
30
38
  const quiz = this.createQuiz();
31
39
  const data = JSON.stringify(quiz, null, 2);
32
- writeFileSync(this.options.outputFilename, data, "utf8");
40
+ const filename = this.options.outputFilename;
41
+ writeFileSync(filename, data, "utf8");
42
+ logger.info(`Converted quiz written to ${filename}.`);
33
43
  }
34
44
  }
@@ -1,13 +1,15 @@
1
- import { BaseAdapter } from "./base-adapter";
2
1
  import type { TestownikQuestion, TestownikAnswer } from "@/types";
3
2
 
3
+ import { BaseAdapter } from "./base-adapter";
4
+ import { logger } from "@/logging";
5
+
4
6
  const QUESTION_REGEX = new RegExp(
5
- String.raw`^(?:(?<number>\d+)\.\s+)?(?<question>.+)$\n(?<answers>(?:(?=^-\s).+$\n?)+\n?)`,
7
+ String.raw`^(?:(?<number>\d+)\.\s+)(?<question>[\s\S]+?)$\n(?<answers>(?:^- .+(?:\n(?!- ).+)*\n?)+)`,
6
8
  "gm",
7
9
  );
8
10
 
9
11
  const ANSWER_REGEX = new RegExp(
10
- String.raw`^- (?<correct>\[✓\] )?(?<question>.+)$`,
12
+ String.raw`^- (?<correct>\[✓\] )?(?<answer>.+)$`,
11
13
  "gm",
12
14
  );
13
15
 
@@ -23,14 +25,32 @@ export class CcnaAdapter extends BaseAdapter {
23
25
  const answers: TestownikAnswer[] = [];
24
26
  ANSWER_REGEX.lastIndex = 0;
25
27
 
26
- const rawAnswers = questionMatch.groups.answers;
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
+
27
40
  let answerMatch: RegExpExecArray | null;
28
41
  while (
29
- (answerMatch = ANSWER_REGEX.exec(rawAnswers)) != null &&
42
+ (answerMatch = ANSWER_REGEX.exec(answersText)) != null &&
30
43
  answerMatch.groups != null
31
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
+ }
32
52
  const answer: TestownikAnswer = {
33
- text: answerMatch.groups.question,
53
+ text: answerText,
34
54
  is_correct: answerMatch.groups.correct != null,
35
55
  };
36
56
  answers.push(answer);
@@ -39,7 +59,7 @@ export class CcnaAdapter extends BaseAdapter {
39
59
  continue;
40
60
  }
41
61
  const question: TestownikQuestion = {
42
- text: questionMatch.groups.question,
62
+ text: questionText,
43
63
  answers,
44
64
  multiple: answers.filter((answer) => answer.is_correct).length > 1,
45
65
  };
package/src/cli/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Command } from "commander";
2
+
2
3
  import { CcnaAdapter } from "@/adapters/ccna-adapter";
3
- import { REPO_NAME, REPO_URL } from "@/constants";
4
+ import { REPO_NAME, REPO_URL, VERSION } from "@/constants";
5
+ import { cardIntro, cardOutro, cardError, logger } from "@/logging";
4
6
 
5
7
  const program = new Command();
6
8
  program
@@ -15,28 +17,38 @@ program
15
17
  `Generated by ${REPO_NAME}: ${REPO_URL}`,
16
18
  )
17
19
  .option("-o, --output <filename>", "output JSON filename", "testownik.json")
18
- .version("1.0.0");
20
+ .version(VERSION);
19
21
 
20
22
  program
21
23
  .command("ccna")
22
24
  .argument("<filename>", "input text filename")
23
25
  .description("Parses questions from Cisco's CCNA exam format.")
24
26
  .action((inputFilename: string) => {
25
- const options = program.opts();
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;
26
34
 
27
- const outputFilename: string = options.output;
28
- const quizTitle: string = options.title;
29
- const quizDescription: string | undefined = options.description;
35
+ const adapter = new CcnaAdapter({
36
+ inputFilename,
37
+ outputFilename,
38
+ quizTitle,
39
+ quizDescription,
40
+ });
30
41
 
31
- const adapter = new CcnaAdapter({
32
- inputFilename,
33
- outputFilename,
34
- quizTitle,
35
- quizDescription,
36
- });
42
+ adapter.writeOutput();
37
43
 
38
- adapter.writeOutput();
39
- console.log(`Quiz written to ${options.output}!`);
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
+ }
40
52
  });
41
53
 
42
54
  program.parse(process.argv);
@@ -1,5 +1,9 @@
1
- export const REPO_NAME = "testownik-converter";
2
-
1
+ export const AUTHOR_EMAIL = "konrad@guzek.uk";
2
+ export const AUTHOR_NAME = "Konrad Guzek";
3
3
  export const AUTHOR_URL = "https://github.com/kguzek";
4
4
 
5
- export const REPO_URL = `${AUTHOR_URL}/${REPO_NAME}`;
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";
@@ -0,0 +1,62 @@
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
+ `);
@@ -0,0 +1,3 @@
1
+ export * from "./logger";
2
+
3
+ export * from "./cards";
@@ -0,0 +1,16 @@
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
+ };
@@ -2,5 +2,5 @@ export interface AdapterOptions {
2
2
  inputFilename: string;
3
3
  outputFilename: string;
4
4
  quizTitle: string;
5
- quizDescription?: string;
5
+ quizDescription: string | undefined;
6
6
  }
@@ -1,3 +1,3 @@
1
- export type { AdapterOptions } from "./adapters";
1
+ export type * from "./adapters";
2
2
 
3
- export type { TestownikQuiz, TestownikQuestion } from "./testownik";
3
+ export type * from "./testownik";
@@ -4,7 +4,7 @@ interface QuizPart {
4
4
  image_url?: string;
5
5
  }
6
6
 
7
- interface TestownikAnswer extends QuizPart {
7
+ export interface TestownikAnswer extends QuizPart {
8
8
  is_correct: boolean;
9
9
  }
10
10