├── .editorconfig ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── 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 | name: bb 2 | on: 3 | issues: 4 | types: [opened, reopened, edited, closed, labeled, unlabeled] 5 | pull_request_target: 6 | types: [opened, reopened, edited, closed, labeled, unlabeled] 7 | jobs: 8 | main: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: unifiedjs/beep-boop-beta@main 12 | with: 13 | repo-token: ${{secrets.GITHUB_TOKEN}} 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | main: 7 | name: ${{matrix.node}} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: ${{matrix.node}} 14 | - run: npm install 15 | - run: npm test 16 | - uses: codecov/codecov-action@v3 17 | strategy: 18 | matrix: 19 | node: 20 | - lts/gallium 21 | - node 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | .DS_Store 4 | *.d.ts 5 | *.log 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export {default} from './lib/index.js' 2 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('mdast').Root} Root 3 | */ 4 | 5 | import {newlineToBreak} from 'mdast-util-newline-to-break' 6 | 7 | /** 8 | * Support hard breaks without needing spaces or escapes (turns enters into 9 | * `
`s). 10 | * 11 | * @returns 12 | * Transform. 13 | */ 14 | export default function remarkBreaks() { 15 | /** 16 | * Transform. 17 | * 18 | * @param {Root} tree 19 | * Tree. 20 | * @returns {undefined} 21 | * Nothing. 22 | */ 23 | return function (tree) { 24 | newlineToBreak(tree) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2017 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 | "name": "remark-breaks", 3 | "version": "4.0.0", 4 | "description": "remark plugin to add break support, without needing spaces", 5 | "license": "MIT", 6 | "keywords": [ 7 | "break", 8 | "linefeed", 9 | "markdown", 10 | "mdast", 11 | "newline", 12 | "plugin", 13 | "remark", 14 | "remark-plugin", 15 | "unified" 16 | ], 17 | "repository": "remarkjs/remark-breaks", 18 | "bugs": "https://github.com/remarkjs/remark-breaks/issues", 19 | "funding": { 20 | "type": "opencollective", 21 | "url": "https://opencollective.com/unified" 22 | }, 23 | "author": "Titus Wormer (https://wooorm.com)", 24 | "contributors": [ 25 | "Titus Wormer (https://wooorm.com)" 26 | ], 27 | "sideEffects": false, 28 | "type": "module", 29 | "exports": "./index.js", 30 | "files": [ 31 | "lib/", 32 | "index.d.ts", 33 | "index.js" 34 | ], 35 | "dependencies": { 36 | "@types/mdast": "^4.0.0", 37 | "mdast-util-newline-to-break": "^2.0.0", 38 | "unified": "^11.0.0" 39 | }, 40 | "devDependencies": { 41 | "@types/node": "^20.0.0", 42 | "c8": "^8.0.0", 43 | "prettier": "^3.0.0", 44 | "rehype-stringify": "^10.0.0", 45 | "remark-cli": "^11.0.0", 46 | "remark-parse": "^11.0.0", 47 | "remark-preset-wooorm": "^9.0.0", 48 | "remark-rehype": "^11.0.0", 49 | "type-coverage": "^2.0.0", 50 | "typescript": "^5.0.0", 51 | "xo": "^0.56.0" 52 | }, 53 | "scripts": { 54 | "build": "tsc --build --clean && tsc --build && type-coverage", 55 | "format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", 56 | "prepack": "npm run build && npm run format", 57 | "test": "npm run build && npm run format && npm run test-coverage", 58 | "test-api": "node --conditions development test.js", 59 | "test-coverage": "c8 --100 --reporter lcov npm run test-api" 60 | }, 61 | "prettier": { 62 | "bracketSpacing": false, 63 | "singleQuote": true, 64 | "semi": false, 65 | "tabWidth": 2, 66 | "trailingComma": "none", 67 | "useTabs": false 68 | }, 69 | "remarkConfig": { 70 | "plugins": [ 71 | "remark-preset-wooorm" 72 | ] 73 | }, 74 | "typeCoverage": { 75 | "atLeast": 100, 76 | "detail": true, 77 | "ignoreCatch": true, 78 | "strict": true 79 | }, 80 | "xo": { 81 | "overrides": [ 82 | { 83 | "files": [ 84 | "test.js" 85 | ], 86 | "rules": { 87 | "no-await-in-loop": "off" 88 | } 89 | } 90 | ], 91 | "prettier": true 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # remark-breaks 2 | 3 | [![Build][build-badge]][build] 4 | [![Coverage][coverage-badge]][coverage] 5 | [![Downloads][downloads-badge]][downloads] 6 | [![Size][size-badge]][size] 7 | [![Sponsors][sponsors-badge]][collective] 8 | [![Backers][backers-badge]][collective] 9 | [![Chat][chat-badge]][chat] 10 | 11 | **[remark][]** plugin to support hard breaks without needing spaces or escapes 12 | (turns enters into `
`s). 13 | 14 | ## Contents 15 | 16 | * [What is this?](#what-is-this) 17 | * [When should I use this?](#when-should-i-use-this) 18 | * [Install](#install) 19 | * [Use](#use) 20 | * [API](#api) 21 | * [`unified().use(remarkBreaks)`](#unifieduseremarkbreaks) 22 | * [Syntax](#syntax) 23 | * [Syntax tree](#syntax-tree) 24 | * [Types](#types) 25 | * [Compatibility](#compatibility) 26 | * [Security](#security) 27 | * [Related](#related) 28 | * [Contribute](#contribute) 29 | * [License](#license) 30 | 31 | ## What is this? 32 | 33 | This package is a [unified][] ([remark][]) plugin to turn soft line endings 34 | (enters) into hard breaks (`
`s) 35 | 36 | ## When should I use this? 37 | 38 | This plugin is useful if you want to display user content closer to how it was 39 | authored, because when a user includes a line ending, it’ll show as such. 40 | GitHub does this in a few places (comments, issues, PRs, and releases), but it’s 41 | not semantic according to HTML and not compliant to markdown. 42 | Markdown already has two ways to include hard breaks, namely trailing spaces and 43 | escapes (note that `␠` represents a normal space): 44 | 45 | ```markdown 46 | lorem␠␠ 47 | ipsum 48 | 49 | lorem\ 50 | ipsum 51 | ``` 52 | 53 | Both will turn into `
`s. 54 | If you control who authors content or can document how markdown works, it’s 55 | recommended to use escapes instead. 56 | 57 | ## Install 58 | 59 | This package is [ESM only][esm]. 60 | In Node.js (version 16+), install with [npm][]: 61 | 62 | ```sh 63 | npm install remark-breaks 64 | ``` 65 | 66 | In Deno with [`esm.sh`][esmsh]: 67 | 68 | ```js 69 | import remarkBreaks from 'https://esm.sh/remark-breaks@4' 70 | ``` 71 | 72 | In browsers with [`esm.sh`][esmsh]: 73 | 74 | ```html 75 | 78 | ``` 79 | 80 | ## Use 81 | 82 | Say we have the following file `example.md` (note: there are no spaces after 83 | `a`): 84 | 85 | ```markdown 86 | Mars is 87 | the fourth planet 88 | ``` 89 | 90 | …and a module `example.js`: 91 | 92 | ```js 93 | import rehypeStringify from 'rehype-stringify' 94 | import remarkBreaks from 'remark-breaks' 95 | import remarkParse from 'remark-parse' 96 | import remarkRehype from 'remark-rehype' 97 | import {read} from 'to-vfile' 98 | import {unified} from 'unified' 99 | 100 | const file = await unified() 101 | .use(remarkParse) 102 | .use(remarkBreaks) 103 | .use(remarkRehype) 104 | .use(rehypeStringify) 105 | .process(await read('example.md')) 106 | 107 | console.log(String(file)) 108 | ``` 109 | 110 | …then running `node example.js` yields: 111 | 112 | ```html 113 |

Mars is
114 | the fourth planet

115 | ``` 116 | 117 | > 👉 **Note**: Without `remark-breaks`, you’d get: 118 | > 119 | > ```html 120 | >

Mars is 121 | > the fourth planet

122 | > ``` 123 | 124 | ## API 125 | 126 | This package exports no identifiers. 127 | The default export is [`remarkBreaks`][api-remark-breaks]. 128 | 129 | ### `unified().use(remarkBreaks)` 130 | 131 | Support hard breaks without needing spaces or escapes (turns enters into 132 | `
`s). 133 | 134 | ###### Parameters 135 | 136 | There are no parameters. 137 | 138 | ###### Returns 139 | 140 | Transform ([`Transformer`][unified-transformer]). 141 | 142 | ## Syntax 143 | 144 | This plugin looks for markdown line endings (`\r`, `\n`, and `\r\n`) preceded 145 | by zero or more spaces and tabs. 146 | 147 | ## Syntax tree 148 | 149 | This plugin adds mdast [`Break`][mdast-break] nodes to the syntax tree. 150 | These are the same nodes that represent breaks with spaces or escapes. 151 | 152 | ## Types 153 | 154 | This package is fully typed with [TypeScript][]. 155 | It exports no additional types. 156 | 157 | ## Compatibility 158 | 159 | Projects maintained by the unified collective are compatible with maintained 160 | versions of Node.js. 161 | 162 | When we cut a new major release, we drop support for unmaintained versions of 163 | Node. 164 | This means we try to keep the current release line, `remark-breaks@^4`, 165 | compatible with Node.js 16. 166 | 167 | This plugin works with `unified` version 6+ and `remark` version 7+. 168 | 169 | ## Security 170 | 171 | Use of `remark-breaks` does not involve **[rehype][]** (**[hast][]**) or user 172 | content so there are no openings for [cross-site scripting (XSS)][wiki-xss] 173 | attacks. 174 | 175 | ## Related 176 | 177 | * [`remark-gfm`](https://github.com/remarkjs/remark-gfm) 178 | — support GFM (autolink literals, footnotes, strikethrough, tables, 179 | tasklists) 180 | * [`remark-github`](https://github.com/remarkjs/remark-github) 181 | — link references to commits, issues, and users, in the same way that 182 | GitHub does 183 | * [`remark-directive`](https://github.com/remarkjs/remark-directive) 184 | — support directives 185 | * [`remark-frontmatter`](https://github.com/remarkjs/remark-frontmatter) 186 | — support frontmatter (YAML, TOML, and more) 187 | * [`remark-math`](https://github.com/remarkjs/remark-math) 188 | — support math 189 | 190 | ## Contribute 191 | 192 | See [`contributing.md`][contributing] in [`remarkjs/.github`][health] for ways 193 | to get started. 194 | See [`support.md`][support] for ways to get help. 195 | 196 | This project has a [code of conduct][coc]. 197 | By interacting with this repository, organization, or community you agree to 198 | abide by its terms. 199 | 200 | ## License 201 | 202 | [MIT][license] © [Titus Wormer][author] 203 | 204 | 205 | 206 | [build-badge]: https://github.com/remarkjs/remark-breaks/workflows/main/badge.svg 207 | 208 | [build]: https://github.com/remarkjs/remark-breaks/actions 209 | 210 | [coverage-badge]: https://img.shields.io/codecov/c/github/remarkjs/remark-breaks.svg 211 | 212 | [coverage]: https://codecov.io/github/remarkjs/remark-breaks 213 | 214 | [downloads-badge]: https://img.shields.io/npm/dm/remark-breaks.svg 215 | 216 | [downloads]: https://www.npmjs.com/package/remark-breaks 217 | 218 | [size-badge]: https://img.shields.io/bundlejs/size/remark-breaks 219 | 220 | [size]: https://bundlejs.com/?q=remark-breaks 221 | 222 | [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg 223 | 224 | [backers-badge]: https://opencollective.com/unified/backers/badge.svg 225 | 226 | [collective]: https://opencollective.com/unified 227 | 228 | [chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg 229 | 230 | [chat]: https://github.com/remarkjs/remark/discussions 231 | 232 | [npm]: https://docs.npmjs.com/cli/install 233 | 234 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 235 | 236 | [esmsh]: https://esm.sh 237 | 238 | [health]: https://github.com/remarkjs/.github 239 | 240 | [contributing]: https://github.com/remarkjs/.github/blob/main/contributing.md 241 | 242 | [support]: https://github.com/remarkjs/.github/blob/main/support.md 243 | 244 | [coc]: https://github.com/remarkjs/.github/blob/main/code-of-conduct.md 245 | 246 | [license]: license 247 | 248 | [author]: https://wooorm.com 249 | 250 | [hast]: https://github.com/syntax-tree/hast 251 | 252 | [mdast-break]: https://github.com/syntax-tree/mdast#break 253 | 254 | [rehype]: https://github.com/rehypejs/rehype 255 | 256 | [remark]: https://github.com/remarkjs/remark 257 | 258 | [typescript]: https://www.typescriptlang.org 259 | 260 | [unified]: https://github.com/unifiedjs/unified 261 | 262 | [unified-transformer]: https://github.com/unifiedjs/unified#transformer 263 | 264 | [wiki-xss]: https://en.wikipedia.org/wiki/Cross-site_scripting 265 | 266 | [api-remark-breaks]: #unifieduseremarkbreaks 267 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import test from 'node:test' 3 | import rehypeStringify from 'rehype-stringify' 4 | import remarkBreaks from 'remark-breaks' 5 | import remarkParse from 'remark-parse' 6 | import remarkRehype from 'remark-rehype' 7 | import {unified} from 'unified' 8 | 9 | test('remarkBreaks', async function (t) { 10 | await t.test('should expose the public api', async function () { 11 | assert.deepEqual(Object.keys(await import('remark-breaks')).sort(), [ 12 | 'default' 13 | ]) 14 | }) 15 | }) 16 | 17 | test('fixtures', async function (t) { 18 | const fixtures = [ 19 | { 20 | in: 'This is a\nparagraph.', 21 | out: '

This is a
\nparagraph.

', 22 | name: 'no space' 23 | }, 24 | { 25 | in: 'This is a \nparagraph.', 26 | out: '

This is a
\nparagraph.

', 27 | name: 'one space' 28 | }, 29 | { 30 | in: 'This is a \nparagraph.', 31 | out: '

This is a
\nparagraph.

', 32 | name: 'two spaces' 33 | }, 34 | { 35 | in: 'This is a \nparagraph.', 36 | out: '

This is a
\nparagraph.

', 37 | name: 'three spaces' 38 | }, 39 | { 40 | in: 'This is a\rparagraph.', 41 | out: '

This is a
\nparagraph.

', 42 | name: 'carriage return' 43 | }, 44 | { 45 | in: 'This is a\r\nparagraph.', 46 | out: '

This is a
\nparagraph.

', 47 | name: 'carriage return + line feed' 48 | }, 49 | { 50 | in: 'After *phrasing*\nmore.', 51 | out: '

After phrasing
\nmore.

', 52 | name: 'after phrasing' 53 | }, 54 | { 55 | in: 'Before\n*phrasing*.', 56 | out: '

Before
\nphrasing.

', 57 | name: 'before phrasing' 58 | }, 59 | { 60 | in: 'Mul\nti\nple.', 61 | out: '

Mul
\nti
\nple.

', 62 | name: 'multiple' 63 | }, 64 | { 65 | in: 'None.', 66 | out: '

None.

', 67 | name: 'none' 68 | }, 69 | { 70 | in: [ 71 | 'no space', 72 | 'asd', 73 | '', 74 | 'one space ', 75 | 'asd', 76 | '', 77 | 'one tab ', 78 | 'asd', 79 | '', 80 | 'in an ![image', 81 | 'alt](#)', 82 | '', 83 | 'in a [link', 84 | 'alt](#)', 85 | '', 86 | 'in an *emphasis', 87 | 'emphasis*.', 88 | '', 89 | 'in a **strong', 90 | 'strong**.', 91 | '', 92 | 'setext', 93 | 'heading', 94 | '===', 95 | '', 96 | '> block', 97 | '> quote.', 98 | '', 99 | '* list', 100 | ' item.' 101 | ].join('\n'), 102 | out: [ 103 | '

no space
', 104 | 'asd

', 105 | '

one space
', 106 | 'asd

', 107 | '

one tab
', 108 | 'asd

', 109 | '

in an image',
110 |         'alt

', 111 | '

in a link
', 112 | 'alt

', 113 | '

in an emphasis
', 114 | 'emphasis
.

', 115 | '

in a strong
', 116 | 'strong
.

', 117 | '

setext
', 118 | 'heading

', 119 | '
', 120 | '

block
', 121 | 'quote.

', 122 | '
', 123 | '
    ', 124 | '
  • list
    ', 125 | 'item.
  • ', 126 | '
' 127 | ].join('\n'), 128 | name: 'document' 129 | } 130 | ] 131 | let index = -1 132 | 133 | while (++index < fixtures.length) { 134 | const fixture = fixtures[index] 135 | await t.test(fixture.name, async function () { 136 | assert.equal( 137 | String( 138 | await unified() 139 | .use(remarkParse) 140 | .use(remarkBreaks) 141 | .use(remarkRehype) 142 | .use(rehypeStringify) 143 | .process(fixture.in) 144 | ), 145 | fixture.out 146 | ) 147 | }) 148 | } 149 | }) 150 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "exactOptionalPropertyTypes": true, 8 | "lib": ["es2022"], 9 | "module": "node16", 10 | "strict": true, 11 | "target": "es2022" 12 | }, 13 | "exclude": ["coverage/", "node_modules/"], 14 | "include": ["**/*.js"] 15 | } 16 | --------------------------------------------------------------------------------