rehype-format
Advanced tools
Comparing version 4.0.1 to 5.0.0
@@ -1,38 +0,2 @@ | ||
/** | ||
* Format whitespace in HTML. | ||
* | ||
* @type {import('unified').Plugin<[Options?] | Array<void>, Root>} | ||
*/ | ||
export default function rehypeFormat( | ||
options?: void | Options | undefined | ||
): | ||
| void | ||
| import('unified').Transformer<import('hast').Root, import('hast').Root> | ||
export type Root = import('hast').Root | ||
export type Child = Root['children'][number] | ||
export type Element = import('hast').Element | ||
export type Node = Root | Child | ||
/** | ||
* Configuration. | ||
*/ | ||
export type Options = { | ||
/** | ||
* Indentation per level (`number`, `string`, default: `2`). | ||
* When number, uses that amount of spaces. | ||
* When `string`, uses that per indentation level. | ||
*/ | ||
indent?: string | number | undefined | ||
/** | ||
* Whether to indent the first level (`boolean`, default: `true`). | ||
* This is usually the `<html>`, thus not indenting `head` and `body`. | ||
*/ | ||
indentInitial?: boolean | undefined | ||
/** | ||
* List of tag names to join with a blank line (`Array<string>`, default: | ||
* `[]`). | ||
* These tags, when next to each other, are joined by a blank line (`\n\n`). | ||
* For example, when `['head', 'body']` is given, a blank line is added | ||
* between these two. | ||
*/ | ||
blanks?: string[] | undefined | ||
} | ||
export { default } from "./lib/index.js"; | ||
export type Options = import('./lib/index.js').Options; |
190
index.js
/** | ||
* @typedef {import('hast').Root} Root | ||
* @typedef {Root['children'][number]} Child | ||
* @typedef {import('hast').Element} Element | ||
* @typedef {Root|Child} Node | ||
* | ||
* @typedef Options | ||
* Configuration. | ||
* @property {number|string} [indent=2] | ||
* Indentation per level (`number`, `string`, default: `2`). | ||
* When number, uses that amount of spaces. | ||
* When `string`, uses that per indentation level. | ||
* @property {boolean} [indentInitial=true] | ||
* Whether to indent the first level (`boolean`, default: `true`). | ||
* This is usually the `<html>`, thus not indenting `head` and `body`. | ||
* @property {Array<string>} [blanks=[]] | ||
* List of tag names to join with a blank line (`Array<string>`, default: | ||
* `[]`). | ||
* These tags, when next to each other, are joined by a blank line (`\n\n`). | ||
* For example, when `['head', 'body']` is given, a blank line is added | ||
* between these two. | ||
* @typedef {import('./lib/index.js').Options} Options | ||
*/ | ||
import rehypeMinifyWhitespace from 'rehype-minify-whitespace' | ||
import {visitParents, SKIP} from 'unist-util-visit-parents' | ||
import {embedded} from 'hast-util-embedded' | ||
import {phrasing} from 'hast-util-phrasing' | ||
import {whitespace} from 'hast-util-whitespace' | ||
import {isElement} from 'hast-util-is-element' | ||
import {whitespaceSensitiveTagNames} from 'html-whitespace-sensitive-tag-names' | ||
const minify = rehypeMinifyWhitespace({newlines: true}) | ||
/** | ||
* Format whitespace in HTML. | ||
* | ||
* @type {import('unified').Plugin<[Options?] | Array<void>, Root>} | ||
*/ | ||
export default function rehypeFormat(options = {}) { | ||
let indent = options.indent || 2 | ||
let indentInitial = options.indentInitial | ||
if (typeof indent === 'number') { | ||
indent = ' '.repeat(indent) | ||
} | ||
// Default to indenting the initial level. | ||
if (indentInitial === null || indentInitial === undefined) { | ||
indentInitial = true | ||
} | ||
return (tree) => { | ||
/** @type {boolean|undefined} */ | ||
let head | ||
// @ts-expect-error: fine, it’s a sync transformer. | ||
minify(tree) | ||
// eslint-disable-next-line complexity | ||
visitParents(tree, (node, parents) => { | ||
let index = -1 | ||
if (!('children' in node)) { | ||
return | ||
} | ||
if (isElement(node, 'head')) { | ||
head = true | ||
} | ||
if (head && isElement(node, 'body')) { | ||
head = undefined | ||
} | ||
if (isElement(node, whitespaceSensitiveTagNames)) { | ||
return SKIP | ||
} | ||
const children = node.children | ||
let level = parents.length | ||
// Don’t indent content of whitespace-sensitive nodes / inlines. | ||
if (children.length === 0 || !padding(node, head)) { | ||
return | ||
} | ||
if (!indentInitial) { | ||
level-- | ||
} | ||
/** @type {boolean|undefined} */ | ||
let eol | ||
// Indent newlines in `text`. | ||
while (++index < children.length) { | ||
const child = children[index] | ||
if (child.type === 'text' || child.type === 'comment') { | ||
if (child.value.includes('\n')) { | ||
eol = true | ||
} | ||
child.value = child.value.replace( | ||
/ *\n/g, | ||
'$&' + String(indent).repeat(level) | ||
) | ||
} | ||
} | ||
/** @type {Array<Child>} */ | ||
const result = [] | ||
/** @type {Child|undefined} */ | ||
let previous | ||
index = -1 | ||
while (++index < children.length) { | ||
const child = children[index] | ||
if (padding(child, head) || (eol && !index)) { | ||
addBreak(result, level, child) | ||
eol = true | ||
} | ||
previous = child | ||
result.push(child) | ||
} | ||
if (previous && (eol || padding(previous, head))) { | ||
// Ignore trailing whitespace (if that already existed), as we’ll add | ||
// properly indented whitespace. | ||
if (whitespace(previous)) { | ||
result.pop() | ||
previous = result[result.length - 1] | ||
} | ||
addBreak(result, level - 1) | ||
} | ||
node.children = result | ||
}) | ||
} | ||
/** | ||
* @param {Array<Child>} list | ||
* @param {number} level | ||
* @param {Child} [next] | ||
* @returns {void} | ||
*/ | ||
function addBreak(list, level, next) { | ||
const tail = list[list.length - 1] | ||
const previous = whitespace(tail) ? list[list.length - 2] : tail | ||
const replace = | ||
(blank(previous) && blank(next) ? '\n\n' : '\n') + | ||
String(indent).repeat(Math.max(level, 0)) | ||
if (tail && tail.type === 'text') { | ||
tail.value = whitespace(tail) ? replace : tail.value + replace | ||
} else { | ||
list.push({type: 'text', value: replace}) | ||
} | ||
} | ||
/** | ||
* @param {Node|undefined} node | ||
* @returns {boolean} | ||
*/ | ||
function blank(node) { | ||
return Boolean( | ||
node && | ||
node.type === 'element' && | ||
options.blanks && | ||
options.blanks.length > 0 && | ||
options.blanks.includes(node.tagName) | ||
) | ||
} | ||
} | ||
/** | ||
* @param {Node} node | ||
* @param {boolean|undefined} head | ||
* @returns {boolean} | ||
*/ | ||
function padding(node, head) { | ||
return ( | ||
node.type === 'root' || | ||
(node.type === 'element' | ||
? head || isElement(node, 'script') || embedded(node) || !phrasing(node) | ||
: false) | ||
) | ||
} | ||
export {default} from './lib/index.js' |
{ | ||
"name": "rehype-format", | ||
"version": "4.0.1", | ||
"version": "5.0.0", | ||
"description": "rehype plugin to format HTML", | ||
@@ -29,5 +29,5 @@ "license": "MIT", | ||
"type": "module", | ||
"main": "index.js", | ||
"types": "index.d.ts", | ||
"exports": "./index.js", | ||
"files": [ | ||
"lib/", | ||
"index.d.ts", | ||
@@ -37,48 +37,43 @@ "index.js" | ||
"dependencies": { | ||
"@types/hast": "^2.0.0", | ||
"hast-util-embedded": "^2.0.0", | ||
"hast-util-is-element": "^2.0.0", | ||
"hast-util-phrasing": "^2.0.0", | ||
"hast-util-whitespace": "^2.0.0", | ||
"html-whitespace-sensitive-tag-names": "^2.0.0", | ||
"rehype-minify-whitespace": "^5.0.0", | ||
"unified": "^10.0.0", | ||
"unist-util-visit-parents": "^5.0.0" | ||
"@types/hast": "^3.0.0", | ||
"hast-util-embedded": "^3.0.0", | ||
"hast-util-is-element": "^3.0.0", | ||
"hast-util-phrasing": "^3.0.0", | ||
"hast-util-whitespace": "^3.0.0", | ||
"html-whitespace-sensitive-tag-names": "^3.0.0", | ||
"rehype-minify-whitespace": "^6.0.0", | ||
"unist-util-visit-parents": "^6.0.0" | ||
}, | ||
"devDependencies": { | ||
"@types/tape": "^4.0.0", | ||
"c8": "^7.0.0", | ||
"@types/node": "^20.0.0", | ||
"c8": "^8.0.0", | ||
"is-hidden": "^2.0.0", | ||
"prettier": "^2.0.0", | ||
"rehype": "^12.0.0", | ||
"remark-cli": "^10.0.0", | ||
"prettier": "^3.0.0", | ||
"rehype": "^13.0.0", | ||
"remark-cli": "^11.0.0", | ||
"remark-preset-wooorm": "^9.0.0", | ||
"rimraf": "^3.0.0", | ||
"tape": "^5.0.0", | ||
"to-vfile": "^7.0.0", | ||
"to-vfile": "^8.0.0", | ||
"type-coverage": "^2.0.0", | ||
"typescript": "^4.0.0", | ||
"xo": "^0.47.0" | ||
"typescript": "^5.0.0", | ||
"xo": "^0.56.0" | ||
}, | ||
"scripts": { | ||
"build": "rimraf \"*.d.ts\" \"test/**/*.d.ts\" && tsc && type-coverage", | ||
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", | ||
"build": "tsc --build --clean && tsc --build && type-coverage", | ||
"format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", | ||
"prepack": "npm run build && npm run format", | ||
"test": "npm run build && npm run format && npm run test-coverage", | ||
"test-api": "node --conditions development test/index.js", | ||
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node --conditions development test/index.js", | ||
"test": "npm run build && npm run format && npm run test-coverage" | ||
"test-coverage": "c8 --100 --check-coverage --reporter lcov npm run test-api" | ||
}, | ||
"prettier": { | ||
"tabWidth": 2, | ||
"useTabs": false, | ||
"bracketSpacing": false, | ||
"singleQuote": true, | ||
"bracketSpacing": false, | ||
"semi": false, | ||
"trailingComma": "none" | ||
"tabWidth": 2, | ||
"trailingComma": "none", | ||
"useTabs": false | ||
}, | ||
"xo": { | ||
"prettier": true | ||
}, | ||
"remarkConfig": { | ||
"plugins": [ | ||
"preset-wooorm" | ||
"remark-preset-wooorm" | ||
] | ||
@@ -89,5 +84,12 @@ }, | ||
"detail": true, | ||
"strict": true, | ||
"ignoreCatch": true | ||
"ignoreCatch": true, | ||
"strict": true | ||
}, | ||
"xo": { | ||
"prettier": true, | ||
"rules": { | ||
"unicorn/prefer-at": "off", | ||
"unicorn/prefer-string-replace-all": "off" | ||
} | ||
} | ||
} |
152
readme.md
@@ -21,2 +21,3 @@ # rehype-format | ||
* [`unified().use(rehypeFormat[, options])`](#unifieduserehypeformat-options) | ||
* [`Options`](#options) | ||
* [Examples](#examples) | ||
@@ -61,4 +62,4 @@ * [Example: markdown input (remark)](#example-markdown-input-remark) | ||
This package is [ESM only](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c). | ||
In Node.js (version 12.20+, 14.14+, or 16.0+), install with [npm][]: | ||
This package is [ESM only][esm]. | ||
In Node.js (version 16+), install with [npm][]: | ||
@@ -69,13 +70,13 @@ ```sh | ||
In Deno with [Skypack][]: | ||
In Deno with [`esm.sh`][esmsh]: | ||
```js | ||
import rehypeFormat from 'https://cdn.skypack.dev/rehype-format@4?dts' | ||
import rehypeFormat from 'https://esm.sh/rehype-format@5' | ||
``` | ||
In browsers with [Skypack][]: | ||
In browsers with [`esm.sh`][esmsh]: | ||
```html | ||
<script type="module"> | ||
import rehypeFormat from 'https://cdn.skypack.dev/rehype-format@4?min' | ||
import rehypeFormat from 'https://esm.sh/rehype-format@5?bundle' | ||
</script> | ||
@@ -100,25 +101,21 @@ ``` | ||
And our module `example.js` looks as follows: | ||
…and our module `example.js` looks as follows: | ||
```js | ||
import rehypeFormat from 'rehype-format' | ||
import rehypeParse from 'rehype-parse' | ||
import rehypeStringify from 'rehype-stringify' | ||
import {read} from 'to-vfile' | ||
import {unified} from 'unified' | ||
import rehypeParse from 'rehype-parse' | ||
import rehypeFormat from 'rehype-format' | ||
import rehypeStringify from 'rehype-stringify' | ||
main() | ||
const file = await unified() | ||
.use(rehypeParse) | ||
.use(rehypeFormat) | ||
.use(rehypeStringify) | ||
.process(await read('index.html')) | ||
async function main() { | ||
const file = await unified() | ||
.use(rehypeParse) | ||
.use(rehypeFormat) | ||
.use(rehypeStringify) | ||
.process(await read('index.html')) | ||
console.log(String(file)) | ||
} | ||
console.log(String(file)) | ||
``` | ||
Now running `node example.js` yields: | ||
…then running `node example.js` yields: | ||
@@ -143,3 +140,3 @@ ```html | ||
This package exports no identifiers. | ||
The default export is `rehypeFormat`. | ||
The default export is [`rehypeFormat`][api-rehype-format]. | ||
@@ -150,24 +147,27 @@ ### `unified().use(rehypeFormat[, options])` | ||
##### `options` | ||
###### Parameters | ||
Configuration (optional). | ||
* `options` ([`Options`][api-options], optional) | ||
— configuration | ||
###### `options.indent` | ||
###### Returns | ||
Indentation per level (`number`, `string`, default: `2`). | ||
When `number`, uses that amount of spaces. | ||
When `string`, uses that per indentation level. | ||
Transform ([`Transformer`][transformer]). | ||
###### `options.indentInitial` | ||
### `Options` | ||
Whether to indent the first level (`boolean`, default: `true`). | ||
The initial element is usually the `<html>` element, so when this is set to | ||
`false`, its children `<head>` and `<body>` would not be indented. | ||
Configuration (TypeScript type). | ||
###### `options.blanks` | ||
###### Fields | ||
List of tag names to join with a blank line (`Array<string>`, default: `[]`). | ||
These tags, when next to each other, are joined by a blank line (`\n\n`). | ||
For example, when `['head', 'body']` is given, a blank line is added between | ||
these two. | ||
* `blanks` (`Array<string>`, default: `[]`) | ||
— list of tag names to join with a blank line (default: `[]`); these tags, | ||
when next to each other, are joined by a blank line (`\n\n`); for example, | ||
when `['head', 'body']` is given, a blank line is added between these two | ||
* `indent` (`number`, `string`, default: `2`) | ||
— indentation per level (default: `2`); when number, uses that amount of | ||
spaces; when `string`, uses that per indentation level | ||
* `indentInitial` (`boolean`, default: `true`) | ||
— whether to indent the first level (default: `true`); this is usually the | ||
`<html>`, thus not indenting `head` and `body` | ||
@@ -182,22 +182,18 @@ ## Examples | ||
```js | ||
import {unified} from 'unified' | ||
import remarkParse from 'remark-parse' | ||
import remarkRehype from 'remark-rehype' | ||
import rehypeDocument from 'rehype-document' | ||
import rehypeFormat from 'rehype-format' | ||
import rehypeStringify from 'rehype-stringify' | ||
import remarkParse from 'remark-parse' | ||
import remarkRehype from 'remark-rehype' | ||
import {unified} from 'unified' | ||
main() | ||
const file = await unified() | ||
.use(remarkParse) | ||
.use(remarkRehype) | ||
.use(rehypeDocument, {title: 'Neptune'}) | ||
.use(rehypeFormat) | ||
.use(rehypeStringify) | ||
.process('# Hello, Neptune!') | ||
async function main() { | ||
const file = await unified() | ||
.use(remarkParse) | ||
.use(remarkRehype) | ||
.use(rehypeDocument, {title: 'Neptune'}) | ||
.use(rehypeFormat) | ||
.use(rehypeStringify) | ||
.process('# Hello, Neptune!') | ||
console.log(String(file)) | ||
} | ||
console.log(String(file)) | ||
``` | ||
@@ -228,18 +224,14 @@ | ||
```js | ||
import {unified} from 'unified' | ||
import rehypeFormat from 'rehype-format' | ||
import rehypeParse from 'rehype-parse' | ||
import rehypeFormat from 'rehype-format' | ||
import rehypeStringify from 'rehype-stringify' | ||
import {unified} from 'unified' | ||
main() | ||
const file = await unified() | ||
.use(rehypeParse) | ||
.use(rehypeFormat, {blanks: ['head', 'body'], indent: '\t'}) | ||
.use(rehypeStringify) | ||
.process('<h1>Hi!</h1><p>Hello, Venus!</p>') | ||
async function main() { | ||
const file = await unified() | ||
.use(rehypeParse) | ||
.use(rehypeFormat, {indent: '\t', blanks: ['head', 'body']}) | ||
.use(rehypeStringify) | ||
.process('<h1>Hi!</h1><p>Hello, Venus!</p>') | ||
console.log(String(file)) | ||
} | ||
console.log(String(file)) | ||
``` | ||
@@ -270,12 +262,14 @@ | ||
This package is fully typed with [TypeScript][]. | ||
It exports an `Options` type, which specifies the interface of the accepted | ||
options. | ||
It exports the additional type [`Options`][api-options]. | ||
## Compatibility | ||
Projects maintained by the unified collective are compatible with all maintained | ||
Projects maintained by the unified collective are compatible with maintained | ||
versions of Node.js. | ||
As of now, that is Node.js 12.20+, 14.14+, and 16.0+. | ||
Our projects sometimes work with older versions, but this is not guaranteed. | ||
When we cut a new major release, we drop support for unmaintained versions of | ||
Node. | ||
This means we try to keep the current release line, `rehype-format@^5`, | ||
compatible with Node.js 16. | ||
This plugin works with `rehype-parse` version 3+, `rehype-stringify` version 3+, | ||
@@ -333,5 +327,5 @@ `rehype` version 5+, and `unified` version 6+. | ||
[size-badge]: https://img.shields.io/bundlephobia/minzip/rehype-format.svg | ||
[size-badge]: https://img.shields.io/bundlejs/size/rehype-format | ||
[size]: https://bundlephobia.com/result?p=rehype-format | ||
[size]: https://bundlejs.com/?q=rehype-format | ||
@@ -348,4 +342,6 @@ [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg | ||
[skypack]: https://www.skypack.dev | ||
[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c | ||
[esmsh]: https://esm.sh | ||
[npm]: https://docs.npmjs.com/cli/install | ||
@@ -355,7 +351,7 @@ | ||
[contributing]: https://github.com/rehypejs/.github/blob/HEAD/contributing.md | ||
[contributing]: https://github.com/rehypejs/.github/blob/main/contributing.md | ||
[support]: https://github.com/rehypejs/.github/blob/HEAD/support.md | ||
[support]: https://github.com/rehypejs/.github/blob/main/support.md | ||
[coc]: https://github.com/rehypejs/.github/blob/HEAD/code-of-conduct.md | ||
[coc]: https://github.com/rehypejs/.github/blob/main/code-of-conduct.md | ||
@@ -372,2 +368,4 @@ [license]: license | ||
[transformer]: https://github.com/unifiedjs/unified#transformer | ||
[rehype]: https://github.com/rehypejs/rehype | ||
@@ -380,1 +378,5 @@ | ||
[rehype-minify]: https://github.com/rehypejs/rehype-minify | ||
[api-options]: #options | ||
[api-rehype-format]: #unifieduserehypeformat-options |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
20184
8
11
7
218
369
1
+ Added@types/hast@3.0.4(transitive)
+ Added@types/unist@3.0.3(transitive)
+ Addedhast-util-embedded@3.0.0(transitive)
+ Addedhast-util-has-property@3.0.0(transitive)
+ Addedhast-util-is-body-ok-link@3.0.1(transitive)
+ Addedhast-util-is-element@3.0.0(transitive)
+ Addedhast-util-minify-whitespace@1.0.1(transitive)
+ Addedhast-util-phrasing@3.0.1(transitive)
+ Addedhast-util-whitespace@3.0.0(transitive)
+ Addedhtml-whitespace-sensitive-tag-names@3.0.1(transitive)
+ Addedrehype-minify-whitespace@6.0.2(transitive)
+ Addedunist-util-is@6.0.0(transitive)
+ Addedunist-util-visit-parents@6.0.1(transitive)
- Removedunified@^10.0.0
- Removed@types/hast@2.3.10(transitive)
- Removed@types/unist@2.0.11(transitive)
- Removedbail@2.0.2(transitive)
- Removedextend@3.0.2(transitive)
- Removedhast-util-embedded@2.0.1(transitive)
- Removedhast-util-has-property@2.0.1(transitive)
- Removedhast-util-is-body-ok-link@2.0.0(transitive)
- Removedhast-util-is-element@2.1.3(transitive)
- Removedhast-util-phrasing@2.0.2(transitive)
- Removedhast-util-whitespace@2.0.1(transitive)
- Removedhtml-whitespace-sensitive-tag-names@2.0.0(transitive)
- Removedis-buffer@2.0.5(transitive)
- Removedis-plain-obj@4.1.0(transitive)
- Removedrehype-minify-whitespace@5.0.1(transitive)
- Removedtrough@2.2.0(transitive)
- Removedunified@10.1.2(transitive)
- Removedunist-util-is@5.2.1(transitive)
- Removedunist-util-stringify-position@3.0.3(transitive)
- Removedunist-util-visit-parents@5.1.3(transitive)
- Removedvfile@5.3.7(transitive)
- Removedvfile-message@3.1.4(transitive)
Updated@types/hast@^3.0.0
Updatedhast-util-embedded@^3.0.0
Updatedhast-util-is-element@^3.0.0
Updatedhast-util-phrasing@^3.0.0
Updatedhast-util-whitespace@^3.0.0