├── .editorconfig ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── index.d.ts ├── index.js ├── lib └── index.js ├── license ├── package.json ├── readme.md ├── test.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/bb.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | main: 3 | runs-on: ubuntu-latest 4 | steps: 5 | - uses: unifiedjs/beep-boop-beta@main 6 | with: 7 | repo-token: ${{secrets.GITHUB_TOKEN}} 8 | name: bb 9 | on: 10 | issues: 11 | types: [closed, edited, labeled, opened, reopened, unlabeled] 12 | pull_request_target: 13 | types: [closed, edited, labeled, opened, reopened, unlabeled] 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | main: 3 | name: ${{matrix.node}} 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v4 7 | - uses: actions/setup-node@v4 8 | with: 9 | node-version: ${{matrix.node}} 10 | - run: npm install 11 | - run: npm test 12 | - uses: codecov/codecov-action@v5 13 | strategy: 14 | matrix: 15 | node: 16 | - lts/hydrogen 17 | - node 18 | name: main 19 | on: 20 | - pull_request 21 | - push 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | *.log 3 | *.map 4 | *.tsbuildinfo 5 | .DS_Store 6 | coverage/ 7 | node_modules/ 8 | yarn.lock 9 | !/index.d.ts 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type {Options} from 'mdast-util-to-nlcst' 2 | export {default} from './lib/index.js' 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Note: types exposed from `index.d.ts`. 2 | export {default} from './lib/index.js' 3 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {Root as MdastRoot} from 'mdast' 3 | * @import {Options, ParserConstructor, ParserInstance} from 'mdast-util-to-nlcst' 4 | * @import {Root as NlcstRoot} from 'nlcst' 5 | * @import {Processor} from 'unified' 6 | * @import {VFile} from 'vfile' 7 | */ 8 | 9 | /** 10 | * @typedef {ParserConstructor | ParserInstance} Parser 11 | */ 12 | 13 | /** 14 | * @callback TransformBridge 15 | * Bridge-mode. 16 | * 17 | * Runs the destination with the new nlcst tree. 18 | * Discards result. 19 | * @param {MdastRoot} tree 20 | * Tree. 21 | * @param {VFile} file 22 | * File. 23 | * @returns {Promise} 24 | * Nothing. 25 | */ 26 | 27 | /** 28 | * @callback TransformMutate 29 | * Mutate-mode. 30 | * 31 | * Further transformers run on the nlcst tree. 32 | * 33 | * @param {MdastRoot} tree 34 | * Tree. 35 | * @param {VFile} file 36 | * File. 37 | * @returns {NlcstRoot} 38 | * Tree (nlcst). 39 | */ 40 | 41 | import {toNlcst} from 'mdast-util-to-nlcst' 42 | import {ParseLatin} from 'parse-latin' 43 | 44 | /** 45 | * Bridge or mutate to retext. 46 | * 47 | * ###### Notes 48 | * 49 | * * if a processor is given, uses its parser to create a new nlcst tree, 50 | * then runs the plugins attached to with that ([*bridge mode*][bridge]); 51 | * you can add a parser to processor for example with `retext-english`; other 52 | * plugins used on the processor should be retext plugins 53 | * * if a parser is given, uses it to create a new nlcst tree, and returns 54 | * it (*mutate mode*); you can get a parser by importing `Parser` from 55 | * `retext-english` for example; other plugins used after `remarkRetext` 56 | * should be retext plugins 57 | * 58 | * @overload 59 | * @param {Processor} processor 60 | * @param {Options | null | undefined} [options] 61 | * @returns {TransformBridge} 62 | * 63 | * @overload 64 | * @param {Parser} parser 65 | * @param {Options | null | undefined} [options] 66 | * @returns {TransformMutate} 67 | * 68 | * @overload 69 | * @param {Parser | Processor} destination 70 | * @param {Options | null | undefined} [options] 71 | * @returns {TransformBridge | TransformMutate} 72 | * 73 | * @param {Parser | Processor} destination 74 | * Parser or processor (required). 75 | * @param {Options | null | undefined} [options] 76 | * Configuration (optional). 77 | * @returns {TransformBridge | TransformMutate} 78 | * Transform. 79 | */ 80 | export default function remarkRetext(destination, options) { 81 | if (!destination) { 82 | throw new Error( 83 | 'Expected `parser` (such as from `parse-english`) or `processor` (a unified pipeline) as `destination`' 84 | ) 85 | } 86 | 87 | if ('run' in destination) { 88 | const processor = destination.freeze() 89 | 90 | /** 91 | * @type {TransformBridge} 92 | */ 93 | return async function (tree, file) { 94 | const parser = parserFromRetextParse(processor) 95 | const nlcstTree = toNlcst(tree, file, parser, options) 96 | await processor.run(nlcstTree, file) 97 | } 98 | } 99 | 100 | const parser = destination 101 | 102 | /** 103 | * @type {TransformMutate} 104 | */ 105 | return function (tree, file) { 106 | return toNlcst(tree, file, parser, options) 107 | } 108 | } 109 | 110 | /** 111 | * 112 | * @param {Processor} processor 113 | * @returns {ParseLatin} 114 | */ 115 | function parserFromRetextParse(processor) { 116 | const parser = new ParseLatin() 117 | add( 118 | parser.tokenizeParagraphPlugins, 119 | processor.data('nlcstParagraphExtensions') 120 | ) 121 | add(parser.tokenizeRootPlugins, processor.data('nlcstRootExtensions')) 122 | add(parser.tokenizeSentencePlugins, processor.data('nlcstSentenceExtensions')) 123 | 124 | return parser 125 | 126 | /** 127 | * @template T 128 | * @param {Array} list 129 | * @param {Array | undefined} values 130 | */ 131 | function add(list, values) { 132 | /* c8 ignore next -- plugins like `retext-emoji`. */ 133 | if (values) list.unshift(...values) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /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 | "bugs": "https://github.com/remarkjs/remark-retext/issues", 4 | "contributors": [ 5 | "Titus Wormer (https://wooorm.com)" 6 | ], 7 | "dependencies": { 8 | "@types/mdast": "^4.0.0", 9 | "@types/nlcst": "^2.0.0", 10 | "mdast-util-to-nlcst": "^7.0.0", 11 | "parse-latin": "^7.0.0", 12 | "unified": "^11.0.0", 13 | "vfile": "^6.0.0" 14 | }, 15 | "description": "remark plugin to support retext", 16 | "devDependencies": { 17 | "@types/node": "^22.0.0", 18 | "c8": "^10.0.0", 19 | "parse-english": "^7.0.0", 20 | "prettier": "^3.0.0", 21 | "remark-cli": "^12.0.0", 22 | "remark-parse": "^11.0.0", 23 | "remark-preset-wooorm": "^11.0.0", 24 | "remark-stringify": "^11.0.0", 25 | "retext-english": "^5.0.0", 26 | "retext-stringify": "^4.0.0", 27 | "type-coverage": "^2.0.0", 28 | "typescript": "^5.0.0", 29 | "xo": "^0.60.0" 30 | }, 31 | "exports": "./index.js", 32 | "files": [ 33 | "index.d.ts.map", 34 | "index.d.ts", 35 | "index.js", 36 | "lib/" 37 | ], 38 | "funding": { 39 | "type": "opencollective", 40 | "url": "https://opencollective.com/unified" 41 | }, 42 | "keywords": [ 43 | "markdown", 44 | "mdast", 45 | "natural language", 46 | "nlcst", 47 | "plugin", 48 | "prose", 49 | "remark", 50 | "remark-plugin", 51 | "retext", 52 | "unified" 53 | ], 54 | "license": "MIT", 55 | "name": "remark-retext", 56 | "prettier": { 57 | "bracketSpacing": false, 58 | "singleQuote": true, 59 | "semi": false, 60 | "tabWidth": 2, 61 | "trailingComma": "none", 62 | "useTabs": false 63 | }, 64 | "remarkConfig": { 65 | "plugins": [ 66 | "remark-preset-wooorm" 67 | ] 68 | }, 69 | "repository": "remarkjs/remark-retext", 70 | "scripts": { 71 | "build": "tsc --build --clean && tsc --build && type-coverage", 72 | "format": "remark --frail --output --quiet -- . && prettier --log-level warn --write -- . && xo --fix", 73 | "test-api": "node --conditions development test.js", 74 | "test-coverage": "c8 --100 --reporter lcov -- npm run test-api", 75 | "test": "npm run build && npm run format && npm run test-coverage" 76 | }, 77 | "sideEffects": false, 78 | "typeCoverage": { 79 | "atLeast": 100, 80 | "strict": true 81 | }, 82 | "type": "module", 83 | "version": "6.0.1", 84 | "xo": { 85 | "prettier": true 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # remark-retext 2 | 3 | [![Build][badge-build-image]][badge-build-url] 4 | [![Coverage][badge-coverage-image]][badge-coverage-url] 5 | [![Downloads][badge-downloads-image]][badge-downloads-url] 6 | [![Size][badge-size-image]][badge-size-url] 7 | 8 | **[remark][github-remark]** plugin to support 9 | **[retext][github-retext]**. 10 | 11 | ## Contents 12 | 13 | * [What is this?](#what-is-this) 14 | * [When should I use this?](#when-should-i-use-this) 15 | * [Install](#install) 16 | * [Use](#use) 17 | * [API](#api) 18 | * [`unified().use(remarkRetext, destination[, options])`](#unifieduseremarkretext-destination-options) 19 | * [`Options`](#options) 20 | * [Types](#types) 21 | * [Compatibility](#compatibility) 22 | * [Security](#security) 23 | * [Related](#related) 24 | * [Contribute](#contribute) 25 | * [License](#license) 26 | 27 | ## What is this? 28 | 29 | This package is a [unified][github-unified] ([remark][github-remark]) plugin to 30 | support [retext][github-retext]. 31 | 32 | ## When should I use this? 33 | 34 | This project is useful if you want to check natural language in markdown. 35 | The retext ecosystem has many useful plugins to check prose, 36 | such as [`retext-indefinite-article`][github-retext-indefinite-article] 37 | which checks that `a` and `an` are used correctly, 38 | or [`retext-readability`][github-retext-readability] which checks that sentences 39 | are not too complex. 40 | This plugins lets you use them on markdown documents. 41 | 42 | This plugin is not able to apply changes by retext plugins (such 43 | as done by `retext-smartypants`) to the markdown content. 44 | 45 | This plugin is built on [`mdast-util-to-nlcst`][github-mdast-util-to-nlcst], 46 | which does the work on syntax trees. 47 | remark focusses on making it easier to transform content by abstracting such 48 | internals away. 49 | 50 | ## Install 51 | 52 | This package is [ESM only][github-gist-esm]. 53 | In Node.js (version 16+), 54 | install with [npm][npmjs-install]: 55 | 56 | ```sh 57 | npm install remark-retext 58 | ``` 59 | 60 | In Deno with [`esm.sh`][esmsh]: 61 | 62 | ```js 63 | import remarkRetext from 'https://esm.sh/remark-retext@6' 64 | ``` 65 | 66 | In browsers with [`esm.sh`][esmsh]: 67 | 68 | ```html 69 | 72 | ``` 73 | 74 | ## Use 75 | 76 | Say we have the following file `example.md`: 77 | 78 | ```markdown 79 | ## Hello guys! 80 | ``` 81 | 82 | …and a module `example.js`: 83 | 84 | ```js 85 | import remarkParse from 'remark-parse' 86 | import remarkRetext from 'remark-retext' 87 | import remarkStringify from 'remark-stringify' 88 | import retextEnglish from 'retext-english' 89 | import retextEquality from 'retext-equality' 90 | import {read} from 'to-vfile' 91 | import {unified} from 'unified' 92 | import {reporter} from 'vfile-reporter' 93 | 94 | const file = await unified() 95 | .use(remarkParse) 96 | .use(remarkRetext, unified().use(retextEnglish).use(retextEquality)) 97 | .use(remarkStringify) 98 | .process(await read('example.md')) 99 | 100 | console.error(reporter(file)) 101 | ``` 102 | 103 | …then running `node example.js` yields: 104 | 105 | ```text 106 | example.md 107 | 1:10-1:14 warning Unexpected potentially insensitive use of `guys`, in somes cases `people`, `persons`, `folks` may be better gals-man retext-equality 108 | 109 | ⚠ 1 warning 110 | ``` 111 | 112 | ## API 113 | 114 | This package exports no identifiers. 115 | The default export is [`remarkRetext`][api-remark-retext]. 116 | 117 | ### `unified().use(remarkRetext, destination[, options])` 118 | 119 | Bridge or mutate to retext. 120 | 121 | ###### Parameters 122 | 123 | * `destination` ([`Parser`][github-unified-parser] or 124 | [`Processor`][github-unified-processor]) 125 | — configuration (required) 126 | 127 | ###### Returns 128 | 129 | Transform ([`Transformer`][github-unified-transformer]). 130 | 131 | ###### Notes 132 | 133 | * if a [processor][github-unified-processor] is given, 134 | uses its parser to create a new nlcst tree, 135 | then runs the plugins attached to with that 136 | (*[bridge mode][github-unified-mode]*); 137 | you can add a parser to processor for example with `parse-english`; 138 | other plugins used on the processor should be retext plugins 139 | * if a [parser][github-unified-parser] is given, 140 | uses it to create a new nlcst tree, 141 | and returns it (*[mutate mode][github-unified-mode]*); 142 | you can get a parser by importing `Parser` from `parse-english` for example; 143 | other plugins used after `remarkRetext` should be retext plugins 144 | 145 | ### `Options` 146 | 147 | Configuration (TypeScript type). 148 | 149 | ###### Fields 150 | 151 | * `options.ignore` 152 | (`Array`, optional) 153 | — list of [mdast][github-mdast] node types to ignore; 154 | the types `'table'`, `'tableRow'`, and `'tableCell'` are always ignored 155 | * `options.source` 156 | (`Array`, optional) 157 | — list of [mdast][github-mdast] node types to mark as [nlcst][github-nlcst] 158 | source nodes; 159 | the type `'inlineCode'` is always marked as source 160 | 161 | ## Types 162 | 163 | This package is fully typed with [TypeScript][]. 164 | It exports the additional type [`Options`][api-options]. 165 | 166 | ## Compatibility 167 | 168 | Projects maintained by the unified collective are compatible with maintained 169 | versions of Node.js. 170 | 171 | When we cut a new major release, 172 | we drop support for unmaintained versions of Node. 173 | This means we try to keep the current release line, 174 | `remark-retext@6`, 175 | compatible with Node.js 16. 176 | 177 | This plugin works with `unified` version 6+, 178 | `remark` version 3+, 179 | and `retext` version 7+. 180 | 181 | ## Security 182 | 183 | Use of `remark-retext` does not involve **[rehype][github-rehype]** 184 | (**[hast][github-hast]**) or user 185 | content so there are no openings for 186 | [cross-site scripting (XSS)][wikipedia-xss] 187 | attacks. 188 | 189 | ## Related 190 | 191 | * [`rehype-retext`](https://github.com/rehypejs/rehype-retext) 192 | — transform HTML ([hast][github-hast]) to natural language 193 | ([nlcst][github-nlcst]) 194 | * [`remark-rehype`](https://github.com/remarkjs/remark-rehype) 195 | — transform markdown ([mdast][github-mdast]) to HTML ([hast][github-hast]) 196 | * [`rehype-remark`](https://github.com/rehypejs/rehype-remark) 197 | — transform HTML ([hast][github-hast]) to markdown ([mdast][github-mdast]) 198 | * [`mdast-util-to-nlcst`][github-mdast-util-to-nlcst] 199 | — underlying algorithm 200 | 201 | ## Contribute 202 | 203 | See [`contributing.md`][health-contributing] in [`remarkjs/.github`][health] for 204 | ways to get started. 205 | See [`support.md`][health-support] for ways to get help. 206 | 207 | This project has a [code of conduct][health-coc]. 208 | By interacting with this repository, 209 | organization, 210 | or community you agree to abide by its terms. 211 | 212 | ## License 213 | 214 | [MIT][file-license] © [Titus Wormer][wooorm] 215 | 216 | 217 | 218 | [api-options]: #options 219 | 220 | [api-remark-retext]: #unifieduseremarkretext-destination-options 221 | 222 | [badge-build-image]: https://github.com/remarkjs/remark-retext/workflows/main/badge.svg 223 | 224 | [badge-build-url]: https://github.com/remarkjs/remark-retext/actions 225 | 226 | [badge-coverage-image]: https://img.shields.io/codecov/c/github/remarkjs/remark-retext.svg 227 | 228 | [badge-coverage-url]: https://codecov.io/github/remarkjs/remark-retext 229 | 230 | [badge-downloads-image]: https://img.shields.io/npm/dm/remark-retext.svg 231 | 232 | [badge-downloads-url]: https://www.npmjs.com/package/remark-retext 233 | 234 | [badge-size-image]: https://img.shields.io/bundlejs/size/remark-retext 235 | 236 | [badge-size-url]: https://bundlejs.com/?q=remark-retext 237 | 238 | [esmsh]: https://esm.sh 239 | 240 | [file-license]: license 241 | 242 | [github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 243 | 244 | [github-hast]: https://github.com/syntax-tree/hast 245 | 246 | [github-mdast]: https://github.com/syntax-tree/mdast 247 | 248 | [github-mdast-util-to-nlcst]: https://github.com/syntax-tree/mdast-util-to-nlcst 249 | 250 | [github-nlcst]: https://github.com/syntax-tree/nlcst 251 | 252 | [github-rehype]: https://github.com/rehypejs/rehype 253 | 254 | [github-remark]: https://github.com/remarkjs/remark 255 | 256 | [github-retext]: https://github.com/retextjs/retext 257 | 258 | [github-retext-indefinite-article]: https://github.com/retextjs/retext-indefinite-article 259 | 260 | [github-retext-readability]: https://github.com/retextjs/retext-readability 261 | 262 | [github-unified]: https://github.com/unifiedjs/unified 263 | 264 | [github-unified-mode]: https://github.com/unifiedjs/unified#processing-between-syntaxes 265 | 266 | [github-unified-parser]: https://github.com/unifiedjs/unified#parser 267 | 268 | [github-unified-processor]: https://github.com/unifiedjs/unified#processor 269 | 270 | [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer 271 | 272 | [health]: https://github.com/remarkjs/.github 273 | 274 | [health-coc]: https://github.com/remarkjs/.github/blob/main/code-of-conduct.md 275 | 276 | [health-contributing]: https://github.com/remarkjs/.github/blob/main/contributing.md 277 | 278 | [health-support]: https://github.com/remarkjs/.github/blob/main/support.md 279 | 280 | [npmjs-install]: https://docs.npmjs.com/cli/install 281 | 282 | [typescript]: https://www.typescriptlang.org 283 | 284 | [wikipedia-xss]: https://en.wikipedia.org/wiki/Cross-site_scripting 285 | 286 | [wooorm]: https://wooorm.com 287 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import test from 'node:test' 3 | import {ParseEnglish} from 'parse-english' 4 | import remarkParse from 'remark-parse' 5 | import remarkRetext from 'remark-retext' 6 | import remarkStringify from 'remark-stringify' 7 | import retextEnglish from 'retext-english' 8 | import retextStringify from 'retext-stringify' 9 | import {unified} from 'unified' 10 | 11 | test('remarkRetext', async function (t) { 12 | await t.test('should expose the public api', async function () { 13 | assert.deepEqual(Object.keys(await import('remark-retext')).sort(), [ 14 | 'default' 15 | ]) 16 | }) 17 | 18 | await t.test('should throw when w/o parser or processor', async function () { 19 | assert.throws(function () { 20 | // @ts-expect-error: check how missing options is handled. 21 | unified().use(remarkRetext).freeze() 22 | }, /Expected `parser` \(such as from `parse-english`\) or `processor` \(a unified pipeline\) as `destination`/) 23 | }) 24 | 25 | await t.test('should mutate', async function () { 26 | const file = await unified() 27 | .use(remarkParse) 28 | .use(remarkRetext, ParseEnglish) 29 | .use(retextStringify) 30 | .process('## Hello, world! ##') 31 | 32 | assert.equal(String(file), 'Hello, world!') 33 | }) 34 | 35 | await t.test('should bridge', async function () { 36 | const file = await unified() 37 | .use(remarkParse) 38 | .use(remarkRetext, unified().use(retextEnglish)) 39 | .use(remarkStringify) 40 | .process('## Hello, world! ##') 41 | 42 | assert.equal(String(file), '## Hello, world!\n') 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declaration": true, 6 | "declarationMap": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "lib": ["es2022"], 10 | "module": "node16", 11 | "strict": true, 12 | "target": "es2022" 13 | }, 14 | "exclude": ["coverage/", "node_modules/"], 15 | "include": ["**/*.js", "index.d.ts"] 16 | } 17 | --------------------------------------------------------------------------------