Files
documentation/node_modules/micromark-util-events-to-acorn/index.js
2024-03-22 03:47:51 +05:30

419 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @typedef {import('acorn').Comment} Comment
* @typedef {import('acorn').Node} AcornNode
* @typedef {import('acorn').Options} AcornOptions
* @typedef {import('acorn').Token} Token
* @typedef {import('estree').Node} EstreeNode
* @typedef {import('estree').Program} Program
* @typedef {import('micromark-util-types').Chunk} Chunk
* @typedef {import('micromark-util-types').Event} Event
* @typedef {import('micromark-util-types').Point} MicromarkPoint
* @typedef {import('micromark-util-types').TokenType} TokenType
* @typedef {import('unist').Point} UnistPoint
*/
/**
* @typedef Acorn
* Acorn-like interface.
* @property {import('acorn').parse} parse
* Parse a program.
* @property {import('acorn').parseExpressionAt} parseExpressionAt
* Parse an expression.
*
* @typedef AcornLoc
* @property {number} line
* @property {number} column
*
* @typedef AcornErrorFields
* @property {number} raisedAt
* @property {number} pos
* @property {AcornLoc} loc
*
* @typedef {Error & AcornErrorFields} AcornError
*
* @typedef Options
* Configuration.
* @property {Acorn} acorn
* Typically `acorn`, object with `parse` and `parseExpressionAt` fields (required).
* @property {Array<TokenType>} tokenTypes
* Names of (void) tokens to consider as data; `'lineEnding'` is always
* included (required).
* @property {AcornOptions | null | undefined} [acornOptions]
* Configuration for `acorn` (optional).
* @property {MicromarkPoint | null | undefined} [start]
* Place where events start (optional, required if `allowEmpty`).
* @property {string | null | undefined} [prefix='']
* Text to place before events (default: `''`).
* @property {string | null | undefined} [suffix='']
* Text to place after events (default: `''`).
* @property {boolean | null | undefined} [expression=false]
* Whether this is a program or expression (default: `false`).
* @property {boolean | null | undefined} [allowEmpty=false]
* Whether an empty expression is allowed (programs are always allowed to
* be empty) (default: `false`).
*
* @typedef Result
* Result.
* @property {Program | undefined} estree
* Program.
* @property {AcornError | undefined} error
* Error if unparseable
* @property {boolean} swallow
* Whether the error, if there is one, can be swallowed and more JavaScript
* could be valid.
*
* @typedef {[number, MicromarkPoint]} Stop
*
* @typedef Collection
* @property {string} value
* @property {Array<Stop>} stops
*/
import { visit } from 'estree-util-visit';
import { VFileMessage } from 'vfile-message';
/**
* Parse a list of micromark events with acorn.
*
* @param {Array<Event>} events
* Events.
* @param {Options} options
* Configuration (required).
* @returns {Result}
* Result.
*/
// eslint-disable-next-line complexity
export function eventsToAcorn(events, options) {
const prefix = options.prefix || '';
const suffix = options.suffix || '';
const acornOptions = Object.assign({}, options.acornOptions);
/** @type {Array<Comment>} */
const comments = [];
/** @type {Array<Token>} */
const tokens = [];
const onComment = acornOptions.onComment;
const onToken = acornOptions.onToken;
let swallow = false;
/** @type {AcornNode | undefined} */
let estree;
/** @type {AcornError | undefined} */
let exception;
/** @type {AcornOptions} */
const acornConfig = Object.assign({}, acornOptions, {
onComment: comments,
preserveParens: true
});
if (onToken) {
acornConfig.onToken = tokens;
}
const collection = collect(events, options.tokenTypes);
const source = collection.value;
const value = prefix + source + suffix;
const isEmptyExpression = options.expression && empty(source);
if (isEmptyExpression && !options.allowEmpty) {
throw new VFileMessage('Unexpected empty expression', {
place: parseOffsetToUnistPoint(0),
ruleId: 'unexpected-empty-expression',
source: 'micromark-extension-mdx-expression'
});
}
try {
estree = options.expression && !isEmptyExpression ? options.acorn.parseExpressionAt(value, 0, acornConfig) : options.acorn.parse(value, acornConfig);
} catch (error_) {
const error = /** @type {AcornError} */error_;
const point = parseOffsetToUnistPoint(error.pos);
error.message = String(error.message).replace(/ \(\d+:\d+\)$/, '');
// Always defined in our unist points that come from micromark.
error.pos = point.offset;
error.loc = {
line: point.line,
column: point.column - 1
};
exception = error;
swallow = error.raisedAt >= prefix.length + source.length ||
// Broken comments are raised at their start, not their end.
error.message === 'Unterminated comment';
}
if (estree && options.expression && !isEmptyExpression) {
if (empty(value.slice(estree.end, value.length - suffix.length))) {
estree = {
type: 'Program',
start: 0,
end: prefix.length + source.length,
// @ts-expect-error: Its good.
body: [{
type: 'ExpressionStatement',
expression: estree,
start: 0,
end: prefix.length + source.length
}],
sourceType: 'module',
comments: []
};
} else {
const point = parseOffsetToUnistPoint(estree.end);
const error = /** @type {AcornError} */
new Error('Unexpected content after expression');
// Always defined in our unist points that come from micromark.
error.pos = point.offset;
error.loc = {
line: point.line,
column: point.column - 1
};
exception = error;
estree = undefined;
}
}
if (estree) {
// @ts-expect-error: acorn *does* allow comments
estree.comments = comments;
// @ts-expect-error: acorn looks enough like estree.
visit(estree, function (esnode, field, index, parents) {
let context = /** @type {AcornNode | Array<AcornNode>} */
parents[parents.length - 1];
/** @type {number | string | undefined} */
let prop = field;
// Remove non-standard `ParenthesizedExpression`.
// @ts-expect-error: included in acorn.
if (esnode.type === 'ParenthesizedExpression' && context && prop) {
/* c8 ignore next 5 */
if (typeof index === 'number') {
// @ts-expect-error: indexable.
context = context[prop];
prop = index;
}
// @ts-expect-error: indexable.
context[prop] = esnode.expression;
}
fixPosition(esnode);
});
// Comment positions are fixed by `visit` because theyre in the tree.
if (Array.isArray(onComment)) {
onComment.push(...comments);
} else if (typeof onComment === 'function') {
for (const comment of comments) {
onComment(comment.type === 'Block', comment.value, comment.start, comment.end, comment.loc.start, comment.loc.end);
}
}
for (const token of tokens) {
// Ignore tokens that ends in prefix or start in suffix:
if (token.end <= prefix.length || token.start - prefix.length >= source.length) {
continue;
}
fixPosition(token);
if (Array.isArray(onToken)) {
onToken.push(token);
} else {
// `tokens` are not added if `onToken` is not defined, so it must be a
// function.
onToken(token);
}
}
}
// @ts-expect-error: Its a program now.
return {
estree,
error: exception,
swallow
};
/**
* Update the position of a node.
*
* @param {AcornNode | EstreeNode | Token} nodeOrToken
* @returns {undefined}
*/
function fixPosition(nodeOrToken) {
const pointStart = parseOffsetToUnistPoint(nodeOrToken.start);
const pointEnd = parseOffsetToUnistPoint(nodeOrToken.end);
// Always defined in our unist points that come from micromark.
nodeOrToken.start = pointStart.offset;
nodeOrToken.end = pointEnd.offset;
nodeOrToken.loc = {
start: {
line: pointStart.line,
column: pointStart.column - 1,
offset: pointStart.offset
},
end: {
line: pointEnd.line,
column: pointEnd.column - 1,
offset: pointEnd.offset
}
};
nodeOrToken.range = [nodeOrToken.start, nodeOrToken.end];
}
/**
* Turn an arbitrary offset into the parsed value, into a point in the source
* value.
*
* @param {number} acornOffset
* @returns {UnistPoint}
*/
function parseOffsetToUnistPoint(acornOffset) {
let sourceOffset = acornOffset - prefix.length;
if (sourceOffset < 0) {
sourceOffset = 0;
} else if (sourceOffset > source.length) {
sourceOffset = source.length;
}
let point = relativeToPoint(collection.stops, sourceOffset);
if (!point) {
point = {
line: options.start.line,
column: options.start.column,
offset: options.start.offset
};
}
return point;
}
}
/**
* @param {string} value
* @returns {boolean}
*/
function empty(value) {
return /^\s*$/.test(value
// Multiline comments.
.replace(/\/\*[\s\S]*?\*\//g, '')
// Line comments.
// EOF instead of EOL is specifically not allowed, because that would
// mean the closing brace is on the commented-out line
.replace(/\/\/[^\r\n]*(\r\n|\n|\r)/g, ''));
}
// Port from <https://github.com/wooorm/markdown-rs/blob/e692ab0/src/util/mdx_collect.rs#L15>.
/**
* @param {Array<Event>} events
* @param {Array<TokenType>} tokenTypes
* @returns {Collection}
*/
function collect(events, tokenTypes) {
/** @type {Collection} */
const result = {
value: '',
stops: []
};
let index = -1;
while (++index < events.length) {
const event = events[index];
// Assume void.
if (event[0] === 'enter') {
const type = event[1].type;
if (type === "lineEnding" || tokenTypes.includes(type)) {
const chunks = event[2].sliceStream(event[1]);
// Drop virtual spaces.
while (chunks.length > 0 && chunks[0] === -1) {
chunks.shift();
}
const value = serializeChunks(chunks);
result.stops.push([result.value.length, event[1].start]);
result.value += value;
result.stops.push([result.value.length, event[1].end]);
}
}
}
return result;
}
// Port from <https://github.com/wooorm/markdown-rs/blob/e692ab0/src/util/location.rs#L91>.
/**
* Turn a relative offset into an absolute offset.
*
* @param {Array<Stop>} stops
* @param {number} relative
* @returns {UnistPoint | undefined}
*/
function relativeToPoint(stops, relative) {
let index = 0;
while (index < stops.length && stops[index][0] <= relative) {
index += 1;
}
// There are no points: that only occurs if there was an empty string.
if (index === 0) {
return undefined;
}
const [stopRelative, stopAbsolute] = stops[index - 1];
const rest = relative - stopRelative;
return {
line: stopAbsolute.line,
column: stopAbsolute.column + rest,
offset: stopAbsolute.offset + rest
};
}
// Copy from <https://github.com/micromark/micromark/blob/ce3593a/packages/micromark/dev/lib/create-tokenizer.js#L595>
// To do: expose that?
/**
* Get the string value of a slice of chunks.
*
* @param {Array<Chunk>} chunks
* @returns {string}
*/
function serializeChunks(chunks) {
let index = -1;
/** @type {Array<string>} */
const result = [];
/** @type {boolean | undefined} */
let atTab;
while (++index < chunks.length) {
const chunk = chunks[index];
/** @type {string} */
let value;
if (typeof chunk === 'string') {
value = chunk;
} else switch (chunk) {
case -5:
{
value = "\r";
break;
}
case -4:
{
value = "\n";
break;
}
case -3:
{
value = "\r" + "\n";
break;
}
case -2:
{
value = "\t";
break;
}
/* c8 ignore next 6 */
case -1:
{
if (atTab) continue;
value = " ";
break;
}
default:
{
// Currently only replacement character.
// eslint-disable-next-line unicorn/prefer-code-point
value = String.fromCharCode(chunk);
}
}
atTab = chunk === -2;
result.push(value);
}
return result.join('');
}