mdast-util-heading-range
Advanced tools
Comparing version 3.1.0 to 3.1.1
@@ -0,32 +1,10 @@ | ||
export {headingRange} from './lib/index.js' | ||
export type Handler = import('./lib/index.js').Handler | ||
export type Options = import('./lib/index.js').Options | ||
export type TestFunction = import('./lib/index.js').TestFunction | ||
export type Test = import('./lib/index.js').Test | ||
export type Info = import('./lib/index.js').Info | ||
/** | ||
* Search `node` with `options` and invoke `callback`. | ||
* | ||
* @param {Node} node | ||
* @param {Test|Options} options | ||
* @param {Handler} handler | ||
* Deprecated: use `Info` | ||
*/ | ||
export function headingRange( | ||
node: Node, | ||
options: Test | Options, | ||
handler: Handler | ||
): void | ||
export type Parent = import('unist').Parent | ||
export type Node = import('mdast').Root | import('mdast').Content | ||
export type Heading = import('mdast').Heading | ||
export type TestFunction = (value: string, node: Heading) => boolean | ||
export type Test = string | RegExp | TestFunction | ||
export type Options = { | ||
test: Test | ||
ignoreFinalDefinitions?: boolean | undefined | ||
} | ||
export type ZoneInfo = { | ||
start: number | ||
end: number | ||
parent: Parent | null | ||
} | ||
export type Handler = ( | ||
start: Heading | undefined, | ||
between: Array<Node>, | ||
end: Node | undefined, | ||
info: ZoneInfo | ||
) => any | ||
export type ZoneInfo = Info |
148
index.js
/** | ||
* @typedef {import('unist').Parent} Parent | ||
* @typedef {import('mdast').Root|import('mdast').Content} Node | ||
* @typedef {import('mdast').Heading} Heading | ||
* | ||
* @typedef {(value: string, node: Heading) => boolean} TestFunction | ||
* @typedef {string|RegExp|TestFunction} Test | ||
* | ||
* @typedef Options | ||
* @property {Test} test | ||
* @property {boolean} [ignoreFinalDefinitions=false] | ||
* | ||
* @typedef ZoneInfo | ||
* @property {number} start | ||
* @property {number} end | ||
* @property {Parent|null} parent | ||
* | ||
* @callback Handler | ||
* @param {Heading|undefined} start | ||
* @param {Array.<Node>} between | ||
* @param {Node|undefined} end | ||
* @param {ZoneInfo} info | ||
* @typedef {import('./lib/index.js').Handler} Handler | ||
* @typedef {import('./lib/index.js').Options} Options | ||
* @typedef {import('./lib/index.js').TestFunction} TestFunction | ||
* @typedef {import('./lib/index.js').Test} Test | ||
* @typedef {import('./lib/index.js').Info} Info | ||
*/ | ||
import {toString} from 'mdast-util-to-string' | ||
// To do: next major: remove. | ||
/** | ||
* Search `node` with `options` and invoke `callback`. | ||
* | ||
* @param {Node} node | ||
* @param {Test|Options} options | ||
* @param {Handler} handler | ||
* @typedef {Info} ZoneInfo | ||
* Deprecated: use `Info` | ||
*/ | ||
// eslint-disable-next-line complexity | ||
export function headingRange(node, options, handler) { | ||
let test = options | ||
/** @type {Array.<Node>} */ | ||
const children = 'children' in node ? node.children : [] | ||
/** @type {boolean|undefined} */ | ||
let ignoreFinalDefinitions | ||
// Object, not regex. | ||
if (test && typeof test === 'object' && !('exec' in test)) { | ||
ignoreFinalDefinitions = test.ignoreFinalDefinitions | ||
test = test.test | ||
} | ||
// Transform a string into an applicable expression. | ||
if (typeof test === 'string') { | ||
test = new RegExp('^(' + test + ')$', 'i') | ||
} | ||
// Regex. | ||
if (test && 'exec' in test) { | ||
test = wrapExpression(test) | ||
} | ||
if (typeof test !== 'function') { | ||
throw new TypeError( | ||
'Expected `string`, `regexp`, or `function` for `test`, not `' + | ||
test + | ||
'`' | ||
) | ||
} | ||
let index = -1 | ||
/** @type {number|undefined} */ | ||
let start | ||
/** @type {number|undefined} */ | ||
let end | ||
/** @type {number|undefined} */ | ||
let depth | ||
// Find the range. | ||
while (++index < children.length) { | ||
const child = children[index] | ||
if (child.type === 'heading') { | ||
if (depth && child.depth <= depth) { | ||
end = index | ||
break | ||
} | ||
if (!depth && test(toString(child), child)) { | ||
depth = child.depth | ||
start = index | ||
// Assume no end heading is found. | ||
end = children.length | ||
} | ||
} | ||
} | ||
// When we have a starting heading. | ||
if (depth && end !== undefined && start !== undefined) { | ||
if (ignoreFinalDefinitions) { | ||
while ( | ||
children[end - 1].type === 'definition' || | ||
children[end - 1].type === 'footnoteDefinition' | ||
) { | ||
end-- | ||
} | ||
} | ||
/** @type {Array.<Node>} */ | ||
const nodes = handler( | ||
// @ts-expect-error `start` points to a heading. | ||
children[start], | ||
children.slice(start + 1, end), | ||
children[end], | ||
{parent: node, start, end: children[end] ? end : null} | ||
) | ||
if (nodes) { | ||
// Ensure no empty nodes are inserted. | ||
// This could be the case if `end` is in `nodes` but no `end` node exists. | ||
/** @type {Array.<Node>} */ | ||
const result = [] | ||
let index = -1 | ||
while (++index < nodes.length) { | ||
if (nodes[index]) result.push(nodes[index]) | ||
} | ||
children.splice(start, end - start + 1, ...result) | ||
} | ||
} | ||
} | ||
/** | ||
* Wrap an expression into an assertion function. | ||
* @param {RegExp} expression | ||
* @returns {(value: string) => boolean} | ||
*/ | ||
function wrapExpression(expression) { | ||
return assertion | ||
/** | ||
* Assert `value` matches the bound `expression`. | ||
* @param {string} value | ||
* @returns {boolean} | ||
*/ | ||
function assertion(value) { | ||
return expression.test(value) | ||
} | ||
} | ||
export {headingRange} from './lib/index.js' |
{ | ||
"name": "mdast-util-heading-range", | ||
"version": "3.1.0", | ||
"version": "3.1.1", | ||
"description": "mdast utility to use headings as ranges in mdast", | ||
@@ -32,2 +32,3 @@ "license": "MIT", | ||
"files": [ | ||
"lib/", | ||
"index.d.ts", | ||
@@ -42,20 +43,20 @@ "index.js" | ||
"devDependencies": { | ||
"@types/tape": "^4.0.0", | ||
"@types/node": "^18.0.0", | ||
"c8": "^7.0.0", | ||
"mdast-util-from-markdown": "^1.0.0", | ||
"mdast-util-to-markdown": "^1.0.0", | ||
"prettier": "^2.0.0", | ||
"remark": "^13.0.0", | ||
"remark-cli": "^9.0.0", | ||
"remark-preset-wooorm": "^8.0.0", | ||
"rimraf": "^3.0.0", | ||
"remark-cli": "^11.0.0", | ||
"remark-preset-wooorm": "^9.0.0", | ||
"tape": "^5.0.0", | ||
"type-coverage": "^2.0.0", | ||
"typescript": "^4.0.0", | ||
"xo": "^0.42.0" | ||
"xo": "^0.53.0" | ||
}, | ||
"scripts": { | ||
"prepack": "npm run build && npm run format", | ||
"build": "rimraf \"*.d.ts\" && tsc && type-coverage", | ||
"build": "tsc --build --clean && tsc --build && type-coverage", | ||
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", | ||
"test-api": "node test.js", | ||
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node test.js", | ||
"test-api": "node --conditions development test.js", | ||
"test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", | ||
"test": "npm run build && npm run format && npm run test-coverage" | ||
@@ -76,3 +77,3 @@ }, | ||
"plugins": [ | ||
"preset-wooorm" | ||
"remark-preset-wooorm" | ||
] | ||
@@ -79,0 +80,0 @@ }, |
247
readme.md
@@ -11,11 +11,46 @@ # mdast-util-heading-range | ||
[**mdast**][mdast] utility to use headings as ranges. | ||
[mdast][] utility to find headings and replace the content in their section. | ||
## Contents | ||
* [What is this?](#what-is-this) | ||
* [When should I use this?](#when-should-i-use-this) | ||
* [Install](#install) | ||
* [Use](#use) | ||
* [API](#api) | ||
* [`headingRange(tree, test|options, handler)`](#headingrangetree-testoptions-handler) | ||
* [`Handler`](#handler) | ||
* [`Info`](#info) | ||
* [`Options`](#options) | ||
* [`Test`](#test) | ||
* [`TestFunction`](#testfunction) | ||
* [Types](#types) | ||
* [Compatibility](#compatibility) | ||
* [Security](#security) | ||
* [Related](#related) | ||
* [Contribute](#contribute) | ||
* [License](#license) | ||
## What is this? | ||
This package is a utility that lets you find a certain heading, then takes the | ||
content in their section (from it to the next heading of the same or lower | ||
depth), and calls a given handler with the result, so that you can change or | ||
replace things. | ||
## When should I use this? | ||
This utility is typically useful when you have certain sections that can be | ||
generated. | ||
For example, this utility is used by [`remark-toc`][remark-toc] to update the | ||
above `Contents` heading. | ||
A similar package, [`mdast-zone`][mdast-zone], does the same but uses comments | ||
to mark the start and end of sections. | ||
## 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. | ||
This package is [ESM only][esm]. | ||
In Node.js (version 14.14+ and 16.0+), install with [npm][]: | ||
[npm][]: | ||
```sh | ||
@@ -25,2 +60,16 @@ npm install mdast-util-heading-range | ||
In Deno with [`esm.sh`][esmsh]: | ||
```js | ||
import {headingRange} from 'https://esm.sh/mdast-util-heading-range@3' | ||
``` | ||
In browsers with [`esm.sh`][esmsh]: | ||
```html | ||
<script type="module"> | ||
import {headingRange} from 'https://esm.sh/mdast-util-heading-range@3?bundle' | ||
</script> | ||
``` | ||
## Use | ||
@@ -38,19 +87,17 @@ | ||
And our script, `example.js`, looks as follows: | ||
…and a module `example.js`: | ||
```js | ||
import {readSync} from 'to-vfile' | ||
import {read} from 'to-vfile' | ||
import {remark} from 'remark' | ||
import {headingRange} from 'mdast-util-heading-range' | ||
const file = readSync('example.md') | ||
const file = await remark() | ||
.use(myPluginThatReplacesFoo) | ||
.process(await read('example.md')) | ||
remark() | ||
.use(plugin) | ||
.process(file) | ||
.then((file) => { | ||
console.log(String(file)) | ||
}) | ||
console.log(String(file)) | ||
function plugin() { | ||
/** @type {import('unified').Plugin<[], import('mdast').Root>} */ | ||
function myPluginThatReplacesFoo() { | ||
return (tree) => { | ||
@@ -66,3 +113,3 @@ headingRange(tree, 'foo', (start, nodes, end) => [ | ||
Now, running `node example` yields: | ||
…now running `node example.js` yields: | ||
@@ -79,3 +126,3 @@ ```markdown | ||
This package exports the following identifiers: `headingRange`. | ||
This package exports the identifier [`headingRange`][api-headingrange]. | ||
There is no default export. | ||
@@ -85,56 +132,115 @@ | ||
Search `tree` ([`Node`][node]) and transform a section without affecting other | ||
parts with `handler` ([`Function`][handler]). | ||
A “section” is a heading that passes `test`, until the next heading of the same | ||
or lower depth, or the end of the document. | ||
Search `tree` for a heading matching `test` and change its “section” with | ||
`handler`. | ||
A “section” ranges from the matched heading until the next heading of the | ||
same or lower depth, or the end of the document. | ||
If `ignoreFinalDefinitions: true`, final definitions “in” the section are | ||
excluded. | ||
##### `options` | ||
###### Parameters | ||
###### `options.test` | ||
* `tree` ([`Node`][node]) | ||
— tree to change | ||
* `test` ([`Test`][api-test]) | ||
— same as passing `{test: Test}` | ||
* `options` ([`Options`][api-options]) | ||
— configuration | ||
* `handler` ([`Handler`][api-handler]) | ||
— handle a section | ||
Heading to look for (`string`, `RegExp`, [`Function`][test]). | ||
When `string`, wrapped in `new RegExp('^(' + value + ')$', 'i')`; | ||
when `RegExp`, wrapped in `function (value) {expression.test(value)}` | ||
###### Returns | ||
###### `options.ignoreFinalDefinitions` | ||
Nothing (`void`). | ||
Ignore final definitions otherwise in the section (`boolean`, default: `false`). | ||
### `Handler` | ||
#### `function test(value, node)` | ||
Callback called when a section is found (TypeScript type). | ||
Function invoked for each heading with its content (`string`) and `node` | ||
itself ([`Heading`][heading]) to check if it’s the one to look for. | ||
###### Parameters | ||
* `start` ([`Heading`][heading]) | ||
— start of section (a heading node matching `test`) | ||
* `nodes` ([`Array<Node>`][node]) | ||
— nodes between `start` and `end` | ||
* `end` ([`Node`][node] or `undefined`) | ||
— end of section, if any | ||
* `info` ([`Info`][api-info]) | ||
— extra info | ||
### `Info` | ||
Extra info (TypeScript type). | ||
###### Fields | ||
* `parent` ([`Node`][node]) | ||
— parent of the section | ||
* `start` (`number`) | ||
— index of `start` in `parent` | ||
* `end` (`number` or `null`) | ||
— index of `end` in `parent` | ||
###### Returns | ||
`Boolean?`, `true` if this is the heading to use. | ||
Results (`Array<Node | null | undefined>`, optional). | ||
#### `function handler(start, nodes, end?, scope)` | ||
If nothing is returned, nothing will be changed. | ||
If an array of nodes (can include `null` and `undefined`) is returned, the | ||
original section will be replaced by those nodes. | ||
Callback invoked when a range is found. | ||
### `Options` | ||
##### Parameters | ||
Configuration (TypeScript type). | ||
###### `start` | ||
###### Fields | ||
Start of range ([`Heading`][heading]). | ||
* `test` ([`Test`][api-test]) | ||
— test for a heading | ||
* `ignoreFinalDefinitions` (`boolean`, default: `false`) | ||
— ignore final definitions otherwise in the section | ||
###### `nodes` | ||
### `Test` | ||
Nodes between `start` and `end` ([`Array.<Node>`][node]). | ||
Test for a heading (TypeScript type). | ||
###### `end` | ||
When `string`, wrapped in `new RegExp('^(' + value + ')$', 'i')`; | ||
when `RegExp`, wrapped in `(value) => expression.test(value)` | ||
End of range, if any ([`Node?`][node]). | ||
###### Type | ||
###### `scope` | ||
```ts | ||
export type Test = string | RegExp | TestFunction | ||
``` | ||
Extra info (`Object`): | ||
### `TestFunction` | ||
* `parent` ([`Node`][node]) — Parent of the range | ||
* `start` (`number`) — Index of `start` in `parent` | ||
* `end` (`number?`) — Index of `end` in `parent` | ||
Check if a node matches (TypeScript type). | ||
###### Parameters | ||
* `value` (`string`) | ||
— plain-text heading | ||
* `node` ([`Heading`][heading]) | ||
— heading node | ||
###### Returns | ||
Whether this is the heading that is searched for (`boolean`, optional). | ||
## Types | ||
This package is fully typed with [TypeScript][]. | ||
This package exports the types [`Handler`][api-handler], | ||
[`Info`][api-info], [`Options`][api-options], [`Test`][api-test], | ||
and [`TestFunction`][api-testfunction]. | ||
## Compatibility | ||
Projects maintained by the unified collective are compatible with all maintained | ||
versions of Node.js. | ||
As of now, that is Node.js 14.14+ and 16.0+. | ||
Our projects sometimes work with older versions, but this is not guaranteed. | ||
## Security | ||
@@ -149,4 +255,5 @@ | ||
```js | ||
/** @type {import('mdast-util-heading-range').Handler} */ | ||
function handler(start, nodes, end) { | ||
return [start, {type: 'html', value: 'alert(1)'}, end] | ||
return [start, {type: 'html', value: '<script>alert(1)</script>'}, end] | ||
} | ||
@@ -166,3 +273,3 @@ ``` | ||
Either do not use user input in `handler` or use | ||
[`hast-util-santize`][sanitize]. | ||
[`hast-util-santize`][hast-util-sanitize]. | ||
@@ -172,12 +279,12 @@ ## Related | ||
* [`mdast-zone`](https://github.com/syntax-tree/mdast-zone) | ||
— comments as ranges or markers instead of headings | ||
— similar but uses comments to mark the start and end of sections | ||
## Contribute | ||
See [`contributing.md` in `syntax-tree/.github`][contributing] for ways to get | ||
started. | ||
See [`contributing.md`][contributing] in [`syntax-tree/.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 | ||
By interacting with this repository, organisation, or community you agree to | ||
abide by its terms. | ||
@@ -219,2 +326,8 @@ | ||
[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c | ||
[esmsh]: https://esm.sh | ||
[typescript]: https://www.typescriptlang.org | ||
[license]: license | ||
@@ -224,8 +337,10 @@ | ||
[contributing]: https://github.com/syntax-tree/.github/blob/HEAD/contributing.md | ||
[health]: https://github.com/syntax-tree/.github | ||
[support]: https://github.com/syntax-tree/.github/blob/HEAD/support.md | ||
[contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md | ||
[coc]: https://github.com/syntax-tree/.github/blob/HEAD/code-of-conduct.md | ||
[support]: https://github.com/syntax-tree/.github/blob/main/support.md | ||
[coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md | ||
[mdast]: https://github.com/syntax-tree/mdast | ||
@@ -235,8 +350,4 @@ | ||
[handler]: #function-handlerstart-nodes-end-scope | ||
[heading]: https://github.com/syntax-tree/mdast#heading | ||
[test]: #function-testvalue-node | ||
[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting | ||
@@ -246,2 +357,18 @@ | ||
[sanitize]: https://github.com/syntax-tree/hast-util-sanitize | ||
[mdast-zone]: https://github.com/syntax-tree/mdast-zone | ||
[hast-util-sanitize]: https://github.com/syntax-tree/hast-util-sanitize | ||
[remark-toc]: https://github.com/remarkjs/remark-toc | ||
[api-headingrange]: #headingrangetree-testoptions-handler | ||
[api-handler]: #handler | ||
[api-options]: #options | ||
[api-test]: #test | ||
[api-testfunction]: #testfunction | ||
[api-info]: #info |
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
20171
7
287
362
1