diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c6c8b36 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.github/workflows/bb.yml b/.github/workflows/bb.yml new file mode 100644 index 0000000..0198fc3 --- /dev/null +++ b/.github/workflows/bb.yml @@ -0,0 +1,13 @@ +name: bb +on: + issues: + types: [opened, reopened, edited, closed, labeled, unlabeled] + pull_request_target: + types: [opened, reopened, edited, closed, labeled, unlabeled] +jobs: + main: + runs-on: ubuntu-latest + steps: + - uses: unifiedjs/beep-boop-beta@main + with: + repo-token: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..fe284ad --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,21 @@ +name: main +on: + - pull_request + - push +jobs: + main: + name: ${{matrix.node}} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: dcodeIO/setup-node-nvm@master + with: + node-version: ${{matrix.node}} + - run: npm install + - run: npm test + - uses: codecov/codecov-action@v1 + strategy: + matrix: + node: + - lts/erbium + - node diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c977c85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +*.d.ts +*.log +coverage/ +node_modules/ +yarn.lock diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..619aa6b --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +coverage/ +*.html +*.md diff --git a/fixture.html b/fixture.html new file mode 100644 index 0000000..283be17 --- /dev/null +++ b/fixture.html @@ -0,0 +1 @@ +unified

unified

Content as structured data

We compile content to syntax trees and syntax trees to content.
We also provide hundreds of packages to work on the trees in between.
You can build on the unified collective to make all kinds of interesting things.

Build

We provide the building blocks: from tiny, focussed, modular utilities to plugins that combine them to perform bigger tasks. And much, much more. You can build on unified, mixing and matching building blocks together, to make all kinds of interesting new things.

  1. Prettier

    Uses unified to format Markdown

  2. Gatsby

    Uses unified to pull content into GraphQL

  3. Write Music

    Uses unified to visualize sentence length

  4. Node.js

    Uses unified to check and build their docs

  5. alex

    Uses unified to catch insensitive, inconsiderate writing

  6. See 23 other cases

Learn

We provide the interface: for parsing, inspecting, transforming, and serializing content. You work on structured data. Learn how to plug building blocks together, write your own, and make things with unified.

  1. Intro to unified

    Guide that summarises the what and why of unified

    1. guide
    2. welcome
    3. introduction
  2. Use unified

    Guide that delves into transforming Markdown to HTML

    1. guide
    2. use
    3. transform
    4. remark
    5. rehype
  3. Tree traversal

    How to do tree traversal (also known as walking or visiting a tree)

    1. recipe
    2. unist
    3. tree
    4. traverse
    5. walk
    6. visit
  4. Find a node

    How to find a node in any unist syntax tree

    1. recipe
    2. node
    3. tree
    4. traverse
    5. walk
    6. find
  5. Support tables in remark

    How to support GitHub-style tables in remark (or react-markdown)

    1. recipe
    2. remark
    3. plugin
    4. gfm
    5. github
    6. table
  6. See 10 other articles

Explore

The ever growing ecosystem that the unified collective provides today consists of 328 open source projects, with a combined 38k stars on GitHub. In comparison, the code that the collective maintains is about 178 Moby Dicks or 70 Linuxes. In the last 30 days, the 502 packages maintained in those projects were downloaded 636m times from npm. Much of this is maintained by our teams, yet others are provided by the community.

  1. nlcst-to-string

    nlcst utility to transform a tree to a string
    1. 72%
    2. 851k
    3. 318 B
  2. hast-util-to-string

    hast utility to get the plain-text value of a node
    1. 72%
    2. 604k
    3. 256 B
  3. unist-util-visit-parents

    unist utility to recursively walk over nodes, with ancestral information
    1. 79%
    2. 24m
    3. 763 B
  4. remark-lint-no-inline-padding

    remark-lint rule to warn when inline nodes are padded with spaces
    1. 71%
    2. 316k
    3. 1.49 kB
  5. unist-util-generated

    unist utility to check if a node is generated
    1. 77%
    2. 11m
    3. 203 B
  6. See 502 packages and 328 projects

Work

Maintaining the collective, developing new projects, keeping everything fast and secure, and helping users, is a lot of work. In total, we’ve closed 4k issues/PRs while 190 are currently open (4%). In the last 30 days, we’ve cut 97 new releases.

  1. rehypejs/rehype-infer-description-meta@1.0.0·

    💯

  2. syntax-tree/hast-util-excerpt@1.0.1·

  3. syntax-tree/hast-util-excerpt@1.0.0·

    💯

  4. Explore recent releases

Sponsor

Thankfully, we are backed financially by our sponsors. This allows us to spend more time maintaining our projects and developing new ones. To support our efforts financially, sponsor or back us on OpenCollective.

  1. Vercel

    Develop. Preview. Ship. – Creators of nextjs.org

  2. GatsbyJS

  3. Netlify

    Netlify is everything you need to build fast, modern websites: continuous deployment, serverless functions, and so much more.

  4. Motif

    When content meets code, magic happens ✨

  5. ThemeIsle

  6. See 33 other sponsors

diff --git a/index.js b/index.js new file mode 100644 index 0000000..2d3cbdf --- /dev/null +++ b/index.js @@ -0,0 +1,60 @@ +/** + * @typedef {import('hast').Root} Root + * @typedef {import('hast').Element} Element + * + * @typedef Options + * Configuration. + * @property {number|[number, number]} [age=[16, 18]] + * 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. + * Can be two numbers in an array to get two estimates. + * @property {string} [mainSelector] + * CSS selector to body of content. + * Useful to exclude other things, such as the head, ads, styles, scripts, and + * other random stuff, by focussing all strategies in one element. + */ + +import {select} from 'hast-util-select' +import {readingTime} from 'hast-util-reading-time' + +/** + * Plugin to infer file metadata from the document. + * + * @type {import('unified').Plugin<[Options?]|[], Root>} + */ +export default function rehypeInferReadingTimeMeta(options = {}) { + const {age = [14, 18], mainSelector} = options + + return (tree, file) => { + const main = mainSelector ? select(mainSelector, tree) : tree + /** @type {[number, number]|number|undefined} */ + let time + + if (main) { + if (Array.isArray(age)) { + // @ts-expect-error: hush, always two items. + time = age + .slice(0, 2) + .map((age) => readingTime(main, {age})) + .sort((a, b) => a - b) + } else { + time = readingTime(main, {age}) + } + } + + if (time) { + const matter = /** @type {Record} */ ( + file.data.matter || {} + ) + const meta = /** @type {Record} */ ( + file.data.meta || (file.data.meta = {}) + ) + + if (!matter.readingTime && !meta.readingTime) { + meta.readingTime = time + } + } + } +} diff --git a/license b/license new file mode 100644 index 0000000..f4fb31f --- /dev/null +++ b/license @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2021 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/package.json b/package.json new file mode 100644 index 0000000..7ff6bb9 --- /dev/null +++ b/package.json @@ -0,0 +1,87 @@ +{ + "name": "rehype-infer-reading-time-meta", + "version": "0.0.0", + "description": "rehype plugin to infer reading time as file metadata from the document", + "license": "MIT", + "keywords": [ + "unified", + "rehype", + "rehype-plugin", + "plugin", + "html", + "hast", + "file", + "meta", + "reading", + "time", + "estimate", + "reading-time" + ], + "repository": "rehypejs/rehype-infer-reading-time-meta", + "bugs": "https://github.com/rehypejs/rehype-infer-reading-time-meta/issues", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "author": "Titus Wormer (https://wooorm.com)", + "contributors": [ + "Titus Wormer (https://wooorm.com)" + ], + "sideEffects": false, + "type": "module", + "main": "index.js", + "types": "index.d.ts", + "files": [ + "index.d.ts", + "index.js" + ], + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-reading-time": "^1.0.0", + "hast-util-select": "^5.0.0", + "unified": "^10.0.0" + }, + "devDependencies": { + "@types/tape": "^4.0.0", + "c8": "^7.0.0", + "prettier": "^2.0.0", + "rehype": "^12.0.0", + "rehype-meta": "^3.0.0", + "remark-cli": "^10.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", + "xo": "^0.44.0" + }, + "scripts": { + "build": "rimraf \"*.d.ts\" && tsc && type-coverage", + "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", + "test-api": "node --conditions development test.js", + "test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node --conditions development test.js", + "test": "npm run build && npm run format && npm run test-coverage" + }, + "prettier": { + "tabWidth": 2, + "useTabs": false, + "singleQuote": true, + "bracketSpacing": false, + "semi": false, + "trailingComma": "none" + }, + "xo": { + "prettier": true + }, + "remarkConfig": { + "plugins": [ + "preset-wooorm" + ] + }, + "typeCoverage": { + "atLeast": 100, + "detail": true, + "strict": true, + "ignoreCatch": true + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..738f24c --- /dev/null +++ b/readme.md @@ -0,0 +1,189 @@ +# rehype-infer-reading-time-meta + +[![Build][build-badge]][build] +[![Coverage][coverage-badge]][coverage] +[![Downloads][downloads-badge]][downloads] +[![Size][size-badge]][size] +[![Sponsors][sponsors-badge]][collective] +[![Backers][backers-badge]][collective] +[![Chat][chat-badge]][chat] + +[**rehype**][rehype] plugin to infer the estimated reading time from a document +as file metadata. +This plugin sets `file.data.meta.readingTime`. +This is mostly useful with [`rehype-meta`][rehype-meta]. + +## Contents + +* [Install](#install) +* [Use](#use) +* [API](#api) + * [`unified().use(rehypeInferReadingTimeMeta, options?)`](#unifieduserehypeinferreadingtimemeta-options) +* [Security](#security) +* [Related](#related) +* [Contribute](#contribute) +* [License](#license) + +## Install + +This package is [ESM only](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c): +Node 12+ is needed to use it and it must be `import`ed instead of `require`d. + +[npm][]: + +```sh +npm install rehype-infer-reading-time-meta +``` + +## Use + +Say `example.js` looks as follows: + +```js +import {unified} from 'unified' +import rehypeParse from 'rehype-parse' +import rehypeInferReadingTimeMeta from 'rehype-infer-reading-time-meta' +import rehypeDocument from 'rehype-document' +import rehypeMeta from 'rehype-meta' +import rehypeFormat from 'rehype-format' +import rehypeStringify from 'rehype-stringify' + +main() + +async function main() { + const file = await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeInferReadingTimeMeta) + .use(rehypeDocument) + .use(rehypeMeta, {twitter: true}) + .use(rehypeFormat) + .use(rehypeStringify) + .process( + '

Build

We provide the building blocks: from tiny, focussed, modular utilities to plugins that combine them to perform bigger tasks. And much, much more. You can build on unified, mixing and matching building blocks together, to make all kinds of interesting new things.

' + ) + + console.log(String(file)) +} +``` + +Now, running `node example` yields: + +```html + + + + + + + + + + +

Build

+

We provide the building blocks: from tiny, focussed, modular utilities to plugins that combine them to perform bigger tasks. And much, much more. You can build on unified, mixing and matching building blocks together, to make all kinds of interesting new things.

+ + +``` + +## API + +This package exports no identifiers. +The default export is `rehypeInferReadingTimeMeta`. + +### `unified().use(rehypeInferReadingTimeMeta, options?)` + +Plugin to infer the estimated reading time from a document as file metadata. + +The reading time is inferred not just on words per minute, but also takes +readability into account. + +##### `options.age` + +Target age group (`number` or `[number, number]`, default: `[16, 18]`). +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. +Can be two numbers in an array to get two estimates. + +##### `options.mainSelector` + +CSS selector to body of content (`string`, optional, example: `'main'`). +Useful to exclude other things, such as the head, ads, styles, scripts, and +other random stuff, by focussing on one element. + +## Security + +Use of `rehype-infer-reading-time-meta` is safe. + +## Related + +* [`rehype-document`](https://github.com/rehypejs/rehype-document) + — Wrap a document around a fragment +* [`rehype-meta`](https://github.com/rehypejs/rehype-meta) + — Add metadata to the head of a document +* [`unified-infer-git-meta`](https://github.com/unifiedjs/unified-infer-git-meta) + — Infer file metadata from Git +* [`rehype-infer-title-meta`](https://github.com/rehypejs/rehype-infer-title-meta) + — Infer file metadata from the title of a document +* [`rehype-infer-description-meta`](https://github.com/rehypejs/rehype-infer-description-meta) + — Infer file metadata from the description of a document + +## Contribute + +See [`contributing.md`][contributing] in [`rehypejs/.github`][health] for ways +to get started. +See [`support.md`][support] for ways to get help. + +This project has a [code of conduct][coc]. +By interacting with this repository, organization, or community you agree to +abide by its terms. + +## License + +[MIT][license] © [Titus Wormer][author] + + + +[build-badge]: https://github.com/rehypejs/rehype-infer-reading-time-meta/workflows/main/badge.svg + +[build]: https://github.com/rehypejs/rehype-infer-reading-time-meta/actions + +[coverage-badge]: https://img.shields.io/codecov/c/github/rehypejs/rehype-infer-reading-time-meta.svg + +[coverage]: https://codecov.io/github/rehypejs/rehype-infer-reading-time-meta + +[downloads-badge]: https://img.shields.io/npm/dm/rehype-infer-reading-time-meta.svg + +[downloads]: https://www.npmjs.com/package/rehype-infer-reading-time-meta + +[size-badge]: https://img.shields.io/bundlephobia/minzip/rehype-infer-reading-time-meta.svg + +[size]: https://bundlephobia.com/result?p=rehype-infer-reading-time-meta + +[sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg + +[backers-badge]: https://opencollective.com/unified/backers/badge.svg + +[collective]: https://opencollective.com/unified + +[chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg + +[chat]: https://github.com/rehypejs/rehype/discussions + +[npm]: https://docs.npmjs.com/cli/install + +[health]: https://github.com/rehypejs/.github + +[contributing]: https://github.com/rehypejs/.github/blob/HEAD/contributing.md + +[support]: https://github.com/rehypejs/.github/blob/HEAD/support.md + +[coc]: https://github.com/rehypejs/.github/blob/HEAD/code-of-conduct.md + +[license]: license + +[author]: https://wooorm.com + +[rehype]: https://github.com/rehypejs/rehype + +[rehype-meta]: https://github.com/rehypejs/rehype-meta diff --git a/test.js b/test.js new file mode 100644 index 0000000..b0accc7 --- /dev/null +++ b/test.js @@ -0,0 +1,47 @@ +import fs from 'node:fs' +import test from 'tape' +import {rehype} from 'rehype' +import rehypeMeta from 'rehype-meta' +import rehypeInferReadingTimeMeta from './index.js' + +const buf = fs.readFileSync('fixture.html') + +test('rehypeInferReadingTimeMeta', async (t) => { + t.deepEqual( + (await rehype().use(rehypeInferReadingTimeMeta).process(buf)).data, + {meta: {readingTime: [2.276_744, 3.477_06]}}, + 'should estimate reading time' + ) + + t.deepEqual( + ( + await rehype() + .use(rehypeInferReadingTimeMeta, {mainSelector: 'main'}) + .process(buf) + ).data, + {meta: {readingTime: [2.078_285, 3.173_011]}}, + 'should support `mainSelector`' + ) + + t.deepEqual( + (await rehype().use(rehypeInferReadingTimeMeta, {age: 16}).process(buf)) + .data, + {meta: {readingTime: 2.751_701}}, + 'should estimate reading time for one age' + ) + + t.equal( + String( + await rehype() + .use(rehypeInferReadingTimeMeta) + .use(rehypeMeta, {twitter: true}) + .process( + '

Build

We provide the building blocks: from tiny, focussed, modular utilities to plugins that combine them to perform bigger tasks. And much, much more. You can build on unified, mixing and matching building blocks together, to make all kinds of interesting new things.

' + ) + ), + '\n\n\n\n

Build

We provide the building blocks: from tiny, focussed, modular utilities to plugins that combine them to perform bigger tasks. And much, much more. You can build on unified, mixing and matching building blocks together, to make all kinds of interesting new things.

', + 'should integrate w/ `rehype-meta`' + ) + + t.end() +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e31adf8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include": ["*.js"], + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020"], + "module": "ES2020", + "moduleResolution": "node", + "allowJs": true, + "checkJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "strict": true + } +}