@learnpack/learnpack 5.0.312 → 5.0.315

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