Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: syntax-tree/hast-util-reading-time
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 1.0.1
Choose a base ref
...
head repository: syntax-tree/hast-util-reading-time
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 1.0.2
Choose a head ref
  • 13 commits
  • 7 files changed
  • 1 contributor

Commits on May 24, 2022

  1. Update dev-dependencies

    wooorm committed May 24, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    wooorm Titus
    Copy the full SHA
    fb7f4f6 View commit details
  2. Add improved docs

    wooorm committed May 24, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    wooorm Titus
    Copy the full SHA
    91aaef7 View commit details

Commits on May 29, 2022

  1. Update example

    wooorm committed May 29, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    wooorm Titus
    Copy the full SHA
    4265ac0 View commit details

Commits on Jan 9, 2023

  1. Update dev-dependencies

    wooorm committed Jan 9, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    wooorm Titus
    Copy the full SHA
    17882b1 View commit details
  2. Replace dev-dependencies

    wooorm committed Jan 9, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    wooorm Titus
    Copy the full SHA
    529016c View commit details
  3. Update Actions

    wooorm committed Jan 9, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    wooorm Titus
    Copy the full SHA
    81b7641 View commit details
  4. Update tsconfig.json

    wooorm committed Jan 9, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    wooorm Titus
    Copy the full SHA
    9d10092 View commit details
  5. Refactor code-style

    *   Add support for `null` as type input in API
    *   Add more docs to JSDoc
    wooorm committed Jan 9, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    wooorm Titus
    Copy the full SHA
    9bc0472 View commit details
  6. Use Node test runner

    wooorm committed Jan 9, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    wooorm Titus
    Copy the full SHA
    cf12471 View commit details
  7. Verified

    This commit was signed with the committer’s verified signature.
    wooorm Titus
    Copy the full SHA
    8437572 View commit details
  8. Verified

    This commit was signed with the committer’s verified signature.
    wooorm Titus
    Copy the full SHA
    b217b6c View commit details
  9. Add improved docs

    wooorm committed Jan 9, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    wooorm Titus
    Copy the full SHA
    e8d0694 View commit details
  10. 1.0.2

    wooorm committed Jan 9, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    wooorm Titus
    Copy the full SHA
    80e2f7d View commit details
Showing with 317 additions and 189 deletions.
  1. +4 −4 .github/workflows/main.yml
  2. +2 −93 index.js
  3. +117 −0 lib/index.js
  4. +9 −12 package.json
  5. +136 −36 readme.md
  6. +40 −36 test.js
  7. +9 −8 tsconfig.json
8 changes: 4 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -7,15 +7,15 @@ jobs:
name: ${{matrix.node}}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: dcodeIO/setup-node-nvm@master
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{matrix.node}}
- run: npm install
- run: npm test
- uses: codecov/codecov-action@v1
- uses: codecov/codecov-action@v3
strategy:
matrix:
node:
- lts/erbium
- lts/hydrogen
- node
95 changes: 2 additions & 93 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,96 +1,5 @@
/**
* @typedef {import('hast').Root} Root
* @typedef {import('hast').Content} Content
* @typedef {Root|Content} Node
*
* @typedef Options
* @property {number} [age=16]
* Target age group.
* This is the age your target audience was still in school.
* Set it to 18 if you expect all readers to have finished high school,
* 21 if you expect your readers to all be college graduates, etc.
* @typedef {import('./lib/index.js').Options} Options
*/

import {toText} from 'hast-util-to-text'
import readabilityScores from 'readability-scores'
// @ts-expect-error: untyped.
import median from 'compute-median'

// See source [1].
const firstGradeAge = 5
const highschoolGraduationAge = 18
const graduationAge = 22

// See source [2].
// Note that different other algorithms vary between 200, 230, 270, and 280.
// 228 seems to at least be based in research.
const reasonableWpm = 228
const reasonableWpmMax = 340

// See source [3].
const addedWpmPerGrade = 14
const baseWpm =
reasonableWpm - (highschoolGraduationAge - firstGradeAge) * addedWpmPerGrade

const accuracy = 1e6

/**
* Utility to estimate the reading time, taking readability of the document and
* a target age group into account.
*
* * [1] For more info on US education/grade levels, see:
* <https://en.wikipedia.org/wiki/Educational_stage#United_States>.
* * [2] For the wpm of people reading English, see:
* <https://en.wikipedia.org/wiki/Words_per_minute#Reading_and_comprehension>
* * [3] For information on reading rate, including how grade levels affect
* them, see: <https://en.wikipedia.org/wiki/Reading#Reading_rate>.
*
* And some more background info/history and a few insight on where this comes
* from, see: <https://martech.org/estimated-reading-times-increase-engagement/>.
*
* @param {Node} tree
* Content to estimate.
* @param {Options} [options]
* Configuration.
* @returns {number}
* Estimated reading time in minutes.
*/
export function readingTime(tree, options = {}) {
// Cap an age to a reasonable and meaningful age in school.
const targetAge = Math.min(
graduationAge,
Math.max(firstGradeAge, Math.round(options.age || 16))
)
const text = toText(tree)
const scores = readabilityScores(text) || {}
const score = median(
[
scores.daleChall,
scores.ari,
scores.colemanLiau,
scores.fleschKincaid,
scores.smog,
scores.gunningFog
].filter((d) => d !== undefined)
)

if (score === null) {
return 0
}

/** @type {number} */
const readabilityAge = firstGradeAge + score

// WPM the target audience normally reads.
const targetWpm = baseWpm + (targetAge - firstGradeAge) * addedWpmPerGrade

// If the text requires higher comprehension than the target age group is,
// estimated to have, make it a bit slower (and vice versa).
const adjustedWpm =
targetWpm - (readabilityAge - targetAge) * (addedWpmPerGrade / 2)

// Cap it to a WPM that’s reasonable.
const wpm = Math.min(reasonableWpmMax, Math.max(baseWpm, adjustedWpm))

return Math.round((scores.wordCount / wpm) * accuracy) / accuracy
}
export {readingTime} from './lib/index.js'
117 changes: 117 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* @typedef {import('hast').Root} Root
* @typedef {import('hast').Content} Content
*/

/**
* @typedef {Root | Content} Node
*
* @typedef Options
* Configuration
* @property {number | null | undefined} [age=16]
* Target age group.
*
* This is the age your target audience was still in school.
* Set it to 18 if you expect all readers to have finished high school,
* 21 if you expect your readers to all be college graduates, etc.
*/

import {toText} from 'hast-util-to-text'
import readabilityScores from 'readability-scores'
// @ts-expect-error: untyped.
import median from 'compute-median'

// See <https://en.wikipedia.org/wiki/Educational_stage#United_States>
// for more info on US education/grade levels.
const firstGradeAge = 5
const highschoolGraduationAge = 18
const graduationAge = 22

// See <https://en.wikipedia.org/wiki/Words_per_minute#Reading_and_comprehension>
// for the wpm of people reading English.
//
// Note that different other algorithms vary between 200, 230, 270, and 280.
// 228 seems to at least be based in research.
const reasonableWpm = 228
const reasonableWpmMax = 340

// See <https://en.wikipedia.org/wiki/Reading#Reading_rate>
// for information on reading rate, including how grade levels affect them.
const addedWpmPerGrade = 14
const baseWpm =
reasonableWpm - (highschoolGraduationAge - firstGradeAge) * addedWpmPerGrade

const accuracy = 1e6

/**
* Estimate the reading time, taking readability of the document and a target
* age group into account.
*
* For some more background info/history and a few insight on where this all
* comes from, see: <https://martech.org/estimated-reading-times-increase-engagement/>.
*
* ###### Algorithm
*
* The algorithm works as follows:
*
* * estimate the WPM (words per minute) of the audience age based on the facts
* that English can be read at ±228 WPM (Trauzettel-Klosinski), and that
* reading rate increases 14 WPM per grade (Carver)
* * apply the readability algorithms Dale—Chall, Automated Readability,
* Coleman-Liau, Flesch, Gunning-Fog, SMOG, and Spache
* * adjust the WPM of the audience for whether the readability algorithms
* estimate its above or below their level
* * `wordCount / adjustedWpm = readingTime`
*
* > ⚠️ **Important**: this algorithm is specific to English.
*
* @param {Node} tree
* Tree to inspect.
* @param {Options | null | undefined} [options]
* Configuration.
* @returns {number}
* Estimated reading time in minutes.
*
* The result is not rounded so it’s possible to retrieve estimated seconds
* from it.
*/
export function readingTime(tree, options) {
const settings = options || {}
// Cap an age to a reasonable and meaningful age in school.
const targetAge = Math.min(
graduationAge,
Math.max(firstGradeAge, Math.round(settings.age || 16))
)
const text = toText(tree)
const scores = readabilityScores(text) || {}
const score = median(
[
scores.daleChall,
scores.ari,
scores.colemanLiau,
scores.fleschKincaid,
scores.smog,
scores.gunningFog
].filter((d) => d !== undefined)
)

if (score === null) {
return 0
}

/** @type {number} */
const readabilityAge = firstGradeAge + score

// WPM the target audience normally reads.
const targetWpm = baseWpm + (targetAge - firstGradeAge) * addedWpmPerGrade

// If the text requires higher comprehension than the target age group is,
// estimated to have, make it a bit slower (and vice versa).
const adjustedWpm =
targetWpm - (readabilityAge - targetAge) * (addedWpmPerGrade / 2)

// Cap it to a WPM that’s reasonable.
const wpm = Math.min(reasonableWpmMax, Math.max(baseWpm, adjustedWpm))

return Math.round((scores.wordCount / wpm) * accuracy) / accuracy
}
21 changes: 9 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hast-util-reading-time",
"version": "1.0.1",
"version": "1.0.2",
"description": "hast utility to estimate the reading time",
"license": "MIT",
"keywords": [
@@ -30,6 +30,7 @@
"main": "index.js",
"types": "index.d.ts",
"files": [
"lib/",
"index.d.ts",
"index.js"
],
@@ -40,26 +41,22 @@
"readability-scores": "^1.0.0"
},
"devDependencies": {
"@types/tape": "^4.0.0",
"@types/node": "^18.0.0",
"c8": "^7.0.0",
"hastscript": "^7.0.0",
"hast-util-from-html": "^1.0.0",
"prettier": "^2.0.0",
"rehype-parse": "^8.0.3",
"remark-cli": "^10.0.0",
"remark-cli": "^11.0.0",
"remark-preset-wooorm": "^9.0.0",
"rimraf": "^3.0.0",
"tape": "^5.0.0",
"type-coverage": "^2.0.0",
"typescript": "^4.0.0",
"unified": "^10.1.0",
"xo": "^0.44.0"
"xo": "^0.53.0"
},
"scripts": {
"prepack": "npm run build && npm run format",
"build": "rimraf \"*.d.ts\" && tsc && type-coverage",
"build": "tsc --build --clean && tsc --build && type-coverage",
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
"test-api": "node test.js",
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node test.js",
"test-api": "node --conditions development test.js",
"test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api",
"test": "npm run build && npm run format && npm run test-coverage"
},
"prettier": {
Loading