Socket
Socket
Sign inDemoInstall

remark-usage

Package Overview
Dependencies
Maintainers
2
Versions
24
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

remark-usage - npm Package Compare versions

Comparing version 10.0.1 to 11.0.0

5

index.d.ts

@@ -1,3 +0,2 @@

export default remarkUsage
export type Options = import('./lib/index.js').Options
import remarkUsage from './lib/index.js'
export { default } from "./lib/index.js";
export type Options = import('./lib/index.js').Options;

4

index.js

@@ -5,4 +5,2 @@ /**

import remarkUsage from './lib/index.js'
export default remarkUsage
export {default} from './lib/index.js'
/**
* Plugin to add a usage example to a readme.
* Add a usage example to a readme.
*
* @type {import('unified').Plugin<[Options?]|void[], Root>}
* Looks for the first heading matching `options.heading` (case insensitive),
* removes everything between it and an equal or higher next heading, and replaces
* that with a example.
*
* The example runs in Node.js (so no side effects!).
* Line comments (`//`) are turned into markdown.
* Calls to `console.log()` are exposed as code blocks, containing the logged
* values, so `console.log(1 + 1)` becomes `2`.
* Use a string as the first argument to `log` to use as the language for the code.
*
* You can ignore lines with `remark-usage-ignore-next`:
*
* ```js
* // remark-usage-ignore-next
* const two = sum(1, 1)
*
* // remark-usage-ignore-next 3
* function sum(a, b) {
* return a + b
* }
* ```
*
* …if no `skip` is given, 1 line is skipped.
*
* @param {Readonly<Options> | null | undefined} [options]
* Configuration (optional).
* @returns
* Transform.
*/
export default function remarkUsage(
options?: void | Options | undefined
):
| void
| import('unified').Transformer<import('mdast').Root, import('mdast').Root>
export type Root = import('mdast').Root
export type BlockContent = import('mdast').BlockContent
export type VFile = import('vfile').VFile
export type Callback = import('trough').Callback
export default function remarkUsage(options?: Readonly<Options> | null | undefined): (tree: Root, file: VFile) => Promise<undefined>;
export type FileResult = import('@babel/core').BabelFileResult;
export type NodePathCallExpression = import('@babel/core').NodePath<CallExpression>;
export type NodePathImportDeclaration = import('@babel/core').NodePath<ImportDeclaration>;
export type PluginPass = import('@babel/core').PluginPass;
export type CallExpression = import('@babel/types').CallExpression;
export type ImportDeclaration = import('@babel/types').ImportDeclaration;
export type StringLiteral = import('@babel/types').StringLiteral;
export type Root = import('mdast').Root;
export type RootContent = import('mdast').RootContent;
export type PackageJson = import('type-fest').PackageJson;
/**
* Code.
*/
export type Code = {
/**
* End.
*/
end: number;
/**
* Start.
*/
start: number;
/**
* Type.
*/
type: 'code';
};
/**
* Code.
*/
export type CodeWithValues = {
/**
* Kind.
*/
type: 'code';
/**
* Results.
*/
values: Array<string>;
};
/**
* Comment.
*/
export type Comment = {
/**
* Type.
*/
type: 'comment';
/**
* Values.
*/
values: Array<string>;
};
export type GenerateOptions = {
example: string;
name: string | null | undefined;
instrumented: InstrumentResult;
};
/**
* Configuration.
*/
export type InstrumentOptions = {
/**
* File.
*/
example: VFile;
/**
* ID.
*/
id: string;
/**
* Main.
*/
main?: string | null | undefined;
};
export type InstrumentResult = {
result: string;
logs: Array<Log>;
references: Array<Reference>;
};
/**
* Log.
*/
export type Log = {
/**
* End.
*/
end: number | null | undefined;
/**
* Code language.
*/
language: string | undefined;
/**
* Start.
*/
start: number | null | undefined;
/**
* Values.
*/
values: Array<string>;
};
/**
* Info on the package.
*/
export type PackageInfo = {
/**
* Data.
*/
value: PackageJson;
/**
* File.
*/
file: VFile;
};
/**
* Reference.
*/
export type Reference = {
/**
* End.
*/
end: number | null | undefined;
/**
* Quote.
*/
quote: string;
/**
* Start.
*/
start: number | null | undefined;
};
/**
* Skip.
*/
export type Skip = {
/**
* Skip.
*/
skip: number;
/**
* Type.
*/
type: 'skip';
};
/**
* Token.
*/
export type Token = Code | Comment | Skip;
/**
* Configuration.
*/
export type Options = {
/**
* Heading to look for (default: `'usage'`).
* Wrapped in `new RegExp('^(' + value + ')$', 'i');`.
*/
heading?: string | undefined
/**
* Path to the example script.
* If given, resolved from `file.cwd`.
* If not given, the following values are attempted and resolved from
* `file.cwd`:
*
* * `'./example.js'`
* * `'./example/index.js'`
* * `'./examples.js'`
* * `'./examples/index.js'`
* * `'./doc/example.js'`
* * `'./doc/example/index.js'`
* * `'./docs/example.js'`
* * `'./docs/example/index.js'`
*
* The first that exists, is used.
*/
example?: string | undefined
/**
* Name of the module (default: `pkg.name`, optional).
* Used to rewrite `import x from './main.js'` to `import x from 'name'`.
*/
name?: string | undefined
/**
* Path to the main file (default: `pkg.main` or `'./index.js'`, optional).
* If given, resolved from `file.cwd`.
* If inferred from `package.json`, resolved relating to that package root.
* Used to rewrite `import x from './main.js'` to `import x from 'name'`.
*/
main?: string | undefined
}
/**
* Path to example file (optional);
* resolved from `file.cwd`;
* defaults to the first example that exists:
*
* * `'example.js'`
* * `'example/index.js'`
* * `'examples.js'`
* * `'examples/index.js'`
* * `'doc/example.js'`
* * `'doc/example/index.js'`
* * `'docs/example.js'`
* * `'docs/example/index.js'`
*/
example?: string | null | undefined;
/**
* Heading to look for (default: `'usage'`);
* wrapped in `new RegExp('^(' + value + ')$', 'i');`.
*/
heading?: string | null | undefined;
/**
* Path to the file (default: `pkg.exports`, `pkg.main`, `'index.js'`);
* resolved from `file.cwd`;
* used to rewrite `import x from './main.js'` to `import x from 'name'`.
*/
main?: string | null | undefined;
/**
* Name of the module (default: `pkg.name`);
* used to rewrite `import x from './main.js'` to `import x from 'name'`.
*/
name?: string | null | undefined;
};
import { VFile } from 'vfile';
/**
* @typedef {import('@babel/core').BabelFileResult} FileResult
* @typedef {import('@babel/core').NodePath<CallExpression>} NodePathCallExpression
* @typedef {import('@babel/core').NodePath<ImportDeclaration>} NodePathImportDeclaration
* @typedef {import('@babel/core').PluginPass} PluginPass
* @typedef {import('@babel/types').CallExpression} CallExpression
* @typedef {import('@babel/types').ImportDeclaration} ImportDeclaration
* @typedef {import('@babel/types').StringLiteral} StringLiteral
* @typedef {import('mdast').Root} Root
* @typedef {import('mdast').BlockContent} BlockContent
* @typedef {import('vfile').VFile} VFile
* @typedef {import('trough').Callback} Callback
* @typedef {import('mdast').RootContent} RootContent
* @typedef {import('type-fest').PackageJson} PackageJson
*/
/**
* @typedef Code
* Code.
* @property {number} end
* End.
* @property {number} start
* Start.
* @property {'code'} type
* Type.
*
* @typedef Options
* @typedef CodeWithValues
* Code.
* @property {'code'} type
* Kind.
* @property {Array<string>} values
* Results.
*
* @typedef Comment
* Comment.
* @property {'comment'} type
* Type.
* @property {Array<string>} values
* Values.
*
* @typedef GenerateOptions
* @property {string} example
* @property {string | null | undefined} name
* @property {InstrumentResult} instrumented
*
* @typedef InstrumentOptions
* Configuration.
* @property {string} [heading]
* Heading to look for (default: `'usage'`).
* Wrapped in `new RegExp('^(' + value + ')$', 'i');`.
* @property {string} [example]
* Path to the example script.
* If given, resolved from `file.cwd`.
* If not given, the following values are attempted and resolved from
* `file.cwd`:
* @property {VFile} example
* File.
* @property {string} id
* ID.
* @property {string | null | undefined} [main]
* Main.
*
* * `'./example.js'`
* * `'./example/index.js'`
* * `'./examples.js'`
* * `'./examples/index.js'`
* * `'./doc/example.js'`
* * `'./doc/example/index.js'`
* * `'./docs/example.js'`
* * `'./docs/example/index.js'`
* @typedef InstrumentResult
* @property {string} result
* @property {Array<Log>} logs
* @property {Array<Reference>} references
*
* The first that exists, is used.
* @property {string} [name]
* Name of the module (default: `pkg.name`, optional).
* Used to rewrite `import x from './main.js'` to `import x from 'name'`.
* @property {string} [main]
* Path to the main file (default: `pkg.main` or `'./index.js'`, optional).
* If given, resolved from `file.cwd`.
* If inferred from `package.json`, resolved relating to that package root.
* Used to rewrite `import x from './main.js'` to `import x from 'name'`.
* @typedef Log
* Log.
* @property {number | null | undefined} end
* End.
* @property {string | undefined} language
* Code language.
* @property {number | null | undefined} start
* Start.
* @property {Array<string>} values
* Values.
*
* @typedef PackageInfo
* Info on the package.
* @property {PackageJson} value
* Data.
* @property {VFile} file
* File.
*
* @typedef Reference
* Reference.
* @property {number | null | undefined} end
* End.
* @property {string} quote
* Quote.
* @property {number | null | undefined} start
* Start.
*
* @typedef Skip
* Skip.
* @property {number} skip
* Skip.
* @property {'skip'} type
* Type.
*
* @typedef {Code | Comment | Skip} Token
* Token.
*/
import fs from 'node:fs'
/**
* @typedef Options
* Configuration.
* @property {string | null | undefined} [example]
* Path to example file (optional);
* resolved from `file.cwd`;
* defaults to the first example that exists:
*
* * `'example.js'`
* * `'example/index.js'`
* * `'examples.js'`
* * `'examples/index.js'`
* * `'doc/example.js'`
* * `'doc/example/index.js'`
* * `'docs/example.js'`
* * `'docs/example/index.js'`
* @property {string | null | undefined} [heading]
* Heading to look for (default: `'usage'`);
* wrapped in `new RegExp('^(' + value + ')$', 'i');`.
* @property {string | null | undefined} [main]
* Path to the file (default: `pkg.exports`, `pkg.main`, `'index.js'`);
* resolved from `file.cwd`;
* used to rewrite `import x from './main.js'` to `import x from 'name'`.
* @property {string | null | undefined} [name]
* Name of the module (default: `pkg.name`);
* used to rewrite `import x from './main.js'` to `import x from 'name'`.
*/
import {exec as execCallback} from 'node:child_process'
import fs from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import {pathToFileURL} from 'node:url'
import {promisify} from 'node:util'
import babel from '@babel/core'
import {resolve} from 'import-meta-resolve'
import {fromMarkdown} from 'mdast-util-from-markdown'
import {headingRange} from 'mdast-util-heading-range'
import {generate} from './generate/index.js'
import {nanoid} from 'nanoid'
import {removePosition} from 'unist-util-remove-position'
import {VFile} from 'vfile'
import {findUp} from 'vfile-find-up'
import {VFileMessage} from 'vfile-message'
const exec = promisify(execCallback)
const relativePrefix = './'
const defaultHeading = 'usage'
/** @type {Readonly<Options>} */
const emptyOptions = {}
/**
* Plugin to add a usage example to a readme.
* Add a usage example to a readme.
*
* @type {import('unified').Plugin<[Options?]|void[], Root>}
* Looks for the first heading matching `options.heading` (case insensitive),
* removes everything between it and an equal or higher next heading, and replaces
* that with a example.
*
* The example runs in Node.js (so no side effects!).
* Line comments (`//`) are turned into markdown.
* Calls to `console.log()` are exposed as code blocks, containing the logged
* values, so `console.log(1 + 1)` becomes `2`.
* Use a string as the first argument to `log` to use as the language for the code.
*
* You can ignore lines with `remark-usage-ignore-next`:
*
* ```js
* // remark-usage-ignore-next
* const two = sum(1, 1)
*
* // remark-usage-ignore-next 3
* function sum(a, b) {
* return a + b
* }
* ```
*
* …if no `skip` is given, 1 line is skipped.
*
* @param {Readonly<Options> | null | undefined} [options]
* Configuration (optional).
* @returns
* Transform.
*/
export default function remarkUsage(options = {}) {
export default function remarkUsage(options) {
const settings = options || emptyOptions
const header = new RegExp(
'^(' + (options.heading || defaultHeading) + ')$',
'^(' + (settings.heading || defaultHeading) + ')$',
'i'
)
return (tree, file, next) => {
/** @type {{tree: Root, file: VFile, options: Options, exampleInstrumentedPath?: string, nodes?: BlockContent[]}} */
const ctx = {tree, file, options}
/**
* Transform.
*
* @param {Root} tree
* Tree.
* @param {VFile} file
* File.
* @returns {Promise<undefined>}
* Nothing.
*/
return async function (tree, file) {
let exists = false

@@ -63,39 +201,584 @@

// node, and generate the example.
headingRange(tree, header, (start, nodes, end) => {
headingRange(tree, header, function () {
exists = true
return [start, ...nodes, end]
})
if (!exists) {
return next()
return
}
generate.run(
ctx,
/** @type {Callback} */
(error) => {
// If something failed and there’s an example, remove it.
if (ctx.exampleInstrumentedPath) {
try {
fs.unlinkSync(ctx.exampleInstrumentedPath)
// Catch just to be sure.
/* c8 ignore next */
} catch {}
const id = 'remark-usage-example-' + nanoid().toLowerCase()
const cwd = file.cwd
const from = file.path
? path.dirname(path.resolve(file.cwd, file.path))
: file.cwd
const pkg = await findPackage(from)
const name = settings.name || pkg?.value.name || undefined
/** @type {string | undefined} */
let main
try {
const exports = pkg?.value.exports
const primary =
/* c8 ignore next 2 -- seems useless to have an array, but types have it. */
exports && typeof exports === 'object' && Array.isArray(exports)
? exports[0]
: exports
const item =
primary && typeof primary === 'object' ? primary['.'] : primary
main = resolve(
relativeModule(
settings.main ||
(typeof item === 'string' ? item : undefined) ||
pkg?.value.main ||
'index.js'
),
pathToFileURL(settings.main ? cwd : pkg?.file.dirname || cwd).href + '/'
)
} catch {}
const example = await findExample(cwd, settings.example)
const instrumented = await instrumentExample(cwd, {example, id, main})
await run(example, instrumented, id)
// Add example.
headingRange(tree, header, function (start, _, end) {
const tokens = tokenize(String(example))
const nodes = generate(tokens, {
example: String(example),
instrumented,
name
})
return [start, ...nodes, end]
})
}
}
/**
* @param {string} from
* From.
* @returns {Promise<PackageInfo | undefined>}
* Nothing.
*/
async function findPackage(from) {
const file = await findUp('package.json', from)
if (!file) return
const doc = String(await fs.readFile(file.path))
/** @type {PackageJson} */
let value
try {
value = JSON.parse(doc)
} catch (error) {
const cause = /** @type {Error} */ (error)
throw new VFileMessage('Cannot parse `package.json` as JSON', {
cause,
ruleId: 'package-json-invalid',
source: 'remark-usage'
})
}
return {value, file}
}
/**
* @param {string} cwd
* @param {string | null | undefined} givenExample
* @returns {Promise<VFile>}
*/
async function findExample(cwd, givenExample) {
const example = givenExample
? findExplicitExample(cwd, givenExample)
: findImplicitExample(cwd)
if (!example) {
throw new VFileMessage(
'Cannot find example file to use, either pass `options.example` or use a name',
{
ruleId: 'example-missing',
source: 'remark-usage'
}
)
}
const url = new URL(example)
const value = String(await fs.readFile(url))
return new VFile({
path: url,
// Make sure there is a final line feed.
value: value.charAt(value.length - 1) === '\n' ? value : value + '\n'
})
}
/**
* @param {string} cwd
* Base.
* @param {string} example
* Name.
* @returns {string}
* URL.
*/
function findExplicitExample(cwd, example) {
const from = pathToFileURL(cwd).href + '/'
return resolve(relativeModule(example), from)
}
/**
* @param {string} cwd
* Base.
* @returns {string | undefined}
* URL.
*/
function findImplicitExample(cwd) {
const from = pathToFileURL(cwd).href + '/'
const examples = [
'./example.js',
'./example/index.js',
'./examples.js',
'./examples/index.js',
'./doc/example.js',
'./doc/example/index.js',
'./docs/example.js',
'./docs/example/index.js'
]
let index = -1
while (++index < examples.length) {
const example = examples[index]
try {
return resolve(example, from)
} catch {}
}
}
/**
* @param {string} cwd
* @param {InstrumentOptions} options
* @returns {Promise<InstrumentResult>}
* Nothing.
*/
async function instrumentExample(cwd, options) {
/** @type {Array<StringLiteral>} */
const nodes = []
/** @type {Array<Log>} */
const logs = []
/** @type {Array<Reference>} */
const references = []
/** @type {FileResult | null} */
let result
try {
result = await babel.transformAsync(String(options.example), {
caller: {name: 'remark-usage'},
cwd,
filename: options.example.path,
plugins: [addIdToConsoleLog],
sourceType: 'unambiguous'
})
} catch (error) {
const cause = /** @type {Error} */ (error)
throw new VFileMessage('Cannot parse example as JS with Babel', {
cause,
ruleId: 'example-invalid-babel',
source: 'remark-usage'
})
}
let index = -1
while (++index < nodes.length) {
const node = nodes[index]
const resolved = resolve(
node.value,
pathToFileURL(options.example.path).href
)
if (resolved === options.main) {
/* c8 ignore next -- babel always adds raw, but just to be sure. */
const raw = String((node && node.extra && node.extra.raw) || "'")
references.push({
end: node.end,
quote: raw.charAt(0),
start: node.start
})
}
}
return {
logs,
references,
/* c8 ignore next -- babel always returns a result (though types say it might not). */
result: result?.code || ''
}
function addIdToConsoleLog() {
const t = babel.types
let index = -1
/** @type {PluginPass} */
return {
visitor: {
/**
* @param {NodePathCallExpression} path
* Path.
* @returns {undefined}
* Nothing.
*/
CallExpression(path) {
const callee = path.get('callee')
const head = path.get('arguments.0')
if (
callee.isIdentifier({name: 'require'}) &&
!Array.isArray(head) &&
head.isStringLiteral()
) {
nodes.push(head.node)
}
if (callee.matchesPattern('console.log')) {
instrumentConsoleLog(path)
}
},
/**
* @param {NodePathImportDeclaration} path
* Path.
* @returns {undefined}
* Nothing.
*/
ImportDeclaration(path) {
nodes.push(path.node.source)
}
}
}
if (!error) {
// Add example.
headingRange(tree, header, (start, _, end) => [
start,
// `nodes` are always defined.
/* c8 ignore next */
...(ctx.nodes || []),
end
])
/**
* @param {NodePathCallExpression} path
* Path.
* @returns {undefined}
* Nothing.
*/
function instrumentConsoleLog(path) {
const node = path.node
const args = [...node.arguments]
const head = args[0]
/** @type {string | undefined} */
let language
index++
if (head && head.type === 'StringLiteral' && args.length > 1) {
language = head.value
args.shift()
}
logs[index] = {
end: node.end,
language,
start: node.start,
values: []
}
node.arguments = [
t.stringLiteral('<' + options.id + '-' + index + '>'),
...args,
t.stringLiteral('</' + options.id + '>')
]
}
}
}
/**
* @param {VFile} example
* @param {InstrumentResult} instrumented
* @param {string} id
* @returns {Promise<undefined>}
* Nothing.
*/
async function run(example, instrumented, id) {
/* c8 ignore next -- there’s always a dirname. */
const filePath = path.join(example.dirname || '', id + example.extname)
let stdout = ''
await fs.writeFile(filePath, instrumented.result)
try {
const result = await exec([process.execPath, filePath].join(' '))
stdout = result.stdout
} catch (error) {
const cause = /** @type {Error} */ (error)
throw new VFileMessage('Cannot run example with Node', {
cause,
ruleId: 'example-invalid-node',
source: 'remark-usage'
})
} finally {
await fs.unlink(filePath)
}
const open = new RegExp('<' + id + '-(\\d+)>', 'g')
const close = new RegExp('</' + id + '>', 'g')
/** @type {RegExpExecArray | null} */
let startMatch
while ((startMatch = open.exec(stdout))) {
close.lastIndex = startMatch.index
const endMatch = close.exec(stdout)
// Else should never occur, console is sync, but just to be sure.
if (endMatch) {
const start = startMatch.index + startMatch[0].length
const end = endMatch.index
const logIndex = Number.parseInt(startMatch[1], 10)
const log = instrumented.logs[logIndex]
let value = stdout.slice(start, end)
// Else should never occur, console adds spaces at start and end, just
// to be sure we’re checking it though.
if (value.charAt(0) === ' ' && value.charAt(value.length - 1) === ' ') {
value = value.slice(1, -1)
}
log.values.push(value)
open.lastIndex = end
}
}
}
/**
* @param {string} value
* @returns {Array<Token>}
* Tokens.
*/
function tokenize(value) {
const lineBreak = /\r?\n/g
const lineComment = /^\s*\/\/\s*/
const ignoreComment = /^remark-usage-ignore-next(?:\s+(\d+))?/
/** @type {Array<Token>} */
const tokens = []
let start = 0
let skip = 0
/** @type {RegExpExecArray | null} */
let match
/** @type {Token | undefined} */
let token
while ((match = lineBreak.exec(value))) {
const end = match.index
let line = value.slice(start, end)
// If the line is supposed to be skipped, skip it.
// Skipping can only happen by starting a comment.
if (skip) {
skip--
}
// Empty:
else if (line.trim().length === 0) {
if (token) {
if (token.type === 'comment') {
token.values.push(match[0])
} else if (token.type === 'code') {
token.end = end
}
}
} else {
const comment = line.match(lineComment)
return next(error)
if (comment) {
line = line.slice(comment[0].length)
const ignore = line.match(ignoreComment)
if (ignore) {
// Skip next couple of lines.
skip =
(ignore[1] !== undefined && Number.parseInt(ignore[1], 10)) || 1
token = {type: 'skip', skip}
tokens.push(token)
} else {
if (!token || token.type !== 'comment') {
token = {type: 'comment', values: []}
tokens.push(token)
}
token.values.push(line, match[0])
}
} else {
if (!token || token.type !== 'code') {
token = {type: 'code', start, end}
tokens.push(token)
}
token.end = end
}
)
}
start = end + match[0].length
}
return tokens
}
/**
* @param {Array<Token>} tokens
* Tokens.
* @param {GenerateOptions} options
* Configuration.
* @returns {Array<RootContent>}
* Result.
*/
function generate(tokens, options) {
/** @type {Array<RootContent>} */
const nodes = []
let index = -1
while (++index < tokens.length) {
const token = tokens[index]
const result =
token.type === 'comment'
? comment(token)
: token.type === 'code'
? code(token, options)
: undefined
if (result) {
nodes.push(...result)
}
}
return nodes
}
/**
* @param {Comment} token
* Token.
* @returns {Array<RootContent>}
* Result.
*/
function comment(token) {
const tree = fromMarkdown(token.values.join(''))
removePosition(tree)
return tree.children
}
/**
* @param {Code} token
* Token.
* @param {GenerateOptions} options
* Configuration.
* @returns {Array<RootContent>}
* Result.
*/
function code(token, options) {
/** @type {Array<CodeWithValues | Log>} */
const tokens = []
let start = token.start
/** @type {CodeWithValues | Log | undefined} */
let tok
while (start < token.end) {
let lineEnd = options.example.indexOf('\n', start)
lineEnd = lineEnd === -1 || lineEnd >= token.end ? token.end : lineEnd
const consoleCall = findInLine(options.instrumented.logs, start, lineEnd)
if (consoleCall && consoleCall.values.length > 0) {
if (tok && tok === consoleCall) {
// Ignore: it’s the same multiline console call.
} else {
tok = consoleCall
tokens.push(tok)
}
} else {
const mainReference = options.name
? findInLine(options.instrumented.references, start, lineEnd)
: undefined
const line =
mainReference &&
typeof mainReference.start === 'number' &&
typeof mainReference.end === 'number'
? options.example.slice(start, mainReference.start) +
mainReference.quote +
options.name +
mainReference.quote +
options.example.slice(mainReference.end, lineEnd)
: options.example.slice(start, lineEnd)
if (!tok || !('type' in tok) || tok.type !== 'code') {
tok = {type: 'code', values: []}
tokens.push(tok)
}
tok.values.push(line)
}
start = lineEnd + 1
}
/** @type {Array<RootContent>} */
const nodes = []
let index = -1
while (++index < tokens.length) {
const token = tokens[index]
nodes.push({
type: 'code',
lang: 'language' in token ? token.language : 'javascript',
value: token.values.join('\n').replace(/^\n+|\n+$/g, '')
})
}
return nodes
}
/**
* @template {Log | Reference} Value
* Token kind.
* @param {Array<Value>} values
* Values.
* @param {number} start
* Start.
* @param {number} end
* End.
* @returns {Value | undefined}
* Found.
*/
function findInLine(values, start, end) {
let index = -1
while (++index < values.length) {
const reference = values[index]
if (
typeof reference.start === 'number' &&
typeof reference.end === 'number' &&
// Reference in:
((reference.start >= start && reference.end <= end) ||
// Line in reference:
(start >= reference.start && end <= reference.end))
) {
return reference
}
}
}
/**
* Make a path relative.
*
* @param {string} moduleId
* Specifier.
* @returns {string}
* Relative specifier.
*/
function relativeModule(moduleId) {
return moduleId.slice(0, 2) === relativePrefix
? moduleId
: relativePrefix + moduleId
}
{
"name": "remark-usage",
"version": "10.0.1",
"version": "11.0.0",
"description": "remark plugin to add a usage example to your readme",
"license": "MIT",
"keywords": [
"unified",
"markdown",
"mdast",
"plain",
"plugin",
"remark",
"remark-plugin",
"plugin",
"mdast",
"markdown",
"plain",
"text"
"text",
"unified"
],

@@ -30,4 +30,3 @@ "repository": "remarkjs/remark-usage",

"type": "module",
"main": "index.js",
"types": "index.d.ts",
"exports": "./index.js",
"files": [

@@ -40,63 +39,45 @@ "lib/",

"@babel/core": "^7.0.0",
"@types/mdast": "^3.0.0",
"import-meta-resolve": "^1.0.0",
"mdast-util-heading-range": "^3.0.0",
"nanoid": "^3.0.0",
"remark-parse": "^10.0.0",
"to-vfile": "^7.0.0",
"trough": "^2.0.0",
"unified": "^10.0.0",
"unist-util-remove-position": "^4.0.0",
"vfile": "^5.0.2",
"vfile-find-up": "^6.0.0"
"@types/mdast": "^4.0.0",
"import-meta-resolve": "^3.0.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-heading-range": "^4.0.0",
"nanoid": "^4.0.0",
"unist-util-remove-position": "^5.0.0",
"vfile": "^6.0.0",
"vfile-find-up": "^7.0.0",
"vfile-message": "^4.0.0"
},
"devDependencies": {
"@types/babel__core": "^7.0.0",
"@types/tape": "^4.0.0",
"c8": "^7.0.0",
"is-hidden": "^2.0.0",
"prettier": "^2.0.0",
"remark": "^14.0.0",
"remark-cli": "^10.0.0",
"@types/node": "^20.0.0",
"c8": "^8.0.0",
"prettier": "^3.0.0",
"remark": "^15.0.0",
"remark-cli": "^11.0.0",
"remark-preset-wooorm": "^9.0.0",
"rimraf": "^3.0.0",
"tape": "^5.0.0",
"to-vfile": "^8.0.0",
"type-coverage": "^2.0.0",
"type-fest": "^2.0.0",
"typescript": "^4.0.0",
"xo": "^0.45.0"
"type-fest": "^4.0.0",
"typescript": "^5.0.0",
"xo": "^0.56.0"
},
"scripts": {
"build": "rimraf \"lib/**/*.d.ts\" \"test/index.d.ts\" \"index.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 npm run test-api",
"test": "npm run build && npm run format && npm run test-coverage"
"test-coverage": "c8 --100 --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,
"ignores": [
"example.js",
"test/fixtures/**/*.js"
],
"overrides": [
{
"files": "test/**/*.js",
"rules": {
"no-await-in-loop": "off"
}
}
]
},
"remarkConfig": {
"plugins": [
"preset-wooorm",
"remark-preset-wooorm",
[

@@ -113,5 +94,26 @@ "./index.js",

"detail": true,
"strict": true,
"ignoreCatch": true
"ignoreCatch": true,
"strict": true
},
"xo": {
"ignores": [
"example.js",
"test/fixtures/**/*.js"
],
"overrides": [
{
"files": [
"test/**/*.js"
],
"rules": {
"no-await-in-loop": "off"
}
}
],
"prettier": true,
"rules": {
"unicorn/prefer-at": "off",
"unicorn/prefer-string-replace-all": "off"
}
}
}

@@ -6,3 +6,2 @@ # remark-usage

[![Downloads][downloads-badge]][downloads]
[![Size][size-badge]][size]
[![Sponsors][sponsors-badge]][collective]

@@ -12,3 +11,3 @@ [![Backers][backers-badge]][collective]

[**remark**][remark] plugin to add a [usage][] example to a readme.
**[remark][]** plugin to add a [usage][section-use] example to a readme.

@@ -23,2 +22,3 @@ ## Contents

* [`unified().use(remarkUsage[, options])`](#unifieduseremarkusage-options)
* [`Options`](#options)
* [Types](#types)

@@ -33,12 +33,5 @@ * [Compatibility](#compatibility)

This package is a [unified][] ([remark][]) plugin to add a Usage section to
This package is a [unified][] ([remark][]) plugin to add a usage section to
markdown.
unified is an AST (abstract syntax tree) based transform project.
**remark** is everything unified that relates to markdown.
The layer under remark is called mdast, which is only concerned with syntax
trees.
Another layer underneath is micromark, which is only concerned with parsing.
This package is a small wrapper to integrate all of these.
## When should I use this?

@@ -51,4 +44,4 @@

This package is [ESM only](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c).
In Node.js (12.20+, 14.14+, 16.0+), install with [npm][]:
This package is [ESM only][esm].
In Node.js (version 16+), install with [npm][]:

@@ -89,15 +82,15 @@ ```sh

…If we use `remark-usage`, we can generate the `Usage` section
…if we use `remark-usage`, we can generate the `Usage` section
```javascript
import {readSync} from 'to-vfile'
import {remark} from 'remark'
import remarkUsage from 'remark-usage'
import {read} from 'to-vfile'
const file = readSync({path: 'readme.md', cwd: 'example'})
const file = await read({path: 'readme.md', cwd: 'example'})
const result = await remark().use(remarkUsage).process(file)
await remark().use(remarkUsage).process(file)
```
Now, printing `result` (the newly generated readme) yields:
…then printing `file` (the newly generated readme) yields:

@@ -131,23 +124,20 @@ ````markdown

This package exports no identifiers.
The default export is `remarkUsage`.
The default export is [`remarkUsage`][api-remark-usage].
### `unified().use(remarkUsage[, options])`
Add `example.js` to the `Usage` section in a readme.
Add a usage example to a readme.
Replaces the current content between the heading containing the text “usage”
(configurable) and the next heading of the same (or higher) rank with the
example.
Looks for the first heading matching `options.heading` (case insensitive),
removes everything between it and an equal or higher next heading, and replaces
that with an example.
The example is run in Node.js.
Make sure no side effects occur when running `example.js`.
Line comments are parsed as markdown.
The example runs in Node.js (so no side effects!).
Line comments (`//`) are turned into markdown.
Calls to `console.log()` are exposed as code blocks, containing the logged
values (optionally with a language flag).
values, so `console.log(1 + 1)` becomes `2`.
Use a string as the first argument to `log` to use as the language for the code.
It may help to compare [`example.js`][example-js] with the above [use][usage]
section.
You can ignore lines with `remark-usage-ignore-next`:
You can ignore lines like so:
```js

@@ -165,46 +155,52 @@ // remark-usage-ignore-next

##### `options`
###### Parameters
###### `options.heading`
* `options` ([`Options`][api-options], optional)
— configuration
Heading to look for (`string?`, default: `'usage'`).
Wrapped in `new RegExp('^(' + value + ')$', 'i');`.
###### Returns
###### `options.example`
Transform ([`Transformer`][unified-transformer]).
Path to the example (`string?`).
If given, resolved from [`file.cwd`][file-cwd].
If not given, the following values are attempted and resolved from `file.cwd`:
`'./example.js'`, `'./example/index.js'`, `'./examples.js'`,
`'./examples/index.js'`, `'./doc/example.js'`, `'./doc/example/index.js'`,
`'./docs/example.js'`, `'./docs/example/index.js'`.
The first that exists, is used.
### `Options`
###### `options.name`
Configuration (TypeScript type).
Name of the module (`string?`, default: `pkg.name`, optional).
Used to rewrite `require('.')` to `require('name')`.
###### Fields
###### `options.main`
* `example` (`string`, optional)
— path to example file (optional);
resolved from `file.cwd`;
defaults to the first example that exists: `'example.js'`,
`'example/index.js'`, `'examples.js'`, `'examples/index.js'`,
`'doc/example.js'`, `'doc/example/index.js'`, `'docs/example.js'`,
`'docs/example/index.js'`
* `heading` (`string`, default: `'usage'`)
— heading to look for;
wrapped in `new RegExp('^(' + value + ')$', 'i');`
* `main` (`string`, default: `pkg.exports`, `pkg.main`, `'index.js'`)
— path to the file;
resolved from `file.cwd`;
used to rewrite `import x from './main.js'` to `import x from 'name'`
* `name` (`string`, default: `pkg.name`)
— name of the module;
used to rewrite `import x from './main.js'` to `import x from 'name'`
Path to the main file (`string?`, default: `pkg.main` or `'.'`, optional).
If given, resolved from [`file.cwd`][file-cwd].
If inferred from `package.json`, resolved relating to that package root.
Used to rewrite `require('.')` to `require('name')`.
## Types
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.
This plugin works with remark 12+ and `remark-cli` 8+.
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, `remark-usage@^11`,
compatible with Node.js 16.
This plugin works with remark version 12+ and `remark-cli` version 8+.
## Security

@@ -253,6 +249,2 @@

[size-badge]: https://img.shields.io/bundlephobia/minzip/remark-usage.svg
[size]: https://bundlephobia.com/result?p=remark-usage
[sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg

@@ -270,9 +262,11 @@

[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
[health]: https://github.com/remarkjs/.github
[contributing]: https://github.com/remarkjs/.github/blob/HEAD/contributing.md
[contributing]: https://github.com/remarkjs/.github/blob/main/contributing.md
[support]: https://github.com/remarkjs/.github/blob/HEAD/support.md
[support]: https://github.com/remarkjs/.github/blob/main/support.md
[coc]: https://github.com/remarkjs/.github/blob/HEAD/code-of-conduct.md
[coc]: https://github.com/remarkjs/.github/blob/main/code-of-conduct.md

@@ -283,4 +277,2 @@ [license]: license

[unified]: https://github.com/unifiedjs/unified
[remark]: https://github.com/remarkjs/remark

@@ -290,6 +282,10 @@

[file-cwd]: https://github.com/vfile/vfile#vfilecwd
[unified]: https://github.com/unifiedjs/unified
[usage]: #use
[unified-transformer]: https://github.com/unifiedjs/unified#transformer
[example-js]: example.js
[section-use]: #use
[api-options]: #options
[api-remark-usage]: #unifieduseremarkusage-options
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc