@learnpack/learnpack 5.0.275 → 5.0.276

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 (71) hide show
  1. package/README.md +409 -409
  2. package/lib/commands/audit.js +15 -15
  3. package/lib/commands/breakToken.js +19 -19
  4. package/lib/commands/clean.js +3 -3
  5. package/lib/commands/init.js +41 -41
  6. package/lib/commands/logout.js +3 -3
  7. package/lib/commands/publish.js +5 -10
  8. package/lib/commands/serve.js +3 -2
  9. package/lib/creatorDist/assets/index-BfLyIQVh.js +10343 -10224
  10. package/lib/managers/config/index.js +77 -77
  11. package/lib/utils/api.d.ts +1 -1
  12. package/lib/utils/api.js +12 -9
  13. package/lib/utils/creatorUtilities.js +14 -14
  14. package/package.json +1 -1
  15. package/src/commands/audit.ts +487 -487
  16. package/src/commands/breakToken.ts +67 -67
  17. package/src/commands/clean.ts +30 -30
  18. package/src/commands/init.ts +650 -650
  19. package/src/commands/logout.ts +38 -38
  20. package/src/commands/publish.ts +20 -25
  21. package/src/commands/serve.ts +8 -3
  22. package/src/commands/start.ts +333 -333
  23. package/src/commands/translate.ts +123 -123
  24. package/src/creator/README.md +54 -54
  25. package/src/creator/eslint.config.js +28 -28
  26. package/src/creator/src/components/syllabus/ContentIndex.tsx +312 -312
  27. package/src/creator/src/i18n.ts +28 -28
  28. package/src/creator/src/index.css +217 -217
  29. package/src/creator/src/locales/en.json +126 -126
  30. package/src/creator/src/locales/es.json +126 -126
  31. package/src/creator/src/utils/configTypes.ts +122 -122
  32. package/src/creator/src/utils/constants.ts +13 -13
  33. package/src/creator/src/utils/creatorUtils.ts +46 -46
  34. package/src/creator/src/utils/eventBus.ts +2 -2
  35. package/src/creator/src/utils/lib.ts +468 -468
  36. package/src/creator/src/utils/socket.ts +61 -61
  37. package/src/creator/src/utils/store.ts +222 -222
  38. package/src/creator/src/vite-env.d.ts +1 -1
  39. package/src/creator/vite.config.ts +13 -13
  40. package/src/creatorDist/assets/index-BfLyIQVh.js +10343 -10224
  41. package/src/managers/config/defaults.ts +49 -49
  42. package/src/managers/config/exercise.ts +364 -364
  43. package/src/managers/config/index.ts +775 -775
  44. package/src/managers/file.ts +236 -236
  45. package/src/managers/server/routes.ts +554 -554
  46. package/src/managers/session.ts +182 -182
  47. package/src/managers/telemetry.ts +188 -188
  48. package/src/models/action.ts +13 -13
  49. package/src/models/config-manager.ts +28 -28
  50. package/src/models/config.ts +106 -106
  51. package/src/models/creator.ts +47 -47
  52. package/src/models/exercise-obj.ts +30 -30
  53. package/src/models/session.ts +39 -39
  54. package/src/models/socket.ts +61 -61
  55. package/src/models/status.ts +16 -16
  56. package/src/ui/_app/app.css +1 -1
  57. package/src/ui/_app/app.js +366 -363
  58. package/src/ui/app.tar.gz +0 -0
  59. package/src/utils/BaseCommand.ts +56 -56
  60. package/src/utils/api.ts +53 -39
  61. package/src/utils/audit.ts +392 -392
  62. package/src/utils/checkNotInstalled.ts +267 -267
  63. package/src/utils/configBuilder.ts +82 -82
  64. package/src/utils/convertCreds.js +34 -34
  65. package/src/utils/creatorUtilities.ts +504 -504
  66. package/src/utils/incrementVersion.js +74 -74
  67. package/src/utils/misc.ts +58 -58
  68. package/src/utils/rigoActions.ts +500 -500
  69. package/src/utils/sidebarGenerator.ts +195 -195
  70. package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  71. package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
@@ -1,504 +1,504 @@
1
- import Console from "./console"
2
-
3
- const frontMatter = require("front-matter")
4
- import * as MarkdownIt from "markdown-it"
5
- import { syllable } from "syllable"
6
-
7
- import * as path from "path"
8
- import * as fs from "fs"
9
- import { homedir } from "os"
10
- import { join } from "path"
11
- import { exec } from "child_process"
12
- import { promisify } from "util"
13
-
14
- import * as yaml from "js-yaml"
15
- import * as prompts from "prompts"
16
-
17
- type TEstimateReadingTimeReturns = {
18
- minutes: number
19
- words: number
20
- }
21
-
22
- export const estimateReadingTime = (
23
- text: string,
24
- wordsPerMinute = 150
25
- ): TEstimateReadingTimeReturns => {
26
- const words = text.trim().split(/\s+/).length
27
- const minutes = words / wordsPerMinute
28
-
29
- if (minutes < 1) {
30
- if (words === 0)
31
- return {
32
- minutes: 1,
33
- words,
34
- }
35
- } else {
36
- return {
37
- minutes,
38
- words,
39
- }
40
- }
41
-
42
- return {
43
- minutes: 1,
44
- words,
45
- }
46
- }
47
-
48
- export type PackageInfo = {
49
- grading: string
50
- difficulty: string
51
- duration: number
52
- description: {
53
- us: string
54
- }
55
- title: {
56
- us: string
57
- }
58
- }
59
-
60
- type TFKGLResult = {
61
- text: string
62
- fkgl: number
63
- }
64
-
65
- export function checkReadability(
66
- markdown: string,
67
- wordsPerMinute = 200,
68
- maxMinutes = 1
69
- ): {
70
- newMarkdown: string
71
- exceedsThreshold: boolean
72
- minutes: number
73
- body: string
74
- fkglResult: TFKGLResult
75
- // readingEase: number
76
- } {
77
- const parsed = frontMatter(markdown)
78
-
79
- const fkglResult = fleschKincaidGrade(parsed.body)
80
-
81
- const readingTime = estimateReadingTime(parsed.body, wordsPerMinute)
82
-
83
- // const readingEase = estimateReadingEase(parsed.body)
84
- let attributes = parsed.attributes ? parsed.attributes : {}
85
-
86
- if (typeof parsed.attributes !== "object") {
87
- attributes = {}
88
- }
89
-
90
- const updatedAttributes = {
91
- ...attributes,
92
- readingTime,
93
- fkglResult: fkglResult.fkgl,
94
- }
95
-
96
- let yamlFrontMatter = ""
97
- try {
98
- yamlFrontMatter = yaml.dump(updatedAttributes).trim()
99
- } catch {
100
- Console.error("Error dumping YAML front matter")
101
- return {
102
- newMarkdown: "",
103
- exceedsThreshold: false,
104
- minutes: 0,
105
- body: "",
106
- fkglResult,
107
- // readingEase: 0,
108
- }
109
- }
110
-
111
- const newMarkdown = `---\n${yamlFrontMatter}\n---\n\n${parsed.body}`
112
-
113
- return {
114
- newMarkdown,
115
- exceedsThreshold: readingTime.minutes > maxMinutes,
116
- minutes: readingTime.minutes,
117
- body: parsed.body,
118
- fkglResult,
119
- }
120
- }
121
-
122
- export const slugify = (text: string) => {
123
- return text
124
- .toString()
125
- .normalize("NFD") // Remove accents
126
- .replace(/[\u0300-\u036F]/g, "") // Remove diacritics
127
- .toLowerCase()
128
- .trim()
129
- .replace(/[^\d\s._a-z-]/g, "") // Hyphen at the end, no escape needed
130
- .replace(/\s+/g, "-") // Replace spaces with hyphens
131
- .replace(/-+/g, "-") // Remove duplicate hyphens
132
- .replace(/^-+|-+$/g, "") // Trim hyphens from start/end
133
- }
134
-
135
- export const getExInfo = (title: string) => {
136
- // Example title: '1.0 - Introduction to AI [READ: Small introduction to important concepts such as AI, machine learning, and their applications]'
137
- let [exNumber, exTitle] = title.split(" - ")
138
-
139
- // Extract kind and description
140
- const kindMatch = exTitle.match(/\[(.*?):(.*?)]/)
141
- const kind = kindMatch ? kindMatch[1].trim().toLowerCase() : "read"
142
- const description = kindMatch ? kindMatch[2].trim() : ""
143
-
144
- exNumber = exNumber.trim()
145
- // Clean title
146
- exTitle = exTitle.replace(kindMatch?.[0] || "", "").trim()
147
- exTitle = slugify(exTitle)
148
-
149
- return {
150
- exNumber,
151
- kind,
152
- description,
153
- exTitle,
154
- }
155
- }
156
-
157
- export function extractImagesFromMarkdown(markdown: string) {
158
- const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g
159
- const images = []
160
- let match
161
-
162
- while ((match = imageRegex.exec(markdown)) !== null) {
163
- const altText = match[1]
164
- const url = match[2]
165
- images.push({ alt: altText, url: url })
166
- }
167
-
168
- return images
169
- }
170
-
171
- export function getFilenameFromUrl(url: string) {
172
- return path.basename(url)
173
- }
174
-
175
- export const makePackageInfo = (choices: any) => {
176
- const packageInfo = {
177
- grading: choices.grading,
178
- difficulty: "beginner",
179
- duration: 5,
180
- description: {
181
- us: choices.description,
182
- },
183
- title: {
184
- us: choices.title,
185
- },
186
- slug: choices.title
187
- .toLowerCase()
188
- .replace(/ /g, "-")
189
- .replace(/[^\w-]+/g, ""),
190
- telemetry: {
191
- batch: "https://breathecode.herokuapp.com/v1/assignment/me/telemetry",
192
- },
193
- }
194
- return packageInfo
195
- }
196
-
197
- export function estimateDuration(listOfSteps: string[]): number {
198
- let duration = 0
199
-
200
- for (const step of listOfSteps) {
201
- if (step.includes("[READ:")) {
202
- duration += 1
203
- } else if (step.includes("[QUIZ:")) {
204
- duration += 2
205
- } else if (step.includes("[CODE:")) {
206
- duration += 3
207
- }
208
- }
209
-
210
- return duration
211
- }
212
-
213
- const writeFilePromise = promisify(fs.writeFile)
214
- const execPromise = promisify(exec)
215
-
216
- export async function createFileOnDesktop(fileName: string, content: string) {
217
- try {
218
- const desktopPath = join(homedir(), "Desktop")
219
- const filePath = join(desktopPath, fileName)
220
-
221
- await writeFilePromise(filePath, content)
222
- console.log(`File created successfully at: ${filePath}`)
223
-
224
- await openFile(filePath)
225
- } catch (error) {
226
- console.error("Error:", error)
227
- }
228
- }
229
-
230
- async function openFile(filePath: string) {
231
- const platform = process.platform
232
- let command
233
-
234
- if (platform === "win32") {
235
- command = `start "" "${filePath}"`
236
- } else if (platform === "darwin") {
237
- command = `open "${filePath}"`
238
- } else {
239
- command = `xdg-open "${filePath}"`
240
- }
241
-
242
- try {
243
- await execPromise(command)
244
- console.log("File opened successfully.")
245
- } catch (error) {
246
- console.error("Error opening the file:", error)
247
- }
248
- }
249
-
250
- export function getDesktopFile(fileName: string) {
251
- const desktopPath = join(homedir(), "Desktop")
252
- const filePath = join(desktopPath, fileName)
253
-
254
- const content = fs.readFileSync(filePath, "utf8")
255
- // Delete the file after reading it
256
- // fs.unlinkSync(filePath)
257
- return content
258
- }
259
-
260
- // export function fleschKincaidReadingEase(text: string): number {
261
- // const sentences = text.split(/[.!?]/).filter((s) => s.trim().length > 0)
262
- // const words = text.split(/\s+/).filter((w) => w.trim().length > 0)
263
- // const totalSyllables = words.reduce((sum, word) => sum + syllable(word), 0)
264
-
265
- // const ASL = words.length / sentences.length // Average Sentence Length
266
- // const ASW = totalSyllables / words.length // Average Syllables per Word
267
-
268
- // return Math.round(206.835 - 1.015 * ASL - 84.6 * ASW)
269
- // }
270
-
271
- export function extractTextFromMarkdown(mdContent: string): string {
272
- return mdContent
273
- .replace(/!\[.*?]\(.*?\)/g, "") // Remove images
274
- .replace(/\[.*?]\(.*?\)/g, "") // Remove links
275
- .replace(/(```[\S\s]*?```|`.*?`)/g, "") // Remove inline & block code
276
- .replace(/^#.*$/gm, "") // Remove headings
277
- .replace(/(\*{1,2}|_{1,2})/g, "") // Remove bold/italic markers
278
- .replace(/>\s?/g, "") // Remove blockquotes
279
- .replace(/[*-]\s+/g, "") // Remove bullets from lists
280
- .trim()
281
- }
282
-
283
- const cleanReadme = (readme: string) => {
284
- // Replace <text> and </text> with nothing
285
- return readme.replace(/<text>/g, "").replace(/<\/text>/g, "")
286
- }
287
-
288
- export const saveTranslatedReadme = async (
289
- exercise: string,
290
- languageCode: string,
291
- readme: string
292
- ) => {
293
- const readmePath = path.join(
294
- process.cwd(),
295
- "exercises",
296
- exercise,
297
- `README.${languageCode}.md`
298
- )
299
- fs.writeFileSync(readmePath, cleanReadme(readme))
300
- }
301
-
302
- /**
303
- * Extracts paragraphs from a Markdown string.
304
- * @param markdownText The input Markdown string.
305
- * @returns An array of paragraph contents.
306
- */
307
- export function extractParagraphs(markdownText: string): string[] {
308
- const md = new MarkdownIt()
309
- const tokens = md.parse(markdownText, {})
310
- const paragraphs: string[] = []
311
-
312
- for (let i = 0; i < tokens.length; i++) {
313
- if (
314
- tokens[i].type === "paragraph_open" &&
315
- tokens[i + 1]?.type === "inline"
316
- ) {
317
- paragraphs.push(tokens[i + 1].content)
318
- }
319
- }
320
-
321
- return paragraphs
322
- }
323
-
324
- /**
325
- * Splits a paragraph into words and separates each word into syllables.
326
- * @param paragraph The input paragraph.
327
- * @returns An array of words, each split into syllables.
328
- */
329
- export function splitIntoSyllables(paragraph: string): string[] {
330
- const words = paragraph.split(/\s+/)
331
- const syllables: string[] = []
332
-
333
- for (const word of words) {
334
- syllables.push(...splitWordIntoSyllables(word))
335
- }
336
-
337
- return syllables
338
- }
339
-
340
- /**
341
- * Splits a word into its syllables using a basic estimation.
342
- * @param word The word to split.
343
- * @returns An array of syllables.
344
- */
345
- export function splitWordIntoSyllables(word: string): string[] {
346
- const syllableCount = syllable(word)
347
-
348
- // Simple heuristic: Split word into equal parts (not perfect, better with a dictionary-based approach)
349
- if (syllableCount <= 1) return [word]
350
-
351
- const approxLength = Math.ceil(word.length / syllableCount)
352
- const syllables: string[] = []
353
- for (let i = 0; i < word.length; i += approxLength) {
354
- // eslint-disable-next-line
355
- syllables.push(word.substring(i, i + approxLength))
356
- }
357
-
358
- return syllables
359
- }
360
-
361
- /**
362
- * Extracts words from a given paragraph.
363
- * @param paragraph The input text.
364
- * @returns An array of words.
365
- */
366
- export function extractWords(paragraph: string): string[] {
367
- const words = paragraph.match(/\b\w+\b/g) // Match words using regex
368
- return words ? words : [] // Return words or an empty array if none found
369
- }
370
-
371
- /**
372
- * Calculates the Flesch-Kincaid Grade Level (FKGL) for a given text.
373
- * @param text The input paragraph.
374
- * @returns The FKGL score.
375
- */
376
- export function fleschKincaidGrade(text: string): TFKGLResult {
377
- const processableText = extractTextFromMarkdown(text)
378
- const words = extractWords(processableText)
379
- const numWords = words.length
380
- const numSentences = countSentences(processableText)
381
- const numSyllables = words.reduce((total, word) => total + syllable(word), 0)
382
-
383
- if (numWords === 0 || numSentences === 0) {
384
- return {
385
- text,
386
- fkgl: 0,
387
- }
388
- }
389
-
390
- const fkgl =
391
- // eslint-disable-next-line
392
- 0.39 * (numWords / numSentences) + 11.8 * (numSyllables / numWords) - 15.59
393
-
394
- return {
395
- text,
396
- fkgl: parseFloat(fkgl.toFixed(2)),
397
- }
398
- }
399
-
400
- /**
401
- * Counts the number of sentences in a given text.
402
- * @param text The input paragraph.
403
- * @returns The total number of sentences.
404
- */
405
- export function countSentences(text: string): number {
406
- const sentences = text
407
- .split(/[!.?]+/)
408
- .filter(sentence => sentence.trim().length > 0)
409
- return sentences.length
410
- }
411
-
412
- export function howManyDifficultParagraphs(
413
- paragraphs: TFKGLResult[],
414
- maxFKGL: number
415
- ): number {
416
- return paragraphs.filter(paragraph => paragraph.fkgl > maxFKGL).length
417
- }
418
-
419
- const example_content = `Write or paste your table of content below this line, each topic should be defined on a new line, here is an example:
420
-
421
- Introduction to AI: Explain what is AI and its applications
422
- Introduction to Machine Learning: Explain what is machine learning and its applications
423
- What is an AI Model: Explain what is an AI model and its applications
424
- How to use an AI Model: Different APIs, local models, etc.
425
- How to build an AI Model: Fine-tuning, data collection, cleaning and more.
426
- `
427
-
428
- export const appendContentIndex = async () => {
429
- const choices = await prompts([
430
- {
431
- type: "confirm",
432
- name: "contentIndex",
433
- message: "Do you have a content index for this tutorial?",
434
- },
435
- ])
436
- if (choices.contentIndex) {
437
- await createFileOnDesktop("content_index.txt", example_content)
438
- Console.info(
439
- "Please make the necessary in the recently created file in your desktop, it should automatically open. Edit the file to match your expectations and save it. Keep the same name and structure as the example file. Continue when ready."
440
- )
441
- const isReady = await prompts([
442
- {
443
- type: "confirm",
444
- name: "isReady",
445
- message: "Are you ready to continue?",
446
- },
447
- ])
448
- if (!isReady.isReady) {
449
- Console.error("Please make the necessary changes and try again.")
450
- process.exit(1)
451
- }
452
-
453
- const contentIndex = getDesktopFile("content_index.txt")
454
- return contentIndex
455
- }
456
-
457
- return null
458
- }
459
-
460
- const example_airules = `
461
- Write with an engaging tone, use simple words and avoid complex sentences.
462
- Write in first person, as if you are talking to the reader.
463
- Add mental maps to help the reader understand the content.
464
- Add diagrams to help the reader understand the content.
465
- No code exercises required
466
-
467
- `
468
-
469
- export const appendAIRules = async () => {
470
- const choices = await prompts([
471
- {
472
- type: "confirm",
473
- name: "airules",
474
- message:
475
- "Do you want to add any specific rules for the AI? (e.g. no code exercises, no quizzes, etc, a particular writting style, etc)",
476
- },
477
- ])
478
- if (choices.airules) {
479
- await createFileOnDesktop("airules.txt", example_airules)
480
- Console.info(
481
- "Please make the necessary in the recently created file in your desktop, it should automatically open. Edit the file to match your expectations and save it. Keep the same name and structure as the example file. Continue when ready."
482
- )
483
- const isReady = await prompts([
484
- {
485
- type: "confirm",
486
- name: "isReady",
487
- message: "Are you ready to continue?",
488
- },
489
- ])
490
- if (!isReady.isReady) {
491
- Console.error("Please make the necessary changes and try again.")
492
- process.exit(1)
493
- }
494
-
495
- const airules = getDesktopFile("airules.txt")
496
- return airules
497
- }
498
-
499
- return null
500
- }
501
-
502
- export const getReadmeExtension = (language: string) => {
503
- return language === "en" || language === "us" ? ".md" : `.${language}.md`
504
- }
1
+ import Console from "./console"
2
+
3
+ const frontMatter = require("front-matter")
4
+ import * as MarkdownIt from "markdown-it"
5
+ import { syllable } from "syllable"
6
+
7
+ import * as path from "path"
8
+ import * as fs from "fs"
9
+ import { homedir } from "os"
10
+ import { join } from "path"
11
+ import { exec } from "child_process"
12
+ import { promisify } from "util"
13
+
14
+ import * as yaml from "js-yaml"
15
+ import * as prompts from "prompts"
16
+
17
+ type TEstimateReadingTimeReturns = {
18
+ minutes: number
19
+ words: number
20
+ }
21
+
22
+ export const estimateReadingTime = (
23
+ text: string,
24
+ wordsPerMinute = 150
25
+ ): TEstimateReadingTimeReturns => {
26
+ const words = text.trim().split(/\s+/).length
27
+ const minutes = words / wordsPerMinute
28
+
29
+ if (minutes < 1) {
30
+ if (words === 0)
31
+ return {
32
+ minutes: 1,
33
+ words,
34
+ }
35
+ } else {
36
+ return {
37
+ minutes,
38
+ words,
39
+ }
40
+ }
41
+
42
+ return {
43
+ minutes: 1,
44
+ words,
45
+ }
46
+ }
47
+
48
+ export type PackageInfo = {
49
+ grading: string
50
+ difficulty: string
51
+ duration: number
52
+ description: {
53
+ us: string
54
+ }
55
+ title: {
56
+ us: string
57
+ }
58
+ }
59
+
60
+ type TFKGLResult = {
61
+ text: string
62
+ fkgl: number
63
+ }
64
+
65
+ export function checkReadability(
66
+ markdown: string,
67
+ wordsPerMinute = 200,
68
+ maxMinutes = 1
69
+ ): {
70
+ newMarkdown: string
71
+ exceedsThreshold: boolean
72
+ minutes: number
73
+ body: string
74
+ fkglResult: TFKGLResult
75
+ // readingEase: number
76
+ } {
77
+ const parsed = frontMatter(markdown)
78
+
79
+ const fkglResult = fleschKincaidGrade(parsed.body)
80
+
81
+ const readingTime = estimateReadingTime(parsed.body, wordsPerMinute)
82
+
83
+ // const readingEase = estimateReadingEase(parsed.body)
84
+ let attributes = parsed.attributes ? parsed.attributes : {}
85
+
86
+ if (typeof parsed.attributes !== "object") {
87
+ attributes = {}
88
+ }
89
+
90
+ const updatedAttributes = {
91
+ ...attributes,
92
+ readingTime,
93
+ fkglResult: fkglResult.fkgl,
94
+ }
95
+
96
+ let yamlFrontMatter = ""
97
+ try {
98
+ yamlFrontMatter = yaml.dump(updatedAttributes).trim()
99
+ } catch {
100
+ Console.error("Error dumping YAML front matter")
101
+ return {
102
+ newMarkdown: "",
103
+ exceedsThreshold: false,
104
+ minutes: 0,
105
+ body: "",
106
+ fkglResult,
107
+ // readingEase: 0,
108
+ }
109
+ }
110
+
111
+ const newMarkdown = `---\n${yamlFrontMatter}\n---\n\n${parsed.body}`
112
+
113
+ return {
114
+ newMarkdown,
115
+ exceedsThreshold: readingTime.minutes > maxMinutes,
116
+ minutes: readingTime.minutes,
117
+ body: parsed.body,
118
+ fkglResult,
119
+ }
120
+ }
121
+
122
+ export const slugify = (text: string) => {
123
+ return text
124
+ .toString()
125
+ .normalize("NFD") // Remove accents
126
+ .replace(/[\u0300-\u036F]/g, "") // Remove diacritics
127
+ .toLowerCase()
128
+ .trim()
129
+ .replace(/[^\d\s._a-z-]/g, "") // Hyphen at the end, no escape needed
130
+ .replace(/\s+/g, "-") // Replace spaces with hyphens
131
+ .replace(/-+/g, "-") // Remove duplicate hyphens
132
+ .replace(/^-+|-+$/g, "") // Trim hyphens from start/end
133
+ }
134
+
135
+ export const getExInfo = (title: string) => {
136
+ // Example title: '1.0 - Introduction to AI [READ: Small introduction to important concepts such as AI, machine learning, and their applications]'
137
+ let [exNumber, exTitle] = title.split(" - ")
138
+
139
+ // Extract kind and description
140
+ const kindMatch = exTitle.match(/\[(.*?):(.*?)]/)
141
+ const kind = kindMatch ? kindMatch[1].trim().toLowerCase() : "read"
142
+ const description = kindMatch ? kindMatch[2].trim() : ""
143
+
144
+ exNumber = exNumber.trim()
145
+ // Clean title
146
+ exTitle = exTitle.replace(kindMatch?.[0] || "", "").trim()
147
+ exTitle = slugify(exTitle)
148
+
149
+ return {
150
+ exNumber,
151
+ kind,
152
+ description,
153
+ exTitle,
154
+ }
155
+ }
156
+
157
+ export function extractImagesFromMarkdown(markdown: string) {
158
+ const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g
159
+ const images = []
160
+ let match
161
+
162
+ while ((match = imageRegex.exec(markdown)) !== null) {
163
+ const altText = match[1]
164
+ const url = match[2]
165
+ images.push({ alt: altText, url: url })
166
+ }
167
+
168
+ return images
169
+ }
170
+
171
+ export function getFilenameFromUrl(url: string) {
172
+ return path.basename(url)
173
+ }
174
+
175
+ export const makePackageInfo = (choices: any) => {
176
+ const packageInfo = {
177
+ grading: choices.grading,
178
+ difficulty: "beginner",
179
+ duration: 5,
180
+ description: {
181
+ us: choices.description,
182
+ },
183
+ title: {
184
+ us: choices.title,
185
+ },
186
+ slug: choices.title
187
+ .toLowerCase()
188
+ .replace(/ /g, "-")
189
+ .replace(/[^\w-]+/g, ""),
190
+ telemetry: {
191
+ batch: "https://breathecode.herokuapp.com/v1/assignment/me/telemetry",
192
+ },
193
+ }
194
+ return packageInfo
195
+ }
196
+
197
+ export function estimateDuration(listOfSteps: string[]): number {
198
+ let duration = 0
199
+
200
+ for (const step of listOfSteps) {
201
+ if (step.includes("[READ:")) {
202
+ duration += 1
203
+ } else if (step.includes("[QUIZ:")) {
204
+ duration += 2
205
+ } else if (step.includes("[CODE:")) {
206
+ duration += 3
207
+ }
208
+ }
209
+
210
+ return duration
211
+ }
212
+
213
+ const writeFilePromise = promisify(fs.writeFile)
214
+ const execPromise = promisify(exec)
215
+
216
+ export async function createFileOnDesktop(fileName: string, content: string) {
217
+ try {
218
+ const desktopPath = join(homedir(), "Desktop")
219
+ const filePath = join(desktopPath, fileName)
220
+
221
+ await writeFilePromise(filePath, content)
222
+ console.log(`File created successfully at: ${filePath}`)
223
+
224
+ await openFile(filePath)
225
+ } catch (error) {
226
+ console.error("Error:", error)
227
+ }
228
+ }
229
+
230
+ async function openFile(filePath: string) {
231
+ const platform = process.platform
232
+ let command
233
+
234
+ if (platform === "win32") {
235
+ command = `start "" "${filePath}"`
236
+ } else if (platform === "darwin") {
237
+ command = `open "${filePath}"`
238
+ } else {
239
+ command = `xdg-open "${filePath}"`
240
+ }
241
+
242
+ try {
243
+ await execPromise(command)
244
+ console.log("File opened successfully.")
245
+ } catch (error) {
246
+ console.error("Error opening the file:", error)
247
+ }
248
+ }
249
+
250
+ export function getDesktopFile(fileName: string) {
251
+ const desktopPath = join(homedir(), "Desktop")
252
+ const filePath = join(desktopPath, fileName)
253
+
254
+ const content = fs.readFileSync(filePath, "utf8")
255
+ // Delete the file after reading it
256
+ // fs.unlinkSync(filePath)
257
+ return content
258
+ }
259
+
260
+ // export function fleschKincaidReadingEase(text: string): number {
261
+ // const sentences = text.split(/[.!?]/).filter((s) => s.trim().length > 0)
262
+ // const words = text.split(/\s+/).filter((w) => w.trim().length > 0)
263
+ // const totalSyllables = words.reduce((sum, word) => sum + syllable(word), 0)
264
+
265
+ // const ASL = words.length / sentences.length // Average Sentence Length
266
+ // const ASW = totalSyllables / words.length // Average Syllables per Word
267
+
268
+ // return Math.round(206.835 - 1.015 * ASL - 84.6 * ASW)
269
+ // }
270
+
271
+ export function extractTextFromMarkdown(mdContent: string): string {
272
+ return mdContent
273
+ .replace(/!\[.*?]\(.*?\)/g, "") // Remove images
274
+ .replace(/\[.*?]\(.*?\)/g, "") // Remove links
275
+ .replace(/(```[\S\s]*?```|`.*?`)/g, "") // Remove inline & block code
276
+ .replace(/^#.*$/gm, "") // Remove headings
277
+ .replace(/(\*{1,2}|_{1,2})/g, "") // Remove bold/italic markers
278
+ .replace(/>\s?/g, "") // Remove blockquotes
279
+ .replace(/[*-]\s+/g, "") // Remove bullets from lists
280
+ .trim()
281
+ }
282
+
283
+ const cleanReadme = (readme: string) => {
284
+ // Replace <text> and </text> with nothing
285
+ return readme.replace(/<text>/g, "").replace(/<\/text>/g, "")
286
+ }
287
+
288
+ export const saveTranslatedReadme = async (
289
+ exercise: string,
290
+ languageCode: string,
291
+ readme: string
292
+ ) => {
293
+ const readmePath = path.join(
294
+ process.cwd(),
295
+ "exercises",
296
+ exercise,
297
+ `README.${languageCode}.md`
298
+ )
299
+ fs.writeFileSync(readmePath, cleanReadme(readme))
300
+ }
301
+
302
+ /**
303
+ * Extracts paragraphs from a Markdown string.
304
+ * @param markdownText The input Markdown string.
305
+ * @returns An array of paragraph contents.
306
+ */
307
+ export function extractParagraphs(markdownText: string): string[] {
308
+ const md = new MarkdownIt()
309
+ const tokens = md.parse(markdownText, {})
310
+ const paragraphs: string[] = []
311
+
312
+ for (let i = 0; i < tokens.length; i++) {
313
+ if (
314
+ tokens[i].type === "paragraph_open" &&
315
+ tokens[i + 1]?.type === "inline"
316
+ ) {
317
+ paragraphs.push(tokens[i + 1].content)
318
+ }
319
+ }
320
+
321
+ return paragraphs
322
+ }
323
+
324
+ /**
325
+ * Splits a paragraph into words and separates each word into syllables.
326
+ * @param paragraph The input paragraph.
327
+ * @returns An array of words, each split into syllables.
328
+ */
329
+ export function splitIntoSyllables(paragraph: string): string[] {
330
+ const words = paragraph.split(/\s+/)
331
+ const syllables: string[] = []
332
+
333
+ for (const word of words) {
334
+ syllables.push(...splitWordIntoSyllables(word))
335
+ }
336
+
337
+ return syllables
338
+ }
339
+
340
+ /**
341
+ * Splits a word into its syllables using a basic estimation.
342
+ * @param word The word to split.
343
+ * @returns An array of syllables.
344
+ */
345
+ export function splitWordIntoSyllables(word: string): string[] {
346
+ const syllableCount = syllable(word)
347
+
348
+ // Simple heuristic: Split word into equal parts (not perfect, better with a dictionary-based approach)
349
+ if (syllableCount <= 1) return [word]
350
+
351
+ const approxLength = Math.ceil(word.length / syllableCount)
352
+ const syllables: string[] = []
353
+ for (let i = 0; i < word.length; i += approxLength) {
354
+ // eslint-disable-next-line
355
+ syllables.push(word.substring(i, i + approxLength))
356
+ }
357
+
358
+ return syllables
359
+ }
360
+
361
+ /**
362
+ * Extracts words from a given paragraph.
363
+ * @param paragraph The input text.
364
+ * @returns An array of words.
365
+ */
366
+ export function extractWords(paragraph: string): string[] {
367
+ const words = paragraph.match(/\b\w+\b/g) // Match words using regex
368
+ return words ? words : [] // Return words or an empty array if none found
369
+ }
370
+
371
+ /**
372
+ * Calculates the Flesch-Kincaid Grade Level (FKGL) for a given text.
373
+ * @param text The input paragraph.
374
+ * @returns The FKGL score.
375
+ */
376
+ export function fleschKincaidGrade(text: string): TFKGLResult {
377
+ const processableText = extractTextFromMarkdown(text)
378
+ const words = extractWords(processableText)
379
+ const numWords = words.length
380
+ const numSentences = countSentences(processableText)
381
+ const numSyllables = words.reduce((total, word) => total + syllable(word), 0)
382
+
383
+ if (numWords === 0 || numSentences === 0) {
384
+ return {
385
+ text,
386
+ fkgl: 0,
387
+ }
388
+ }
389
+
390
+ const fkgl =
391
+ // eslint-disable-next-line
392
+ 0.39 * (numWords / numSentences) + 11.8 * (numSyllables / numWords) - 15.59
393
+
394
+ return {
395
+ text,
396
+ fkgl: parseFloat(fkgl.toFixed(2)),
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Counts the number of sentences in a given text.
402
+ * @param text The input paragraph.
403
+ * @returns The total number of sentences.
404
+ */
405
+ export function countSentences(text: string): number {
406
+ const sentences = text
407
+ .split(/[!.?]+/)
408
+ .filter(sentence => sentence.trim().length > 0)
409
+ return sentences.length
410
+ }
411
+
412
+ export function howManyDifficultParagraphs(
413
+ paragraphs: TFKGLResult[],
414
+ maxFKGL: number
415
+ ): number {
416
+ return paragraphs.filter(paragraph => paragraph.fkgl > maxFKGL).length
417
+ }
418
+
419
+ const example_content = `Write or paste your table of content below this line, each topic should be defined on a new line, here is an example:
420
+
421
+ Introduction to AI: Explain what is AI and its applications
422
+ Introduction to Machine Learning: Explain what is machine learning and its applications
423
+ What is an AI Model: Explain what is an AI model and its applications
424
+ How to use an AI Model: Different APIs, local models, etc.
425
+ How to build an AI Model: Fine-tuning, data collection, cleaning and more.
426
+ `
427
+
428
+ export const appendContentIndex = async () => {
429
+ const choices = await prompts([
430
+ {
431
+ type: "confirm",
432
+ name: "contentIndex",
433
+ message: "Do you have a content index for this tutorial?",
434
+ },
435
+ ])
436
+ if (choices.contentIndex) {
437
+ await createFileOnDesktop("content_index.txt", example_content)
438
+ Console.info(
439
+ "Please make the necessary in the recently created file in your desktop, it should automatically open. Edit the file to match your expectations and save it. Keep the same name and structure as the example file. Continue when ready."
440
+ )
441
+ const isReady = await prompts([
442
+ {
443
+ type: "confirm",
444
+ name: "isReady",
445
+ message: "Are you ready to continue?",
446
+ },
447
+ ])
448
+ if (!isReady.isReady) {
449
+ Console.error("Please make the necessary changes and try again.")
450
+ process.exit(1)
451
+ }
452
+
453
+ const contentIndex = getDesktopFile("content_index.txt")
454
+ return contentIndex
455
+ }
456
+
457
+ return null
458
+ }
459
+
460
+ const example_airules = `
461
+ Write with an engaging tone, use simple words and avoid complex sentences.
462
+ Write in first person, as if you are talking to the reader.
463
+ Add mental maps to help the reader understand the content.
464
+ Add diagrams to help the reader understand the content.
465
+ No code exercises required
466
+
467
+ `
468
+
469
+ export const appendAIRules = async () => {
470
+ const choices = await prompts([
471
+ {
472
+ type: "confirm",
473
+ name: "airules",
474
+ message:
475
+ "Do you want to add any specific rules for the AI? (e.g. no code exercises, no quizzes, etc, a particular writting style, etc)",
476
+ },
477
+ ])
478
+ if (choices.airules) {
479
+ await createFileOnDesktop("airules.txt", example_airules)
480
+ Console.info(
481
+ "Please make the necessary in the recently created file in your desktop, it should automatically open. Edit the file to match your expectations and save it. Keep the same name and structure as the example file. Continue when ready."
482
+ )
483
+ const isReady = await prompts([
484
+ {
485
+ type: "confirm",
486
+ name: "isReady",
487
+ message: "Are you ready to continue?",
488
+ },
489
+ ])
490
+ if (!isReady.isReady) {
491
+ Console.error("Please make the necessary changes and try again.")
492
+ process.exit(1)
493
+ }
494
+
495
+ const airules = getDesktopFile("airules.txt")
496
+ return airules
497
+ }
498
+
499
+ return null
500
+ }
501
+
502
+ export const getReadmeExtension = (language: string) => {
503
+ return language === "en" || language === "us" ? ".md" : `.${language}.md`
504
+ }