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
+
1
19
import { automatedReadability } from 'automated-readability'
2
20
import { colemanLiau } from 'coleman-liau'
3
21
import { daleChall } from 'dale-chall'
@@ -22,18 +40,24 @@ const round = Math.round
22
40
const ceil = Math . ceil
23
41
const sqrt = Math . sqrt
24
42
43
+ /**
44
+ * Plugin to detect possibly hard to read sentences.
45
+ *
46
+ * @type {import('unified').Plugin<[Options?]> }
47
+ */
25
48
export default function retextReadability ( options = { } ) {
26
49
const targetAge = options . age || defaultTargetAge
27
50
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
33
55
34
56
return ( tree , file ) => {
35
57
visit ( tree , 'SentenceNode' , ( sentence ) => {
58
+ /** @type {Record<string, boolean> } */
36
59
const familiarWords = { }
60
+ /** @type {Record<string, boolean> } */
37
61
const easyWord = { }
38
62
let complexPolysillabicWord = 0
39
63
let familiarWordCount = 0
@@ -42,17 +66,15 @@ export default function retextReadability(options = {}) {
42
66
let easyWordCount = 0
43
67
let wordCount = 0
44
68
let letters = 0
45
- let counts
46
- let caseless
47
69
48
70
visit ( sentence , 'WordNode' , ( node ) => {
49
71
const value = toString ( node )
72
+ const caseless = value . toLowerCase ( )
50
73
const syllables = syllable ( value )
51
74
52
75
wordCount ++
53
76
totalSyllables += syllables
54
77
letters += value . length
55
- caseless = value . toLowerCase ( )
56
78
57
79
// Count complex words for gunning-fog based on whether they have three
58
80
// or more syllables and whether they aren’t proper nouns. The last is
@@ -79,7 +101,7 @@ export default function retextReadability(options = {}) {
79
101
} )
80
102
81
103
if ( wordCount >= minWords ) {
82
- counts = {
104
+ const counts = {
83
105
complexPolysillabicWord,
84
106
polysillabicWord,
85
107
unfamiliarWord : wordCount - familiarWordCount ,
@@ -91,64 +113,81 @@ export default function retextReadability(options = {}) {
91
113
letter : letters
92
114
}
93
115
94
- report ( file , sentence , threshold , targetAge , [
116
+ /** @type {number[] } */
117
+ const scores = [
95
118
gradeToAge ( daleChallGradeLevel ( daleChallFormula ( counts ) ) [ 1 ] ) ,
96
119
gradeToAge ( automatedReadability ( counts ) ) ,
97
120
gradeToAge ( colemanLiau ( counts ) ) ,
98
121
fleschToAge ( flesch ( counts ) ) ,
99
122
smogToAge ( smogFormula ( counts ) ) ,
100
123
gradeToAge ( gunningFog ( counts ) ) ,
101
124
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
+ }
103
155
}
104
156
105
157
return SKIP
106
158
} )
107
159
}
108
160
}
109
161
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
+ */
113
170
function gradeToAge ( grade ) {
114
171
return round ( grade + 5 )
115
172
}
116
173
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
+ */
118
180
function fleschToAge ( value ) {
119
181
return 20 - floor ( value / 10 )
120
182
}
121
183
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
+ */
124
191
function smogToAge ( value ) {
125
192
return ceil ( sqrt ( value ) + 2.5 )
126
193
}
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
- }
0 commit comments