(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 ',
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 
',
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 |
--------------------------------------------------------------------------------