@learnpack/learnpack 5.0.128 → 5.0.132

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.
@@ -1,499 +1,499 @@
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
- const slugify = (text: string) => {
123
- return text
124
- .toString()
125
- .normalize("NFD")
126
- .replace(/[\u0300-\u036F]/g, "")
127
- .toLowerCase()
128
- .trim()
129
- .replace(/\s+/g, "-")
130
- .replace(/[^\w-]+/g, "")
131
- }
132
-
133
- export const getExInfo = (title: string) => {
134
- // Example title: '1.0 - Introduction to AI [READ: Small introduction to important concepts such as AI, machine learning, and their applications]'
135
- let [exNumber, exTitle] = title.split(" - ")
136
-
137
- // Extract kind and description
138
- const kindMatch = exTitle.match(/\[(.*?):(.*?)]/)
139
- const kind = kindMatch ? kindMatch[1].trim().toLowerCase() : "read"
140
- const description = kindMatch ? kindMatch[2].trim() : ""
141
-
142
- exNumber = exNumber.trim()
143
- // Clean title
144
- exTitle = exTitle.replace(kindMatch?.[0] || "", "").trim()
145
- exTitle = slugify(exTitle)
146
-
147
- return {
148
- exNumber,
149
- kind,
150
- description,
151
- exTitle,
152
- }
153
- }
154
-
155
- export function extractImagesFromMarkdown(markdown: string) {
156
- const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g
157
- const images = []
158
- let match
159
-
160
- while ((match = imageRegex.exec(markdown)) !== null) {
161
- const altText = match[1]
162
- const url = match[2]
163
- images.push({ alt: altText, url: url })
164
- }
165
-
166
- return images
167
- }
168
-
169
- export function getFilenameFromUrl(url: string) {
170
- return path.basename(url)
171
- }
172
-
173
- export const makePackageInfo = (choices: any) => {
174
- const packageInfo = {
175
- grading: choices.grading,
176
- difficulty: "beginner",
177
- duration: 5,
178
- description: {
179
- us: choices.description,
180
- },
181
- title: {
182
- us: choices.title,
183
- },
184
- slug: choices.title
185
- .toLowerCase()
186
- .replace(/ /g, "-")
187
- .replace(/[^\w-]+/g, ""),
188
- telemetry: {
189
- batch: "https://breathecode.herokuapp.com/v1/assignment/me/telemetry",
190
- },
191
- }
192
- return packageInfo
193
- }
194
-
195
- export function estimateDuration(listOfSteps: string[]): number {
196
- let duration = 0
197
-
198
- for (const step of listOfSteps) {
199
- if (step.includes("[READ:")) {
200
- duration += 1
201
- } else if (step.includes("[QUIZ:")) {
202
- duration += 2
203
- } else if (step.includes("[CODE:")) {
204
- duration += 3
205
- }
206
- }
207
-
208
- return duration
209
- }
210
-
211
- const writeFilePromise = promisify(fs.writeFile)
212
- const execPromise = promisify(exec)
213
-
214
- export async function createFileOnDesktop(fileName: string, content: string) {
215
- try {
216
- const desktopPath = join(homedir(), "Desktop")
217
- const filePath = join(desktopPath, fileName)
218
-
219
- await writeFilePromise(filePath, content)
220
- console.log(`File created successfully at: ${filePath}`)
221
-
222
- await openFile(filePath)
223
- } catch (error) {
224
- console.error("Error:", error)
225
- }
226
- }
227
-
228
- async function openFile(filePath: string) {
229
- const platform = process.platform
230
- let command
231
-
232
- if (platform === "win32") {
233
- command = `start "" "${filePath}"`
234
- } else if (platform === "darwin") {
235
- command = `open "${filePath}"`
236
- } else {
237
- command = `xdg-open "${filePath}"`
238
- }
239
-
240
- try {
241
- await execPromise(command)
242
- console.log("File opened successfully.")
243
- } catch (error) {
244
- console.error("Error opening the file:", error)
245
- }
246
- }
247
-
248
- export function getDesktopFile(fileName: string) {
249
- const desktopPath = join(homedir(), "Desktop")
250
- const filePath = join(desktopPath, fileName)
251
-
252
- const content = fs.readFileSync(filePath, "utf8")
253
- // Delete the file after reading it
254
- // fs.unlinkSync(filePath)
255
- return content
256
- }
257
-
258
- // export function fleschKincaidReadingEase(text: string): number {
259
- // const sentences = text.split(/[.!?]/).filter((s) => s.trim().length > 0)
260
- // const words = text.split(/\s+/).filter((w) => w.trim().length > 0)
261
- // const totalSyllables = words.reduce((sum, word) => sum + syllable(word), 0)
262
-
263
- // const ASL = words.length / sentences.length // Average Sentence Length
264
- // const ASW = totalSyllables / words.length // Average Syllables per Word
265
-
266
- // return Math.round(206.835 - 1.015 * ASL - 84.6 * ASW)
267
- // }
268
-
269
- export function extractTextFromMarkdown(mdContent: string): string {
270
- return mdContent
271
- .replace(/!\[.*?]\(.*?\)/g, "") // Remove images
272
- .replace(/\[.*?]\(.*?\)/g, "") // Remove links
273
- .replace(/(```[\S\s]*?```|`.*?`)/g, "") // Remove inline & block code
274
- .replace(/^#.*$/gm, "") // Remove headings
275
- .replace(/(\*{1,2}|_{1,2})/g, "") // Remove bold/italic markers
276
- .replace(/>\s?/g, "") // Remove blockquotes
277
- .replace(/[*-]\s+/g, "") // Remove bullets from lists
278
- .trim()
279
- }
280
-
281
- const cleanReadme = (readme: string) => {
282
- // Replace <text> and </text> with nothing
283
- return readme.replace(/<text>/g, "").replace(/<\/text>/g, "")
284
- }
285
-
286
- export const saveTranslatedReadme = async (
287
- exercise: string,
288
- languageCode: string,
289
- readme: string
290
- ) => {
291
- const readmePath = path.join(
292
- process.cwd(),
293
- "exercises",
294
- exercise,
295
- `README.${languageCode}.md`
296
- )
297
- fs.writeFileSync(readmePath, cleanReadme(readme))
298
- }
299
-
300
- /**
301
- * Extracts paragraphs from a Markdown string.
302
- * @param markdownText The input Markdown string.
303
- * @returns An array of paragraph contents.
304
- */
305
- export function extractParagraphs(markdownText: string): string[] {
306
- const md = new MarkdownIt()
307
- const tokens = md.parse(markdownText, {})
308
- const paragraphs: string[] = []
309
-
310
- for (let i = 0; i < tokens.length; i++) {
311
- if (
312
- tokens[i].type === "paragraph_open" &&
313
- tokens[i + 1]?.type === "inline"
314
- ) {
315
- paragraphs.push(tokens[i + 1].content)
316
- }
317
- }
318
-
319
- return paragraphs
320
- }
321
-
322
- /**
323
- * Splits a paragraph into words and separates each word into syllables.
324
- * @param paragraph The input paragraph.
325
- * @returns An array of words, each split into syllables.
326
- */
327
- export function splitIntoSyllables(paragraph: string): string[] {
328
- const words = paragraph.split(/\s+/)
329
- const syllables: string[] = []
330
-
331
- for (const word of words) {
332
- syllables.push(...splitWordIntoSyllables(word))
333
- }
334
-
335
- return syllables
336
- }
337
-
338
- /**
339
- * Splits a word into its syllables using a basic estimation.
340
- * @param word The word to split.
341
- * @returns An array of syllables.
342
- */
343
- export function splitWordIntoSyllables(word: string): string[] {
344
- const syllableCount = syllable(word)
345
-
346
- // Simple heuristic: Split word into equal parts (not perfect, better with a dictionary-based approach)
347
- if (syllableCount <= 1)
348
- return [word]
349
-
350
- const approxLength = Math.ceil(word.length / syllableCount)
351
- const syllables: string[] = []
352
- for (let i = 0; i < word.length; i += approxLength) {
353
- // eslint-disable-next-line
354
- syllables.push(word.substring(i, i + approxLength))
355
- }
356
-
357
- return syllables
358
- }
359
-
360
- /**
361
- * Extracts words from a given paragraph.
362
- * @param paragraph The input text.
363
- * @returns An array of words.
364
- */
365
- export function extractWords(paragraph: string): string[] {
366
- const words = paragraph.match(/\b\w+\b/g) // Match words using regex
367
- return words ? words : [] // Return words or an empty array if none found
368
- }
369
-
370
- /**
371
- * Calculates the Flesch-Kincaid Grade Level (FKGL) for a given text.
372
- * @param text The input paragraph.
373
- * @returns The FKGL score.
374
- */
375
- export function fleschKincaidGrade(text: string): TFKGLResult {
376
- const processableText = extractTextFromMarkdown(text)
377
- const words = extractWords(processableText)
378
- const numWords = words.length
379
- const numSentences = countSentences(processableText)
380
- const numSyllables = words.reduce((total, word) => total + syllable(word), 0)
381
-
382
- if (numWords === 0 || numSentences === 0) {
383
- return {
384
- text,
385
- fkgl: 0,
386
- }
387
- }
388
-
389
- const fkgl =
390
- // eslint-disable-next-line
391
- 0.39 * (numWords / numSentences) + 11.8 * (numSyllables / numWords) - 15.59
392
-
393
- return {
394
- text,
395
- fkgl: parseFloat(fkgl.toFixed(2)),
396
- }
397
- }
398
-
399
- /**
400
- * Counts the number of sentences in a given text.
401
- * @param text The input paragraph.
402
- * @returns The total number of sentences.
403
- */
404
- export function countSentences(text: string): number {
405
- const sentences = text
406
- .split(/[!.?]+/)
407
- .filter(sentence => sentence.trim().length > 0)
408
- return sentences.length
409
- }
410
-
411
- export function howManyDifficultParagraphs(
412
- paragraphs: TFKGLResult[],
413
- maxFKGL: number
414
- ): number {
415
- return paragraphs.filter(paragraph => paragraph.fkgl > maxFKGL).length
416
- }
417
-
418
- 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:
419
-
420
- Introduction to AI: Explain what is AI and its applications
421
- Introduction to Machine Learning: Explain what is machine learning and its applications
422
- What is an AI Model: Explain what is an AI model and its applications
423
- How to use an AI Model: Different APIs, local models, etc.
424
- How to build an AI Model: Fine-tuning, data collection, cleaning and more.
425
- `
426
-
427
- export const appendContentIndex = async () => {
428
- const choices = await prompts([
429
- {
430
- type: "confirm",
431
- name: "contentIndex",
432
- message: "Do you have a content index for this tutorial?",
433
- },
434
- ])
435
- if (choices.contentIndex) {
436
- await createFileOnDesktop("content_index.txt", example_content)
437
- Console.info(
438
- "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."
439
- )
440
- const isReady = await prompts([
441
- {
442
- type: "confirm",
443
- name: "isReady",
444
- message: "Are you ready to continue?",
445
- },
446
- ])
447
- if (!isReady.isReady) {
448
- Console.error("Please make the necessary changes and try again.")
449
- process.exit(1)
450
- }
451
-
452
- const contentIndex = getDesktopFile("content_index.txt")
453
- return contentIndex
454
- }
455
-
456
- return null
457
- }
458
-
459
- const example_airules = `
460
- Write with an engaging tone, use simple words and avoid complex sentences.
461
- Write in first person, as if you are talking to the reader.
462
- Add mental maps to help the reader understand the content.
463
- Add diagrams to help the reader understand the content.
464
- No code exercises required
465
-
466
- `
467
-
468
- export const appendAIRules = async () => {
469
- const choices = await prompts([
470
- {
471
- type: "confirm",
472
- name: "airules",
473
- message:
474
- "Do you want to add any specific rules for the AI? (e.g. no code exercises, no quizzes, etc, a particular writting style, etc)",
475
- },
476
- ])
477
- if (choices.airules) {
478
- await createFileOnDesktop("airules.txt", example_airules)
479
- Console.info(
480
- "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."
481
- )
482
- const isReady = await prompts([
483
- {
484
- type: "confirm",
485
- name: "isReady",
486
- message: "Are you ready to continue?",
487
- },
488
- ])
489
- if (!isReady.isReady) {
490
- Console.error("Please make the necessary changes and try again.")
491
- process.exit(1)
492
- }
493
-
494
- const airules = getDesktopFile("airules.txt")
495
- return airules
496
- }
497
-
498
- return null
499
- }
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")
126
+ .replace(/[\u0300-\u036F]/g, "")
127
+ .toLowerCase()
128
+ .trim()
129
+ .replace(/\s+/g, "-")
130
+ .replace(/[^\w-]+/g, "")
131
+ }
132
+
133
+ export const getExInfo = (title: string) => {
134
+ // Example title: '1.0 - Introduction to AI [READ: Small introduction to important concepts such as AI, machine learning, and their applications]'
135
+ let [exNumber, exTitle] = title.split(" - ")
136
+
137
+ // Extract kind and description
138
+ const kindMatch = exTitle.match(/\[(.*?):(.*?)]/)
139
+ const kind = kindMatch ? kindMatch[1].trim().toLowerCase() : "read"
140
+ const description = kindMatch ? kindMatch[2].trim() : ""
141
+
142
+ exNumber = exNumber.trim()
143
+ // Clean title
144
+ exTitle = exTitle.replace(kindMatch?.[0] || "", "").trim()
145
+ exTitle = slugify(exTitle)
146
+
147
+ return {
148
+ exNumber,
149
+ kind,
150
+ description,
151
+ exTitle,
152
+ }
153
+ }
154
+
155
+ export function extractImagesFromMarkdown(markdown: string) {
156
+ const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g
157
+ const images = []
158
+ let match
159
+
160
+ while ((match = imageRegex.exec(markdown)) !== null) {
161
+ const altText = match[1]
162
+ const url = match[2]
163
+ images.push({ alt: altText, url: url })
164
+ }
165
+
166
+ return images
167
+ }
168
+
169
+ export function getFilenameFromUrl(url: string) {
170
+ return path.basename(url)
171
+ }
172
+
173
+ export const makePackageInfo = (choices: any) => {
174
+ const packageInfo = {
175
+ grading: choices.grading,
176
+ difficulty: "beginner",
177
+ duration: 5,
178
+ description: {
179
+ us: choices.description,
180
+ },
181
+ title: {
182
+ us: choices.title,
183
+ },
184
+ slug: choices.title
185
+ .toLowerCase()
186
+ .replace(/ /g, "-")
187
+ .replace(/[^\w-]+/g, ""),
188
+ telemetry: {
189
+ batch: "https://breathecode.herokuapp.com/v1/assignment/me/telemetry",
190
+ },
191
+ }
192
+ return packageInfo
193
+ }
194
+
195
+ export function estimateDuration(listOfSteps: string[]): number {
196
+ let duration = 0
197
+
198
+ for (const step of listOfSteps) {
199
+ if (step.includes("[READ:")) {
200
+ duration += 1
201
+ } else if (step.includes("[QUIZ:")) {
202
+ duration += 2
203
+ } else if (step.includes("[CODE:")) {
204
+ duration += 3
205
+ }
206
+ }
207
+
208
+ return duration
209
+ }
210
+
211
+ const writeFilePromise = promisify(fs.writeFile)
212
+ const execPromise = promisify(exec)
213
+
214
+ export async function createFileOnDesktop(fileName: string, content: string) {
215
+ try {
216
+ const desktopPath = join(homedir(), "Desktop")
217
+ const filePath = join(desktopPath, fileName)
218
+
219
+ await writeFilePromise(filePath, content)
220
+ console.log(`File created successfully at: ${filePath}`)
221
+
222
+ await openFile(filePath)
223
+ } catch (error) {
224
+ console.error("Error:", error)
225
+ }
226
+ }
227
+
228
+ async function openFile(filePath: string) {
229
+ const platform = process.platform
230
+ let command
231
+
232
+ if (platform === "win32") {
233
+ command = `start "" "${filePath}"`
234
+ } else if (platform === "darwin") {
235
+ command = `open "${filePath}"`
236
+ } else {
237
+ command = `xdg-open "${filePath}"`
238
+ }
239
+
240
+ try {
241
+ await execPromise(command)
242
+ console.log("File opened successfully.")
243
+ } catch (error) {
244
+ console.error("Error opening the file:", error)
245
+ }
246
+ }
247
+
248
+ export function getDesktopFile(fileName: string) {
249
+ const desktopPath = join(homedir(), "Desktop")
250
+ const filePath = join(desktopPath, fileName)
251
+
252
+ const content = fs.readFileSync(filePath, "utf8")
253
+ // Delete the file after reading it
254
+ // fs.unlinkSync(filePath)
255
+ return content
256
+ }
257
+
258
+ // export function fleschKincaidReadingEase(text: string): number {
259
+ // const sentences = text.split(/[.!?]/).filter((s) => s.trim().length > 0)
260
+ // const words = text.split(/\s+/).filter((w) => w.trim().length > 0)
261
+ // const totalSyllables = words.reduce((sum, word) => sum + syllable(word), 0)
262
+
263
+ // const ASL = words.length / sentences.length // Average Sentence Length
264
+ // const ASW = totalSyllables / words.length // Average Syllables per Word
265
+
266
+ // return Math.round(206.835 - 1.015 * ASL - 84.6 * ASW)
267
+ // }
268
+
269
+ export function extractTextFromMarkdown(mdContent: string): string {
270
+ return mdContent
271
+ .replace(/!\[.*?]\(.*?\)/g, "") // Remove images
272
+ .replace(/\[.*?]\(.*?\)/g, "") // Remove links
273
+ .replace(/(```[\S\s]*?```|`.*?`)/g, "") // Remove inline & block code
274
+ .replace(/^#.*$/gm, "") // Remove headings
275
+ .replace(/(\*{1,2}|_{1,2})/g, "") // Remove bold/italic markers
276
+ .replace(/>\s?/g, "") // Remove blockquotes
277
+ .replace(/[*-]\s+/g, "") // Remove bullets from lists
278
+ .trim()
279
+ }
280
+
281
+ const cleanReadme = (readme: string) => {
282
+ // Replace <text> and </text> with nothing
283
+ return readme.replace(/<text>/g, "").replace(/<\/text>/g, "")
284
+ }
285
+
286
+ export const saveTranslatedReadme = async (
287
+ exercise: string,
288
+ languageCode: string,
289
+ readme: string
290
+ ) => {
291
+ const readmePath = path.join(
292
+ process.cwd(),
293
+ "exercises",
294
+ exercise,
295
+ `README.${languageCode}.md`
296
+ )
297
+ fs.writeFileSync(readmePath, cleanReadme(readme))
298
+ }
299
+
300
+ /**
301
+ * Extracts paragraphs from a Markdown string.
302
+ * @param markdownText The input Markdown string.
303
+ * @returns An array of paragraph contents.
304
+ */
305
+ export function extractParagraphs(markdownText: string): string[] {
306
+ const md = new MarkdownIt()
307
+ const tokens = md.parse(markdownText, {})
308
+ const paragraphs: string[] = []
309
+
310
+ for (let i = 0; i < tokens.length; i++) {
311
+ if (
312
+ tokens[i].type === "paragraph_open" &&
313
+ tokens[i + 1]?.type === "inline"
314
+ ) {
315
+ paragraphs.push(tokens[i + 1].content)
316
+ }
317
+ }
318
+
319
+ return paragraphs
320
+ }
321
+
322
+ /**
323
+ * Splits a paragraph into words and separates each word into syllables.
324
+ * @param paragraph The input paragraph.
325
+ * @returns An array of words, each split into syllables.
326
+ */
327
+ export function splitIntoSyllables(paragraph: string): string[] {
328
+ const words = paragraph.split(/\s+/)
329
+ const syllables: string[] = []
330
+
331
+ for (const word of words) {
332
+ syllables.push(...splitWordIntoSyllables(word))
333
+ }
334
+
335
+ return syllables
336
+ }
337
+
338
+ /**
339
+ * Splits a word into its syllables using a basic estimation.
340
+ * @param word The word to split.
341
+ * @returns An array of syllables.
342
+ */
343
+ export function splitWordIntoSyllables(word: string): string[] {
344
+ const syllableCount = syllable(word)
345
+
346
+ // Simple heuristic: Split word into equal parts (not perfect, better with a dictionary-based approach)
347
+ if (syllableCount <= 1)
348
+ return [word]
349
+
350
+ const approxLength = Math.ceil(word.length / syllableCount)
351
+ const syllables: string[] = []
352
+ for (let i = 0; i < word.length; i += approxLength) {
353
+ // eslint-disable-next-line
354
+ syllables.push(word.substring(i, i + approxLength))
355
+ }
356
+
357
+ return syllables
358
+ }
359
+
360
+ /**
361
+ * Extracts words from a given paragraph.
362
+ * @param paragraph The input text.
363
+ * @returns An array of words.
364
+ */
365
+ export function extractWords(paragraph: string): string[] {
366
+ const words = paragraph.match(/\b\w+\b/g) // Match words using regex
367
+ return words ? words : [] // Return words or an empty array if none found
368
+ }
369
+
370
+ /**
371
+ * Calculates the Flesch-Kincaid Grade Level (FKGL) for a given text.
372
+ * @param text The input paragraph.
373
+ * @returns The FKGL score.
374
+ */
375
+ export function fleschKincaidGrade(text: string): TFKGLResult {
376
+ const processableText = extractTextFromMarkdown(text)
377
+ const words = extractWords(processableText)
378
+ const numWords = words.length
379
+ const numSentences = countSentences(processableText)
380
+ const numSyllables = words.reduce((total, word) => total + syllable(word), 0)
381
+
382
+ if (numWords === 0 || numSentences === 0) {
383
+ return {
384
+ text,
385
+ fkgl: 0,
386
+ }
387
+ }
388
+
389
+ const fkgl =
390
+ // eslint-disable-next-line
391
+ 0.39 * (numWords / numSentences) + 11.8 * (numSyllables / numWords) - 15.59
392
+
393
+ return {
394
+ text,
395
+ fkgl: parseFloat(fkgl.toFixed(2)),
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Counts the number of sentences in a given text.
401
+ * @param text The input paragraph.
402
+ * @returns The total number of sentences.
403
+ */
404
+ export function countSentences(text: string): number {
405
+ const sentences = text
406
+ .split(/[!.?]+/)
407
+ .filter(sentence => sentence.trim().length > 0)
408
+ return sentences.length
409
+ }
410
+
411
+ export function howManyDifficultParagraphs(
412
+ paragraphs: TFKGLResult[],
413
+ maxFKGL: number
414
+ ): number {
415
+ return paragraphs.filter(paragraph => paragraph.fkgl > maxFKGL).length
416
+ }
417
+
418
+ 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:
419
+
420
+ Introduction to AI: Explain what is AI and its applications
421
+ Introduction to Machine Learning: Explain what is machine learning and its applications
422
+ What is an AI Model: Explain what is an AI model and its applications
423
+ How to use an AI Model: Different APIs, local models, etc.
424
+ How to build an AI Model: Fine-tuning, data collection, cleaning and more.
425
+ `
426
+
427
+ export const appendContentIndex = async () => {
428
+ const choices = await prompts([
429
+ {
430
+ type: "confirm",
431
+ name: "contentIndex",
432
+ message: "Do you have a content index for this tutorial?",
433
+ },
434
+ ])
435
+ if (choices.contentIndex) {
436
+ await createFileOnDesktop("content_index.txt", example_content)
437
+ Console.info(
438
+ "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."
439
+ )
440
+ const isReady = await prompts([
441
+ {
442
+ type: "confirm",
443
+ name: "isReady",
444
+ message: "Are you ready to continue?",
445
+ },
446
+ ])
447
+ if (!isReady.isReady) {
448
+ Console.error("Please make the necessary changes and try again.")
449
+ process.exit(1)
450
+ }
451
+
452
+ const contentIndex = getDesktopFile("content_index.txt")
453
+ return contentIndex
454
+ }
455
+
456
+ return null
457
+ }
458
+
459
+ const example_airules = `
460
+ Write with an engaging tone, use simple words and avoid complex sentences.
461
+ Write in first person, as if you are talking to the reader.
462
+ Add mental maps to help the reader understand the content.
463
+ Add diagrams to help the reader understand the content.
464
+ No code exercises required
465
+
466
+ `
467
+
468
+ export const appendAIRules = async () => {
469
+ const choices = await prompts([
470
+ {
471
+ type: "confirm",
472
+ name: "airules",
473
+ message:
474
+ "Do you want to add any specific rules for the AI? (e.g. no code exercises, no quizzes, etc, a particular writting style, etc)",
475
+ },
476
+ ])
477
+ if (choices.airules) {
478
+ await createFileOnDesktop("airules.txt", example_airules)
479
+ Console.info(
480
+ "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."
481
+ )
482
+ const isReady = await prompts([
483
+ {
484
+ type: "confirm",
485
+ name: "isReady",
486
+ message: "Are you ready to continue?",
487
+ },
488
+ ])
489
+ if (!isReady.isReady) {
490
+ Console.error("Please make the necessary changes and try again.")
491
+ process.exit(1)
492
+ }
493
+
494
+ const airules = getDesktopFile("airules.txt")
495
+ return airules
496
+ }
497
+
498
+ return null
499
+ }