Skip to content

Commit 8cb0d09

Browse files
committed
Add JSDoc based types
1 parent f376b7d commit 8cb0d09

File tree

5 files changed

+209
-137
lines changed

5 files changed

+209
-137
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
node_modules/
22
coverage/
33
.DS_Store
4+
*.d.ts
45
*.log
56
yarn.lock

index.js

+84-45
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
/**
2+
* @typedef Options
3+
* @property {number} [age=16]
4+
* Target age group.
5+
* Note that the different algorithms provide varying results, so your milage
6+
* may vary with people actually that age.
7+
* @property {number} [threshold=4/7]
8+
* Number of algorithms that need to agree.
9+
* By default, 4 out of the 7 algorithms need to agree that a sentence is hard
10+
* to read for the target age, in which case it’s warned about.
11+
* @property {number} [minWords=5]
12+
* Minimum number of words a sentence should have when warning.
13+
* Most algorithms are designed to take a large sample of sentences to detect
14+
* the body’s reading level.
15+
* This plugin works on a per-sentence basis and that makes the results quite
16+
* skewered when a short sentence has a few long words or some unknown ones.
17+
*/
18+
119
import {automatedReadability} from 'automated-readability'
220
import {colemanLiau} from 'coleman-liau'
321
import {daleChall} from 'dale-chall'
@@ -22,18 +40,24 @@ const round = Math.round
2240
const ceil = Math.ceil
2341
const sqrt = Math.sqrt
2442

43+
/**
44+
* Plugin to detect possibly hard to read sentences.
45+
*
46+
* @type {import('unified').Plugin<[Options?]>}
47+
*/
2548
export default function retextReadability(options = {}) {
2649
const targetAge = options.age || defaultTargetAge
2750
const threshold = options.threshold || defaultThreshold
28-
let minWords = options.minWords
29-
30-
if (minWords === null || minWords === undefined) {
31-
minWords = defaultWordynessThreshold
32-
}
51+
const minWords =
52+
options.minWords === null || options.minWords === undefined
53+
? defaultWordynessThreshold
54+
: options.minWords
3355

3456
return (tree, file) => {
3557
visit(tree, 'SentenceNode', (sentence) => {
58+
/** @type {Record<string, boolean>} */
3659
const familiarWords = {}
60+
/** @type {Record<string, boolean>} */
3761
const easyWord = {}
3862
let complexPolysillabicWord = 0
3963
let familiarWordCount = 0
@@ -42,17 +66,15 @@ export default function retextReadability(options = {}) {
4266
let easyWordCount = 0
4367
let wordCount = 0
4468
let letters = 0
45-
let counts
46-
let caseless
4769

4870
visit(sentence, 'WordNode', (node) => {
4971
const value = toString(node)
72+
const caseless = value.toLowerCase()
5073
const syllables = syllable(value)
5174

5275
wordCount++
5376
totalSyllables += syllables
5477
letters += value.length
55-
caseless = value.toLowerCase()
5678

5779
// Count complex words for gunning-fog based on whether they have three
5880
// or more syllables and whether they aren’t proper nouns. The last is
@@ -79,7 +101,7 @@ export default function retextReadability(options = {}) {
79101
})
80102

81103
if (wordCount >= minWords) {
82-
counts = {
104+
const counts = {
83105
complexPolysillabicWord,
84106
polysillabicWord,
85107
unfamiliarWord: wordCount - familiarWordCount,
@@ -91,64 +113,81 @@ export default function retextReadability(options = {}) {
91113
letter: letters
92114
}
93115

94-
report(file, sentence, threshold, targetAge, [
116+
/** @type {number[]} */
117+
const scores = [
95118
gradeToAge(daleChallGradeLevel(daleChallFormula(counts))[1]),
96119
gradeToAge(automatedReadability(counts)),
97120
gradeToAge(colemanLiau(counts)),
98121
fleschToAge(flesch(counts)),
99122
smogToAge(smogFormula(counts)),
100123
gradeToAge(gunningFog(counts)),
101124
gradeToAge(spacheFormula(counts))
102-
])
125+
]
126+
127+
let index = -1
128+
let failCount = 0
129+
130+
while (++index < scores.length) {
131+
if (scores[index] > targetAge) {
132+
failCount++
133+
}
134+
}
135+
136+
const confidence = failCount / scores.length
137+
138+
if (confidence >= threshold) {
139+
const label = failCount + '/' + scores.length
140+
141+
Object.assign(
142+
file.message(
143+
'Hard to read sentence (confidence: ' + label + ')',
144+
sentence,
145+
origin
146+
),
147+
{
148+
actual: toString(sentence),
149+
expected: [],
150+
confidence,
151+
confidenceLabel: label
152+
}
153+
)
154+
}
103155
}
104156

105157
return SKIP
106158
})
107159
}
108160
}
109161

110-
// Calculate the typical starting age (on the higher-end) when someone joins
111-
// `grade` grade, in the US. See:
112-
// https://en.wikipedia.org/wiki/Educational_stage#United_States
162+
/**
163+
* Calculate the typical starting age (on the higher-end) when someone joins
164+
* `grade` grade, in the US.
165+
* See: <https://en.wikipedia.org/wiki/Educational_stage#United_States>
166+
*
167+
* @param {number} grade
168+
* @returns {number}
169+
*/
113170
function gradeToAge(grade) {
114171
return round(grade + 5)
115172
}
116173

117-
// Calculate the age relating to a Flesch result.
174+
/**
175+
* Calculate the age relating to a Flesch result.
176+
*
177+
* @param {number} value
178+
* @returns {number}
179+
*/
118180
function fleschToAge(value) {
119181
return 20 - floor(value / 10)
120182
}
121183

122-
// Calculate the age relating to a SMOG result. See:
123-
// http://www.readabilityformulas.com/smog-readability-formula.php
184+
/**
185+
* Calculate the age relating to a SMOG result.
186+
* See: <http://www.readabilityformulas.com/smog-readability-formula.php>
187+
*
188+
* @param {number} value
189+
* @returns {number}
190+
*/
124191
function smogToAge(value) {
125192
return ceil(sqrt(value) + 2.5)
126193
}
127-
128-
// Report the `results` if they’re reliably too hard for the `target` age.
129-
// eslint-disable-next-line max-params
130-
function report(file, node, threshold, target, results) {
131-
let index = -1
132-
let failCount = 0
133-
134-
while (++index < results.length) {
135-
if (results[index] > target) {
136-
failCount++
137-
}
138-
}
139-
140-
const confidence = failCount / results.length
141-
142-
if (confidence >= threshold) {
143-
const label = failCount + '/' + results.length
144-
145-
Object.assign(
146-
file.message(
147-
'Hard to read sentence (confidence: ' + label + ')',
148-
node,
149-
origin
150-
),
151-
{actual: toString(node), expected: [], confidence, confidenceLabel: label}
152-
)
153-
}
154-
}

package.json

+15-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141
"sideEffects": false,
4242
"type": "module",
4343
"main": "index.js",
44+
"types": "index.d.ts",
4445
"files": [
46+
"index.d.ts",
4547
"index.js"
4648
],
4749
"dependencies": {
@@ -56,22 +58,28 @@
5658
"spache": "^2.0.0",
5759
"spache-formula": "^2.0.0",
5860
"syllable": "^5.0.0",
61+
"unified": "^10.0.0",
5962
"unist-util-visit": "^3.0.0"
6063
},
6164
"devDependencies": {
65+
"@types/tape": "^4.0.0",
6266
"c8": "^7.0.0",
6367
"prettier": "^2.0.0",
6468
"remark-cli": "^9.0.0",
6569
"remark-preset-wooorm": "^8.0.0",
6670
"retext": "^8.0.0",
71+
"rimraf": "^3.0.0",
6772
"tape": "^5.0.0",
73+
"type-coverage": "^2.0.0",
74+
"typescript": "^4.0.0",
6875
"xo": "^0.39.0"
6976
},
7077
"scripts": {
78+
"build": "rimraf \"*.d.ts\" && tsc && type-coverage",
7179
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
7280
"test-api": "node --conditions development test.js",
7381
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node --conditions development test.js",
74-
"test": "npm run format && npm run test-coverage"
82+
"test": "npm run build && npm run format && npm run test-coverage"
7583
},
7684
"prettier": {
7785
"tabWidth": 2,
@@ -88,5 +96,11 @@
8896
"plugins": [
8997
"preset-wooorm"
9098
]
99+
},
100+
"typeCoverage": {
101+
"atLeast": 100,
102+
"detail": true,
103+
"strict": true,
104+
"ignoreCatch": true
91105
}
92106
}

0 commit comments

Comments
 (0)