├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── dest └── .gitkeep ├── funding.yml ├── license ├── package.json ├── readme.md ├── screenshot.png ├── src ├── index.css ├── index.html └── index.jsx └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | main: 3 | runs-on: ubuntu-latest 4 | steps: 5 | - uses: actions/checkout@v4 6 | - uses: actions/setup-node@v4 7 | with: 8 | node-version: node 9 | - run: npm install 10 | - run: npm test 11 | - uses: JamesIves/github-pages-deploy-action@releases/v4 12 | with: 13 | branch: gh-pages 14 | commit-message: . 15 | folder: dest 16 | git-config-email: tituswormer@gmail.com 17 | git-config-name: Titus Wormer 18 | single-commit: true 19 | name: main 20 | on: 21 | push: 22 | branches: 23 | - main 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dest/ 2 | node_modules/ 3 | *.d.ts 4 | *.log 5 | *.map 6 | *.tsbuildinfo 7 | .DS_Store 8 | yarn.lock 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dest/ 2 | *.html 3 | *.md 4 | -------------------------------------------------------------------------------- /dest/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooorm/readability/c67389863b176161283fea8d4920c25f246b2d3a/dest/.gitkeep -------------------------------------------------------------------------------- /funding.yml: -------------------------------------------------------------------------------- 1 | github: wooorm 2 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Titus Wormer (https://wooorm.com)", 3 | "browserslist": [ 4 | "> 1%", 5 | "last 2 versions", 6 | "not ie <= 8" 7 | ], 8 | "bugs": "https://github.com/wooorm/readability/issues", 9 | "contributors": [ 10 | "Titus Wormer (https://wooorm.com)" 11 | ], 12 | "devDependencies": { 13 | "@types/d3-array": "^3.0.0", 14 | "@types/nlcst": "^2.0.0", 15 | "@types/react": "^18.0.0", 16 | "@types/react-dom": "^18.0.0", 17 | "automated-readability": "^2.0.0", 18 | "coleman-liau": "^2.0.0", 19 | "cssnano": "^7.0.0", 20 | "d3-array": "^3.0.0", 21 | "dale-chall": "^2.0.0", 22 | "dale-chall-formula": "^2.0.0", 23 | "esbuild": "^0.24.0", 24 | "flesch": "^2.0.0", 25 | "gunning-fog": "^2.0.0", 26 | "lerp": "^1.0.0", 27 | "nlcst-to-string": "^4.0.0", 28 | "parse-english": "^7.0.0", 29 | "postcss-cli": "^11.0.0", 30 | "postcss-preset-env": "^10.0.0", 31 | "prettier": "^3.0.0", 32 | "react": "^18.0.0", 33 | "react-dom": "^18.0.0", 34 | "rehype-cli": "^12.0.0", 35 | "rehype-preset-minify": "^7.0.0", 36 | "rehype-prevent-favicon-request": "^4.0.0", 37 | "remark-cli": "^12.0.0", 38 | "remark-preset-wooorm": "^10.0.0", 39 | "smog-formula": "^2.0.0", 40 | "spache": "^2.0.0", 41 | "spache-formula": "^2.0.0", 42 | "stylelint": "^16.0.0", 43 | "stylelint-config-standard": "^36.0.0", 44 | "syllable": "^5.0.0", 45 | "type-coverage": "^2.0.0", 46 | "typescript": "^5.0.0", 47 | "unist-util-visit": "^5.0.0", 48 | "unlerp": "^1.0.0", 49 | "xo": "^0.59.0" 50 | }, 51 | "license": "MIT", 52 | "name": "www-readability", 53 | "postcss": { 54 | "plugins": { 55 | "postcss-preset-env": {}, 56 | "cssnano": { 57 | "preset": "default" 58 | } 59 | } 60 | }, 61 | "prettier": { 62 | "bracketSpacing": false, 63 | "semi": false, 64 | "singleQuote": true, 65 | "tabWidth": 2, 66 | "trailingComma": "none", 67 | "useTabs": false 68 | }, 69 | "private": true, 70 | "remarkConfig": { 71 | "plugins": [ 72 | "remark-preset-wooorm" 73 | ] 74 | }, 75 | "repository": "wooorm/readability", 76 | "typeCoverage": { 77 | "atLeast": 100, 78 | "strict": true 79 | }, 80 | "type": "module", 81 | "scripts": { 82 | "build": "tsc --build --clean && tsc --build && type-coverage", 83 | "format": "remark --frail --output --quiet -- . && prettier --log-level warn --write -- . && xo --fix && stylelint src/index.css --fix", 84 | "generate:css": "postcss --output dest/index.css -- src/index.css", 85 | "generate:html": "rehype --frail --output dest/ --quiet --use rehype-preset-minify --use rehype-prevent-favicon-request -- src/", 86 | "generate:js:module": "esbuild src/index.jsx --bundle --conditions=browser,production --define:process.env.NODE_ENV=\\\"production\\\" --format=esm --loader:.js=jsx --log-level=warning --minify --outfile=dest/index.module.js --target=es2020", 87 | "generate:js:nomodule": "esbuild src/index.jsx --bundle --conditions=browser,production --define:process.env.NODE_ENV=\\\"production\\\" --loader:.js=jsx --log-level=warning --minify --outfile=dest/index.nomodule.js --target=es6", 88 | "generate:js": "npm run generate:js:module && npm run generate:js:nomodule", 89 | "generate": "npm run generate:css && npm run generate:html && npm run generate:js", 90 | "test": "npm run build && npm run format && npm run generate" 91 | }, 92 | "stylelint": { 93 | "extends": "stylelint-config-standard" 94 | }, 95 | "xo": { 96 | "prettier": true, 97 | "rules": { 98 | "unicorn/prefer-code-point": "off" 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # readability 2 | 3 | [![screenshot](screenshot.png)](https://wooorm.com/readability) 4 | 5 | ## Related 6 | 7 | * [`write-music`](https://github.com/wooorm/write-music) 8 | * [`common-words`](https://github.com/wooorm/common-words) 9 | * [`short-words`](https://github.com/wooorm/short-words) 10 | 11 | ## License 12 | 13 | [MIT](license) © [Titus Wormer](https://wooorm.com) 14 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooorm/readability/c67389863b176161283fea8d4920c25f246b2d3a/screenshot.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | line-height: calc(1em + 1ex); 4 | } 5 | 6 | a { 7 | color: #0367d8; 8 | text-decoration: none; 9 | transition: 200ms; 10 | transition-property: color; 11 | } 12 | 13 | a:focus, 14 | a:hover, 15 | a:target { 16 | color: inherit; 17 | } 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | code { 24 | font-size: 16px; 25 | } 26 | 27 | h1, 28 | p { 29 | margin-bottom: calc(1em + 1ex); 30 | margin-top: calc(1em + 1ex); 31 | } 32 | 33 | h1 { 34 | font-size: 3em; 35 | font-weight: 100; 36 | text-align: center; 37 | } 38 | 39 | html { 40 | background-color: hsl(0deg 0% 95%); 41 | color-scheme: light dark; 42 | font-family: system-ui; 43 | word-break: break-word; 44 | } 45 | 46 | main { 47 | background-color: hsl(0deg 0% 97.5%); 48 | position: relative; 49 | max-width: 40em; 50 | margin: 0 auto; 51 | padding: 0 calc(2em + 2ex); 52 | } 53 | 54 | main > div { 55 | border-radius: inherit; 56 | } 57 | 58 | section { 59 | border: 0 solid hsl(214deg 13% 90%); 60 | border-top-width: 1px; 61 | border-bottom-width: 1px; 62 | margin: calc(2em + 2ex) calc(-2em - 2ex); 63 | padding: calc(2em + 2ex); 64 | } 65 | 66 | section + section { 67 | margin-top: calc(-2em - 2ex - 1px); 68 | } 69 | 70 | section:first-child { 71 | border-top-left-radius: inherit; 72 | border-top-right-radius: inherit; 73 | border-top-width: 0; 74 | margin-top: 0; 75 | } 76 | 77 | section:last-child { 78 | border-bottom-left-radius: inherit; 79 | border-bottom-right-radius: inherit; 80 | border-bottom-width: 0; 81 | margin-bottom: 0; 82 | } 83 | 84 | section > :first-child { 85 | margin-top: 0; 86 | } 87 | 88 | section > :last-child { 89 | margin-bottom: 0; 90 | } 91 | 92 | template { 93 | display: none; 94 | } 95 | 96 | textarea, 97 | .draw { 98 | /* Can’t use a nice font: kerning renders differently in textareas. */ 99 | background: transparent; 100 | box-sizing: border-box; 101 | border: none; 102 | font-family: monospace; 103 | font-size: 16px; 104 | height: 100%; 105 | letter-spacing: normal; 106 | line-height: 1.5; 107 | margin: 0; 108 | outline: none; 109 | overflow: hidden; 110 | padding: 0; 111 | resize: none; 112 | white-space: pre-wrap; 113 | width: 100%; 114 | word-wrap: break-word; 115 | } 116 | 117 | textarea { 118 | color: inherit; 119 | position: absolute; 120 | top: 0; 121 | } 122 | 123 | [title] { 124 | border-bottom: 1px dotted; 125 | } 126 | 127 | .credits { 128 | text-align: center; 129 | } 130 | 131 | .draw { 132 | -webkit-print-color-adjust: exact; 133 | color: transparent; 134 | print-color-adjust: exact; 135 | } 136 | 137 | .editor { 138 | overflow: hidden; 139 | position: relative; 140 | max-width: 100%; 141 | } 142 | 143 | .highlight { 144 | background-color: hsl(0deg 0% 100%); 145 | } 146 | 147 | @media (width >= 40em) and (height >= 20em) { 148 | main { 149 | /* Go all Tschichold when supported */ 150 | border: 1px solid hsl(214deg 13% 90%); 151 | border-radius: 3px; 152 | margin: 11.1vh 22.2vw 22.2vh 11.1vw; 153 | } 154 | } 155 | 156 | @media (prefers-color-scheme: dark) { 157 | html { 158 | background-color: hsl(214deg 13% 7.5%); 159 | color: hsl(214deg 13% 95%); 160 | } 161 | 162 | main { 163 | background-color: hsl(214deg 13% 5%); 164 | } 165 | 166 | @media (width >= 40em) and (height >= 20em) { 167 | main { 168 | border-color: hsl(214deg 13% 12.5%); 169 | } 170 | } 171 | 172 | section { 173 | border-color: hsl(214deg 13% 12.5%); 174 | } 175 | 176 | .highlight { 177 | background-color: hsl(214deg 13% 2.5%); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | readability 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | /// 4 | 5 | /** 6 | * @import {Nodes, Paragraph, Parents, Sentence} from 'nlcst' 7 | */ 8 | 9 | import {automatedReadability} from 'automated-readability' 10 | import {colemanLiau} from 'coleman-liau' 11 | import {mean, median, mode} from 'd3-array' 12 | import {daleChallFormula, daleChallGradeLevel} from 'dale-chall-formula' 13 | import {daleChall} from 'dale-chall' 14 | import {flesch} from 'flesch' 15 | import {gunningFog} from 'gunning-fog' 16 | // @ts-expect-error: untyped. 17 | import lerp from 'lerp' 18 | import {toString} from 'nlcst-to-string' 19 | import {ParseEnglish} from 'parse-english' 20 | import ReactDom from 'react-dom/client' 21 | import React from 'react' 22 | import {smogFormula} from 'smog-formula' 23 | import {spacheFormula} from 'spache-formula' 24 | import {spache} from 'spache' 25 | import {syllable} from 'syllable' 26 | import {SKIP, visit} from 'unist-util-visit' 27 | // @ts-expect-error: untyped. 28 | import unlerp from 'unlerp' 29 | 30 | const $main = /** @type {HTMLElement} */ (document.querySelector('main')) 31 | const defaultAge = 12 32 | const maxAge = 22 33 | const minAge = 5 34 | /** @type {Record} */ 35 | const samples = { 36 | 'Readability: Definition, Wikipedia': `People have defined readability in various ways, e.g., in: The Literacy Dictionary, Jeanne Chall and Edgar Dale, G. Harry McLaughlin, William DuBay. 37 | 38 | Easy reading helps learning and enjoyment, so what we write should be easy to understand. 39 | 40 | While many writers and speakers since ancient times have used plain language, the 20th century brought more focus to reading ease. Much research has focused on matching prose to reading skills. This has used many successful formulas: in research, government, teaching, publishing, the military, medicine, and business. Many people in many languages have been helped by this. 41 | 42 | By the year 2000, there were over 1,000 studies on readability formulas in professional journals about their validity and merit. The study of reading is not just in teaching. Research has shown that much money is wasted by companies in making texts hard for the average reader to read. 43 | 44 | There are summaries of this research; see the links in this section. Many textbooks on reading include pointers to readability.`, 45 | 'Ernest Hemingway, The Sun Also Rises': `Finally, after a couple more false klaxons, the bus started, and Robert Cohn waved good-by to us, and all the Basques waved good-by to him. As soon as we started out on the road outside of town it was cool. It felt nice riding high up and close under the trees. The bus went quite fast and made a good breeze, and as we went out along the road with the dust powdering the trees and down the hill, we had a fine view, back through the trees, of the town rising up from the bluff above the river. The Basque lying against my knees pointed out the view with the neck of a wine-bottle, and winked at us. He nodded his head.`, 46 | 'Dr Seuss, The Cat in the Hat': `Then our mother came in 47 | And she said to us two, 48 | “Did you have any fun? 49 | Tell me. What did you do?” 50 | 51 | And Sally and I did not 52 | know what to say. 53 | Should we tell her 54 | The things that went on 55 | there that day? 56 | 57 | Well… what would YOU do 58 | If your mother asked you? 59 | 60 | The Cat in the Hat 61 | Look at me! 62 | Look at me! 63 | Look at me NOW! 64 | It is fun to have fun 65 | But you have 66 | to know how.`, 67 | 'Trump, Presidential Bid announcement': `Thank you. It’s true, and these are the best and the finest. When Mexico sends its people, they’re not sending their best. They’re not sending you. They’re not sending you. They’re sending people that have lots of problems, and they’re bringing those problems with us. They’re bringing drugs. They’re bringing crime. They’re rapists. And some, I assume, are good people. 68 | 69 | But I speak to border guards and they tell us what we’re getting. And it only makes common sense. It only makes common sense. They’re sending us not the right people.`, 70 | 'Obama, Farewell Speech': `On Tuesday, January 10, I’ll go home to Chicago to say my grateful farewell to you, even if you can’t be there in person. 71 | 72 | I’m just beginning to write my remarks. But I’m thinking about them as a chance to say thank you for this amazing journey, to celebrate the ways you’ve changed this country for the better these past eight years, and to offer some thoughts on where we all go from here. 73 | Since 2009, we’ve faced our fair share of challenges, and come through them stronger. That’s because we have never let go of a belief that has guided us ever since our founding — our conviction that, together, we can change this country for the better. So I hope you’ll join me one last time.` 74 | } 75 | const scale = 6 76 | 77 | const parser = new ParseEnglish() 78 | 79 | const root = ReactDom.createRoot($main) 80 | 81 | root.render(React.createElement(Playground)) 82 | 83 | function Playground() { 84 | const sampleNames = Object.keys(samples) 85 | const [age, setAge] = React.useState(defaultAge) 86 | const [average, setAverage] = React.useState('median') 87 | const [paragraph, setParagraph] = React.useState(false) 88 | const [sample, setSample] = React.useState(sampleNames[0]) 89 | const [text, setText] = React.useState(textFromSample(sample)) 90 | const tree = parser.parse(text) 91 | let unselected = true 92 | /** @type {Array} */ 93 | const options = [] 94 | 95 | for (const sampleName of sampleNames) { 96 | const selected = textFromSample(sampleName) === text 97 | 98 | if (selected) unselected = false 99 | 100 | options.push( 101 | 104 | ) 105 | } 106 | 107 | return ( 108 |
109 |
110 |

111 | readability 112 |

113 |
114 |
115 |
116 | {all(tree)} 117 | {/* Trailing whitespace in a `textarea` is shown, 118 | but not in a `div` with `white-space: pre-wrap`; 119 | add a `br` to make the last newline explicit. */} 120 | {/\n[ \t]*$/.test(text) ?
: undefined} 121 |
122 |