@learnpack/learnpack 5.0.33 → 5.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -12
- package/lib/commands/breakToken.js +36 -8
- package/lib/commands/init.js +49 -37
- package/lib/managers/config/exercise.js +16 -0
- package/lib/managers/server/routes.js +21 -0
- package/lib/models/exercise-obj.d.ts +1 -0
- package/lib/utils/creatorUtilities.d.ts +43 -1
- package/lib/utils/creatorUtilities.js +123 -22
- package/lib/utils/rigoActions.d.ts +3 -1
- package/lib/utils/rigoActions.js +2 -2
- package/oclif.manifest.json +1 -1
- package/package.json +4 -1
- package/src/commands/breakToken.ts +67 -36
- package/src/commands/init.ts +100 -65
- package/src/managers/config/exercise.ts +18 -1
- package/src/managers/server/routes.ts +26 -0
- package/src/models/exercise-obj.ts +30 -29
- package/src/utils/creatorUtilities.ts +143 -23
- package/src/utils/rigoActions.ts +3 -1
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "@learnpack/learnpack",
|
3
3
|
"description": "Seamlessly build, sell and/or take interactive & auto-graded tutorials, start learning now or build a new tutorial to your audience.",
|
4
|
-
"version": "5.0.
|
4
|
+
"version": "5.0.35",
|
5
5
|
"author": "Alejandro Sanchez @alesanchezr",
|
6
6
|
"contributors": [
|
7
7
|
{
|
@@ -42,6 +42,7 @@
|
|
42
42
|
"express": "^4.17.1",
|
43
43
|
"front-matter": "^4.0.2",
|
44
44
|
"js-yaml": "^4.1.0",
|
45
|
+
"markdown-it": "^14.1.0",
|
45
46
|
"moment": "^2.27.0",
|
46
47
|
"node-emoji": "^1.10.0",
|
47
48
|
"node-fetch": "^2.7.0",
|
@@ -49,6 +50,7 @@
|
|
49
50
|
"prompts": "^2.3.2",
|
50
51
|
"shelljs": "^0.8.4",
|
51
52
|
"socket.io": "^4.4.1",
|
53
|
+
"syllable": "^5.0.1",
|
52
54
|
"targz": "^1.0.1",
|
53
55
|
"text-readability": "^1.1.0",
|
54
56
|
"tslib": "^1",
|
@@ -63,6 +65,7 @@
|
|
63
65
|
"@types/express": "^4.17.13",
|
64
66
|
"@types/fs-extra": "^8.1.0",
|
65
67
|
"@types/js-yaml": "^4.0.9",
|
68
|
+
"@types/markdown-it": "^14.1.2",
|
66
69
|
"@types/mocha": "^5",
|
67
70
|
"@types/mock-fs": "^4.13.1",
|
68
71
|
"@types/node": "^22.13.5",
|
@@ -1,36 +1,67 @@
|
|
1
|
-
import { flags } from "@oclif/command"
|
2
|
-
import BaseCommand from "../utils/BaseCommand"
|
3
|
-
|
4
|
-
import
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
1
|
+
import { flags } from "@oclif/command"
|
2
|
+
import BaseCommand from "../utils/BaseCommand"
|
3
|
+
|
4
|
+
import Console from "../utils/console"
|
5
|
+
|
6
|
+
import {
|
7
|
+
extractParagraphs,
|
8
|
+
splitIntoSyllables,
|
9
|
+
extractWords,
|
10
|
+
countSentences,
|
11
|
+
fleschKincaidGrade,
|
12
|
+
} from "../utils/creatorUtilities"
|
13
|
+
|
14
|
+
const exampleMd = `# How to Install Node.js
|
15
|
+
|
16
|
+
Node.js lets you run JavaScript outside a web browser.
|
17
|
+
|
18
|
+
## Step 1: Download Node.js
|
19
|
+
|
20
|
+
Get the Node.js installer from the [official site](https://nodejs.org/en/download/).
|
21
|
+
|
22
|
+
## Step 2: Install Node.js
|
23
|
+
|
24
|
+
Open the installer and follow the steps to finish.
|
25
|
+
|
26
|
+
## Step 3: Verify the Installation
|
27
|
+
|
28
|
+
Open a terminal and type:
|
29
|
+
|
30
|
+
\`\`\`bash
|
31
|
+
node -v
|
32
|
+
\`\`\`
|
33
|
+
`
|
34
|
+
|
35
|
+
class BreakTokenCommand extends BaseCommand {
|
36
|
+
static description = "Break the token"
|
37
|
+
|
38
|
+
static flags = {
|
39
|
+
...BaseCommand.flags,
|
40
|
+
grading: flags.help({ char: "h" }),
|
41
|
+
}
|
42
|
+
|
43
|
+
async run() {
|
44
|
+
const { flags } = this.parse(BreakTokenCommand)
|
45
|
+
|
46
|
+
// await SessionManager.breakToken()
|
47
|
+
const paragraphs = extractParagraphs(exampleMd)
|
48
|
+
for (const paragraph of paragraphs) {
|
49
|
+
const syllables = splitIntoSyllables(paragraph)
|
50
|
+
const words = extractWords(paragraph)
|
51
|
+
const sentences = countSentences(paragraph)
|
52
|
+
const fkgl = fleschKincaidGrade(paragraph)
|
53
|
+
Console.info(paragraph)
|
54
|
+
Console.info(`Number of syllables: ${syllables.length}`)
|
55
|
+
Console.info(syllables)
|
56
|
+
Console.info(`Number of words: ${words.length}`)
|
57
|
+
Console.info(words)
|
58
|
+
Console.info(`Number of sentences: ${sentences}`)
|
59
|
+
Console.info(`FKGL: ${fkgl}`)
|
60
|
+
Console.info("---")
|
61
|
+
}
|
62
|
+
|
63
|
+
process.exit(0)
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
export default BreakTokenCommand
|
package/src/commands/init.ts
CHANGED
@@ -19,12 +19,12 @@ import {
|
|
19
19
|
createCodeFile,
|
20
20
|
readmeCreator,
|
21
21
|
createPreviewReadme,
|
22
|
-
|
22
|
+
makeReadmeReadable,
|
23
23
|
isValidRigoToken,
|
24
24
|
} from "../utils/rigoActions"
|
25
25
|
import { getConsumable } from "../utils/api"
|
26
26
|
import {
|
27
|
-
|
27
|
+
checkReadability,
|
28
28
|
PackageInfo,
|
29
29
|
getExInfo,
|
30
30
|
extractImagesFromMarkdown,
|
@@ -42,6 +42,13 @@ const durationByKind: Record<string, number> = {
|
|
42
42
|
read: 1,
|
43
43
|
}
|
44
44
|
|
45
|
+
const PARAMS = {
|
46
|
+
expected_grade_level: "6",
|
47
|
+
max_fkgl: 8,
|
48
|
+
max_words: 200,
|
49
|
+
max_rewrite_attempts: 3,
|
50
|
+
}
|
51
|
+
|
45
52
|
const whichTargetAudience = async () => {
|
46
53
|
const res = await prompts([
|
47
54
|
{
|
@@ -53,6 +60,94 @@ const whichTargetAudience = async () => {
|
|
53
60
|
return res.targetAudience
|
54
61
|
}
|
55
62
|
|
63
|
+
async function processExercise(
|
64
|
+
rigoToken: string,
|
65
|
+
steps: string[],
|
66
|
+
packageContext: string,
|
67
|
+
exercise: any,
|
68
|
+
exercisesDir: string
|
69
|
+
): Promise<string> {
|
70
|
+
const { exNumber, exTitle, kind, description } = getExInfo(exercise)
|
71
|
+
const exerciseDir = path.join(exercisesDir, `${exNumber}-${exTitle}`)
|
72
|
+
|
73
|
+
const readme = await readmeCreator(rigoToken, {
|
74
|
+
title: `${exNumber} - ${exTitle}`,
|
75
|
+
output_lang: "en",
|
76
|
+
list_of_exercises: steps.join(","),
|
77
|
+
tutorial_description: packageContext,
|
78
|
+
lesson_description: description,
|
79
|
+
kind: kind.toLowerCase(),
|
80
|
+
})
|
81
|
+
|
82
|
+
const duration = durationByKind[kind.toLowerCase()]
|
83
|
+
let attempts = 0
|
84
|
+
let readability = checkReadability(readme.parsed.content, 200, duration || 1)
|
85
|
+
|
86
|
+
while (
|
87
|
+
readability.fkglResult.fkgl > PARAMS.max_fkgl &&
|
88
|
+
attempts < PARAMS.max_rewrite_attempts
|
89
|
+
) {
|
90
|
+
Console.warning(
|
91
|
+
`The lesson ${exTitle} has as readability score of ${
|
92
|
+
readability.fkglResult.fkgl
|
93
|
+
} . It exceeds the maximum of words per minute. Rewriting it... (Attempt ${
|
94
|
+
attempts + 1
|
95
|
+
})`
|
96
|
+
)
|
97
|
+
|
98
|
+
// eslint-disable-next-line
|
99
|
+
const reducedReadme = await makeReadmeReadable(rigoToken, {
|
100
|
+
lesson: readability.body,
|
101
|
+
number_of_words: readability.minutes.toString(),
|
102
|
+
expected_number_words: PARAMS.max_words.toString(),
|
103
|
+
fkgl_results: JSON.stringify(readability.fkglResult),
|
104
|
+
expected_grade_level: PARAMS.expected_grade_level,
|
105
|
+
})
|
106
|
+
|
107
|
+
if (!reducedReadme)
|
108
|
+
break
|
109
|
+
|
110
|
+
readability = checkReadability(
|
111
|
+
reducedReadme.parsed.content,
|
112
|
+
PARAMS.max_words,
|
113
|
+
duration || 1
|
114
|
+
)
|
115
|
+
|
116
|
+
attempts++
|
117
|
+
}
|
118
|
+
|
119
|
+
Console.success(
|
120
|
+
`After ${attempts} attempts, the lesson ${exTitle} has a readability score of ${
|
121
|
+
readability.fkglResult.fkgl
|
122
|
+
} using FKGL. And it has ${readability.minutes.toFixed(
|
123
|
+
2
|
124
|
+
)} minutes of reading time.`
|
125
|
+
)
|
126
|
+
|
127
|
+
const readmeFilename = "README.md"
|
128
|
+
fs.writeFileSync(
|
129
|
+
path.join(exerciseDir, readmeFilename),
|
130
|
+
readability.newMarkdown
|
131
|
+
)
|
132
|
+
|
133
|
+
if (kind.toLowerCase() === "code") {
|
134
|
+
const codeFile = await createCodeFile(rigoToken, {
|
135
|
+
readme: readability.newMarkdown,
|
136
|
+
tutorial_info: packageContext,
|
137
|
+
})
|
138
|
+
|
139
|
+
fs.writeFileSync(
|
140
|
+
path.join(
|
141
|
+
exerciseDir,
|
142
|
+
`app.${codeFile.parsed.extension.replace(".", "")}`
|
143
|
+
),
|
144
|
+
codeFile.parsed.content
|
145
|
+
)
|
146
|
+
}
|
147
|
+
|
148
|
+
return readability.newMarkdown
|
149
|
+
}
|
150
|
+
|
56
151
|
const initializeInteractiveCreation = async (
|
57
152
|
rigoToken: string,
|
58
153
|
courseInfo: string
|
@@ -252,69 +347,9 @@ const handleAILogic = async (tutorialDir: string, packageInfo: PackageInfo) => {
|
|
252
347
|
fs.ensureDirSync(exerciseDir)
|
253
348
|
}
|
254
349
|
|
255
|
-
const exercisePromises = steps.map(
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
const readme = await readmeCreator(rigoToken, {
|
260
|
-
title: `${exNumber} - ${exTitle}`,
|
261
|
-
output_lang: "en",
|
262
|
-
list_of_exercises: steps.join(","),
|
263
|
-
tutorial_description: packageContext,
|
264
|
-
lesson_description: description,
|
265
|
-
kind: kind.toLowerCase(),
|
266
|
-
})
|
267
|
-
|
268
|
-
const duration = durationByKind[kind.toLowerCase()]
|
269
|
-
let readingTime = checkReadingTime(
|
270
|
-
readme.parsed.content,
|
271
|
-
200,
|
272
|
-
duration || 1
|
273
|
-
)
|
274
|
-
|
275
|
-
if (readingTime.exceedsThreshold) {
|
276
|
-
// Console.info(
|
277
|
-
// `The reading time for the lesson ${exTitle} exceeds the threshold, reducing it...`
|
278
|
-
// )
|
279
|
-
const reducedReadme = await reduceReadme(rigoToken, {
|
280
|
-
lesson: readingTime.body,
|
281
|
-
number_of_words: readingTime.minutes.toString(),
|
282
|
-
expected_number_words: "200",
|
283
|
-
})
|
284
|
-
|
285
|
-
if (reducedReadme) {
|
286
|
-
readingTime = checkReadingTime(
|
287
|
-
reducedReadme.parsed.content,
|
288
|
-
200,
|
289
|
-
duration || 1
|
290
|
-
)
|
291
|
-
}
|
292
|
-
}
|
293
|
-
|
294
|
-
const readmeFilename = "README.md"
|
295
|
-
|
296
|
-
fs.writeFileSync(
|
297
|
-
path.join(exerciseDir, readmeFilename),
|
298
|
-
readingTime.newMarkdown
|
299
|
-
)
|
300
|
-
|
301
|
-
if (kind.toLowerCase() === "code") {
|
302
|
-
const codeFile = await createCodeFile(rigoToken, {
|
303
|
-
readme: readme.parsed.content,
|
304
|
-
tutorial_info: packageContext,
|
305
|
-
})
|
306
|
-
|
307
|
-
fs.writeFileSync(
|
308
|
-
path.join(
|
309
|
-
exerciseDir,
|
310
|
-
`app.${codeFile.parsed.extension.replace(".", "")}`
|
311
|
-
),
|
312
|
-
codeFile.parsed.content
|
313
|
-
)
|
314
|
-
}
|
315
|
-
|
316
|
-
return readingTime.newMarkdown
|
317
|
-
})
|
350
|
+
const exercisePromises = steps.map(exercise =>
|
351
|
+
processExercise(rigoToken, steps, packageContext, exercise, exercisesDir)
|
352
|
+
)
|
318
353
|
|
319
354
|
const readmeContents = await Promise.all(exercisePromises)
|
320
355
|
Console.success("Lessons created! 🎉")
|
@@ -9,7 +9,7 @@ import { IFile } from "../../models/file"
|
|
9
9
|
import { IExercise } from "../../models/exercise-obj"
|
10
10
|
|
11
11
|
// eslint-disable-next-line
|
12
|
-
const frontMatter = require("front-matter")
|
12
|
+
const frontMatter = require("front-matter")
|
13
13
|
|
14
14
|
export const exercise = (
|
15
15
|
path: string,
|
@@ -140,6 +140,23 @@ continue
|
|
140
140
|
|
141
141
|
return content
|
142
142
|
},
|
143
|
+
|
144
|
+
renameFolder: function (newName: string) {
|
145
|
+
if (!config?.dirPath) {
|
146
|
+
throw new Error("No config directory found")
|
147
|
+
}
|
148
|
+
|
149
|
+
try {
|
150
|
+
const newPath = p.join(config?.exercisesPath, newName)
|
151
|
+
fs.renameSync(this.path, newPath)
|
152
|
+
this.path = newPath
|
153
|
+
this.slug = newName
|
154
|
+
this.title = newName
|
155
|
+
} catch (error) {
|
156
|
+
console.log(error)
|
157
|
+
throw new Error("Failed to rename exercise: " + error)
|
158
|
+
}
|
159
|
+
},
|
143
160
|
saveFile: function (name: string, content: string) {
|
144
161
|
const file: IFile | undefined = this.files.find(
|
145
162
|
(f: IFile) => f.name === name
|
@@ -453,6 +453,32 @@ throw new Error("File not found: " + filePath)
|
|
453
453
|
})
|
454
454
|
)
|
455
455
|
|
456
|
+
app.put(
|
457
|
+
"/actions/rename",
|
458
|
+
jsonBodyParser,
|
459
|
+
withHandler(async (req: express.Request, res: express.Response) => {
|
460
|
+
const { slug, newSlug } = req.body
|
461
|
+
const exercise = configManager.getExercise(slug)
|
462
|
+
if (!exercise) {
|
463
|
+
return res.status(400).json({ error: "Exercise not found" })
|
464
|
+
}
|
465
|
+
|
466
|
+
try {
|
467
|
+
if (exercise.renameFolder) {
|
468
|
+
exercise.renameFolder(newSlug)
|
469
|
+
res.json({ status: "ok" })
|
470
|
+
} else {
|
471
|
+
res.status(500).json({
|
472
|
+
error:
|
473
|
+
"Failed to rename exercise because it's not supported by the exercise",
|
474
|
+
})
|
475
|
+
}
|
476
|
+
} catch {
|
477
|
+
res.status(500).json({ error: "Failed to rename exercise" })
|
478
|
+
}
|
479
|
+
})
|
480
|
+
)
|
481
|
+
|
456
482
|
app.post(
|
457
483
|
"/exercise/:slug/create",
|
458
484
|
jsonBodyParser,
|
@@ -1,29 +1,30 @@
|
|
1
|
-
import { IFile } from "./file"
|
2
|
-
import { IConfig } from "./config"
|
3
|
-
import { ISocket } from "./socket"
|
4
|
-
|
5
|
-
export interface IExercise {
|
6
|
-
position?: number
|
7
|
-
files: Array<IFile
|
8
|
-
slug: string
|
9
|
-
path: string
|
10
|
-
done: boolean
|
11
|
-
language?: string | null
|
12
|
-
entry?: string | null
|
13
|
-
graded?: boolean
|
14
|
-
translations?: { [key: string]: string }
|
15
|
-
title: string
|
16
|
-
getReadme?: (lang: string | null) => any
|
17
|
-
getFile?: (name: string) => string | Buffer
|
18
|
-
saveFile?: (name: string, content: string) => void
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
1
|
+
import { IFile } from "./file"
|
2
|
+
import { IConfig } from "./config"
|
3
|
+
import { ISocket } from "./socket"
|
4
|
+
|
5
|
+
export interface IExercise {
|
6
|
+
position?: number
|
7
|
+
files: Array<IFile>
|
8
|
+
slug: string
|
9
|
+
path: string
|
10
|
+
done: boolean
|
11
|
+
language?: string | null
|
12
|
+
entry?: string | null
|
13
|
+
graded?: boolean
|
14
|
+
translations?: { [key: string]: string }
|
15
|
+
title: string
|
16
|
+
getReadme?: (lang: string | null) => any
|
17
|
+
getFile?: (name: string) => string | Buffer
|
18
|
+
saveFile?: (name: string, content: string) => void
|
19
|
+
renameFolder?: (newName: string) => void
|
20
|
+
getTestReport?: () => any
|
21
|
+
test?: (sessionConfig: any, config: IConfig, socket: ISocket) => void
|
22
|
+
}
|
23
|
+
|
24
|
+
export interface IExerciseData {
|
25
|
+
lastMessages?: any
|
26
|
+
userMessage?: string
|
27
|
+
exerciseSlug: string
|
28
|
+
entryPoint?: string
|
29
|
+
files: string[]
|
30
|
+
}
|
@@ -1,5 +1,8 @@
|
|
1
|
+
import Console from "./console"
|
1
2
|
|
2
3
|
const frontMatter = require("front-matter")
|
4
|
+
import * as MarkdownIt from "markdown-it"
|
5
|
+
import { syllable } from "syllable"
|
3
6
|
|
4
7
|
import * as path from "path"
|
5
8
|
import * as fs from "fs"
|
@@ -53,7 +56,12 @@ export type PackageInfo = {
|
|
53
56
|
}
|
54
57
|
}
|
55
58
|
|
56
|
-
|
59
|
+
type TFKGLResult = {
|
60
|
+
text: string
|
61
|
+
fkgl: number
|
62
|
+
}
|
63
|
+
|
64
|
+
export function checkReadability(
|
57
65
|
markdown: string,
|
58
66
|
wordsPerMinute = 200,
|
59
67
|
maxMinutes = 1
|
@@ -62,10 +70,13 @@ export function checkReadingTime(
|
|
62
70
|
exceedsThreshold: boolean
|
63
71
|
minutes: number
|
64
72
|
body: string
|
73
|
+
fkglResult: TFKGLResult
|
65
74
|
// readingEase: number
|
66
75
|
} {
|
67
76
|
const parsed = frontMatter(markdown)
|
68
77
|
|
78
|
+
const fkglResult = fleschKincaidGrade(parsed.body)
|
79
|
+
|
69
80
|
const readingTime = estimateReadingTime(parsed.body, wordsPerMinute)
|
70
81
|
|
71
82
|
// const readingEase = estimateReadingEase(parsed.body)
|
@@ -78,23 +89,24 @@ export function checkReadingTime(
|
|
78
89
|
const updatedAttributes = {
|
79
90
|
...attributes,
|
80
91
|
readingTime,
|
81
|
-
|
92
|
+
fkglResult: fkglResult.fkgl,
|
82
93
|
}
|
83
94
|
|
84
95
|
let yamlFrontMatter = ""
|
85
96
|
try {
|
86
97
|
yamlFrontMatter = yaml.dump(updatedAttributes).trim()
|
87
98
|
} catch {
|
99
|
+
Console.error("Error dumping YAML front matter")
|
88
100
|
return {
|
89
101
|
newMarkdown: "",
|
90
102
|
exceedsThreshold: false,
|
91
103
|
minutes: 0,
|
92
104
|
body: "",
|
105
|
+
fkglResult,
|
93
106
|
// readingEase: 0,
|
94
107
|
}
|
95
108
|
}
|
96
109
|
|
97
|
-
// Reconstruct the markdown with the front matter
|
98
110
|
const newMarkdown = `---\n${yamlFrontMatter}\n---\n\n${parsed.body}`
|
99
111
|
|
100
112
|
return {
|
@@ -102,7 +114,7 @@ export function checkReadingTime(
|
|
102
114
|
exceedsThreshold: readingTime.minutes > maxMinutes,
|
103
115
|
minutes: readingTime.minutes,
|
104
116
|
body: parsed.body,
|
105
|
-
|
117
|
+
fkglResult,
|
106
118
|
}
|
107
119
|
}
|
108
120
|
|
@@ -260,27 +272,17 @@ export function getContentIndex() {
|
|
260
272
|
// }
|
261
273
|
|
262
274
|
export function extractTextFromMarkdown(mdContent: string): string {
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
275
|
+
return mdContent
|
276
|
+
.replace(/!\[.*?]\(.*?\)/g, "") // Remove images
|
277
|
+
.replace(/\[.*?]\(.*?\)/g, "") // Remove links
|
278
|
+
.replace(/(```[\S\s]*?```|`.*?`)/g, "") // Remove inline & block code
|
279
|
+
.replace(/^#.*$/gm, "") // Remove headings
|
280
|
+
.replace(/(\*{1,2}|_{1,2})/g, "") // Remove bold/italic markers
|
281
|
+
.replace(/>\s?/g, "") // Remove blockquotes
|
282
|
+
.replace(/[*-]\s+/g, "") // Remove bullets from lists
|
283
|
+
.trim()
|
269
284
|
}
|
270
285
|
|
271
|
-
// export const estimateReadingEase = async (text: string): Promise<number> => {
|
272
|
-
// const cleanedText = extractTextFromMarkdown(text)
|
273
|
-
// // @ts-ignore
|
274
|
-
// const rs = (await import("text-readability")).default;
|
275
|
-
// // return fleschKincaidReadingEase(cleanedText)
|
276
|
-
// // const score = readability(cleanedText)
|
277
|
-
// // console.log(score)
|
278
|
-
// // return score.fleschKincaid ?? 0
|
279
|
-
// const score = rs.fleschReadingEase(cleanedText)
|
280
|
-
// console.log(score, "SCORE FLESCH READING EASE")
|
281
|
-
// return score
|
282
|
-
// }
|
283
|
-
|
284
286
|
const cleanReadme = (readme: string) => {
|
285
287
|
// Replace <text> and </text> with nothing
|
286
288
|
return readme.replace(/<text>/g, "").replace(/<\/text>/g, "")
|
@@ -299,3 +301,121 @@ export const saveTranslatedReadme = async (
|
|
299
301
|
)
|
300
302
|
fs.writeFileSync(readmePath, cleanReadme(readme))
|
301
303
|
}
|
304
|
+
|
305
|
+
/**
|
306
|
+
* Extracts paragraphs from a Markdown string.
|
307
|
+
* @param markdownText The input Markdown string.
|
308
|
+
* @returns An array of paragraph contents.
|
309
|
+
*/
|
310
|
+
export function extractParagraphs(markdownText: string): string[] {
|
311
|
+
const md = new MarkdownIt()
|
312
|
+
const tokens = md.parse(markdownText, {})
|
313
|
+
const paragraphs: string[] = []
|
314
|
+
|
315
|
+
for (let i = 0; i < tokens.length; i++) {
|
316
|
+
if (
|
317
|
+
tokens[i].type === "paragraph_open" &&
|
318
|
+
tokens[i + 1]?.type === "inline"
|
319
|
+
) {
|
320
|
+
paragraphs.push(tokens[i + 1].content)
|
321
|
+
}
|
322
|
+
}
|
323
|
+
|
324
|
+
return paragraphs
|
325
|
+
}
|
326
|
+
|
327
|
+
/**
|
328
|
+
* Splits a paragraph into words and separates each word into syllables.
|
329
|
+
* @param paragraph The input paragraph.
|
330
|
+
* @returns An array of words, each split into syllables.
|
331
|
+
*/
|
332
|
+
export function splitIntoSyllables(paragraph: string): string[] {
|
333
|
+
const words = paragraph.split(/\s+/)
|
334
|
+
const syllables: string[] = []
|
335
|
+
|
336
|
+
for (const word of words) {
|
337
|
+
syllables.push(...splitWordIntoSyllables(word))
|
338
|
+
}
|
339
|
+
|
340
|
+
return syllables
|
341
|
+
}
|
342
|
+
|
343
|
+
/**
|
344
|
+
* Splits a word into its syllables using a basic estimation.
|
345
|
+
* @param word The word to split.
|
346
|
+
* @returns An array of syllables.
|
347
|
+
*/
|
348
|
+
export function splitWordIntoSyllables(word: string): string[] {
|
349
|
+
const syllableCount = syllable(word)
|
350
|
+
|
351
|
+
// Simple heuristic: Split word into equal parts (not perfect, better with a dictionary-based approach)
|
352
|
+
if (syllableCount <= 1)
|
353
|
+
return [word]
|
354
|
+
|
355
|
+
const approxLength = Math.ceil(word.length / syllableCount)
|
356
|
+
const syllables: string[] = []
|
357
|
+
for (let i = 0; i < word.length; i += approxLength) {
|
358
|
+
// eslint-disable-next-line
|
359
|
+
syllables.push(word.substring(i, i + approxLength))
|
360
|
+
}
|
361
|
+
|
362
|
+
return syllables
|
363
|
+
}
|
364
|
+
|
365
|
+
/**
|
366
|
+
* Extracts words from a given paragraph.
|
367
|
+
* @param paragraph The input text.
|
368
|
+
* @returns An array of words.
|
369
|
+
*/
|
370
|
+
export function extractWords(paragraph: string): string[] {
|
371
|
+
const words = paragraph.match(/\b\w+\b/g) // Match words using regex
|
372
|
+
return words ? words : [] // Return words or an empty array if none found
|
373
|
+
}
|
374
|
+
|
375
|
+
/**
|
376
|
+
* Calculates the Flesch-Kincaid Grade Level (FKGL) for a given text.
|
377
|
+
* @param text The input paragraph.
|
378
|
+
* @returns The FKGL score.
|
379
|
+
*/
|
380
|
+
export function fleschKincaidGrade(text: string): TFKGLResult {
|
381
|
+
const processableText = extractTextFromMarkdown(text)
|
382
|
+
const words = extractWords(processableText)
|
383
|
+
const numWords = words.length
|
384
|
+
const numSentences = countSentences(processableText)
|
385
|
+
const numSyllables = words.reduce((total, word) => total + syllable(word), 0)
|
386
|
+
|
387
|
+
if (numWords === 0 || numSentences === 0) {
|
388
|
+
return {
|
389
|
+
text,
|
390
|
+
fkgl: 0,
|
391
|
+
}
|
392
|
+
}
|
393
|
+
|
394
|
+
const fkgl =
|
395
|
+
// eslint-disable-next-line
|
396
|
+
0.39 * (numWords / numSentences) + 11.8 * (numSyllables / numWords) - 15.59
|
397
|
+
|
398
|
+
return {
|
399
|
+
text,
|
400
|
+
fkgl: parseFloat(fkgl.toFixed(2)),
|
401
|
+
}
|
402
|
+
}
|
403
|
+
|
404
|
+
/**
|
405
|
+
* Counts the number of sentences in a given text.
|
406
|
+
* @param text The input paragraph.
|
407
|
+
* @returns The total number of sentences.
|
408
|
+
*/
|
409
|
+
export function countSentences(text: string): number {
|
410
|
+
const sentences = text
|
411
|
+
.split(/[!.?]+/)
|
412
|
+
.filter(sentence => sentence.trim().length > 0)
|
413
|
+
return sentences.length
|
414
|
+
}
|
415
|
+
|
416
|
+
export function howManyDifficultParagraphs(
|
417
|
+
paragraphs: TFKGLResult[],
|
418
|
+
maxFKGL: number
|
419
|
+
): number {
|
420
|
+
return paragraphs.filter(paragraph => paragraph.fkgl > maxFKGL).length
|
421
|
+
}
|