micromark-util-events-to-acorn
Advanced tools
Comparing version 1.2.1 to 1.2.2
@@ -5,38 +5,101 @@ /** | ||
* @param {Array<Event>} events | ||
* Events. | ||
* @param {Options} options | ||
* @returns {{estree: Program | undefined, error: Error | undefined, swallow: boolean}} | ||
* Configuration. | ||
* @returns {Result} | ||
* Result. | ||
*/ | ||
export function eventsToAcorn(events: Array<import("micromark-util-types").Event>, options: Options): { | ||
estree: Program | undefined; | ||
error: Error | undefined; | ||
swallow: boolean; | ||
}; | ||
export type Event = import('micromark-util-types').Event; | ||
export type Point = import('micromark-util-types').Point; | ||
export type AcornOptions = import('acorn').Options; | ||
export type Comment = import('acorn').Comment; | ||
export type Token = import('acorn').Token; | ||
export type AcornNode = import('acorn').Node; | ||
export type Program = import('estree').Program; | ||
export type EstreeNode = import('estree').Node; | ||
export function eventsToAcorn( | ||
events: Array<import('micromark-util-types').Event>, | ||
options: Options | ||
): Result | ||
export type Comment = import('acorn').Comment | ||
export type AcornNode = import('acorn').Node | ||
export type AcornOptions = import('acorn').Options | ||
export type Token = import('acorn').Token | ||
export type EstreeNode = import('estree').Node | ||
export type Program = import('estree').Program | ||
export type Chunk = import('micromark-util-types').Chunk | ||
export type Event = import('micromark-util-types').Event | ||
export type MicromarkPoint = import('micromark-util-types').Point | ||
export type UnistPoint = import('unist').Point | ||
/** | ||
* Acorn-like interface. | ||
*/ | ||
export type Acorn = { | ||
parse: typeof import("acorn").parse; | ||
parseExpressionAt: typeof import("acorn").parseExpressionAt; | ||
}; | ||
export type AcornError = Error & { | ||
raisedAt: number; | ||
pos: number; | ||
loc: { | ||
line: number; | ||
column: number; | ||
}; | ||
}; | ||
/** | ||
* Parse a program. | ||
*/ | ||
parse: typeof import('acorn').parse | ||
/** | ||
* Parse an expression. | ||
*/ | ||
parseExpressionAt: typeof import('acorn').parseExpressionAt | ||
} | ||
export type AcornLoc = { | ||
line: number | ||
column: number | ||
} | ||
export type AcornErrorFields = { | ||
raisedAt: number | ||
pos: number | ||
loc: AcornLoc | ||
} | ||
export type AcornError = Error & AcornErrorFields | ||
/** | ||
* Configuration. | ||
*/ | ||
export type Options = { | ||
acorn: Acorn; | ||
acornOptions?: AcornOptions | null | undefined; | ||
start?: Point | null | undefined; | ||
prefix?: string | null | undefined; | ||
suffix?: string | null | undefined; | ||
expression?: boolean | null | undefined; | ||
allowEmpty?: boolean | null | undefined; | ||
}; | ||
/** | ||
* Typically `acorn`, object with `parse` and `parseExpressionAt` fields. | ||
*/ | ||
acorn: Acorn | ||
/** | ||
* Configuration for `acorn`. | ||
*/ | ||
acornOptions?: AcornOptions | null | undefined | ||
/** | ||
* Place where events start. | ||
*/ | ||
start?: MicromarkPoint | null | undefined | ||
/** | ||
* Text to place before events. | ||
*/ | ||
prefix?: string | null | undefined | ||
/** | ||
* Text to place after events. | ||
*/ | ||
suffix?: string | null | undefined | ||
/** | ||
* Whether this is a program or expression. | ||
*/ | ||
expression?: boolean | null | undefined | ||
/** | ||
* Whether an empty expression is allowed (programs are always allowed to | ||
* be empty). | ||
*/ | ||
allowEmpty?: boolean | null | undefined | ||
} | ||
/** | ||
* Result. | ||
*/ | ||
export type Result = { | ||
/** | ||
* Program. | ||
*/ | ||
estree: Program | undefined | ||
/** | ||
* Error if unparseable | ||
*/ | ||
error: AcornError | undefined | ||
/** | ||
* Whether the error, if there is one, can be swallowed and more JavaScript | ||
* could be valid. | ||
*/ | ||
swallow: boolean | ||
} | ||
export type Stop = [number, MicromarkPoint] | ||
export type Collection = { | ||
value: string | ||
stops: Array<Stop> | ||
} |
306
dev/index.js
/** | ||
* @typedef {import('micromark-util-types').Event} Event | ||
* @typedef {import('micromark-util-types').Point} Point | ||
* @typedef {import('acorn').Comment} Comment | ||
* @typedef {import('acorn').Node} AcornNode | ||
* @typedef {import('acorn').Options} AcornOptions | ||
* @typedef {import('acorn').Comment} Comment | ||
* @typedef {import('acorn').Token} Token | ||
* @typedef {import('acorn').Node} AcornNode | ||
* @typedef {import('estree').Node} EstreeNode | ||
* @typedef {import('estree').Program} Program | ||
* @typedef {import('estree').Node} EstreeNode | ||
* @typedef {import('micromark-util-types').Chunk} Chunk | ||
* @typedef {import('micromark-util-types').Event} Event | ||
* @typedef {import('micromark-util-types').Point} MicromarkPoint | ||
* @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 {{parse: import('acorn').parse, parseExpressionAt: import('acorn').parseExpressionAt}} Acorn | ||
* @typedef {Error & {raisedAt: number, pos: number, loc: {line: number, column: number}}} AcornError | ||
* @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. | ||
* @property {AcornOptions | null | undefined} [acornOptions] | ||
* @property {Point | null | undefined} [start] | ||
* Configuration for `acorn`. | ||
* @property {MicromarkPoint | null | undefined} [start] | ||
* Place where events start. | ||
* @property {string | null | undefined} [prefix=''] | ||
* Text to place before events. | ||
* @property {string | null | undefined} [suffix=''] | ||
* Text to place after events. | ||
* @property {boolean | null | undefined} [expression=false] | ||
* Whether this is a program or expression. | ||
* @property {boolean | null | undefined} [allowEmpty=false] | ||
* Whether an empty expression is allowed (programs are always allowed to | ||
* be empty). | ||
* | ||
* @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 {codes} from 'micromark-util-symbol/codes.js' | ||
import {values} from 'micromark-util-symbol/values.js' | ||
import {types} from 'micromark-util-symbol/types.js' | ||
import {ok as assert} from 'uvu/assert' | ||
import {visit} from 'estree-util-visit' | ||
import {VFileMessage} from 'vfile-message' | ||
import {location} from 'vfile-location' | ||
@@ -33,4 +79,7 @@ /** | ||
* @param {Array<Event>} events | ||
* Events. | ||
* @param {Options} options | ||
* @returns {{estree: Program | undefined, error: Error | undefined, swallow: boolean}} | ||
* Configuration. | ||
* @returns {Result} | ||
* Result. | ||
*/ | ||
@@ -48,14 +97,7 @@ // eslint-disable-next-line complexity | ||
const onToken = acornOptions.onToken | ||
/** @type {Array<string>} */ | ||
const chunks = [] | ||
/** @type {Record<string, Point>} */ | ||
const lines = {} | ||
let index = -1 | ||
let swallow = false | ||
/** @type {AcornNode | undefined} */ | ||
let estree | ||
/** @type {Error | undefined} */ | ||
/** @type {AcornError | undefined} */ | ||
let exception | ||
/** @type {number} */ | ||
let startLine | ||
/** @type {AcornOptions} */ | ||
@@ -71,24 +113,21 @@ const acornConfig = Object.assign({}, acornOptions, { | ||
// We use `events` to detect everything, however, it could be empty. | ||
// In that case, we need `options.start` to make sense of positional info. | ||
if (options.start) { | ||
startLine = options.start.line | ||
lines[startLine] = options.start | ||
} | ||
const collection = collect(events, [ | ||
types.lineEnding, | ||
// To do: these should be passed by users in parameters. | ||
'expressionChunk', // From tests. | ||
'mdxFlowExpressionChunk', // Flow chunk. | ||
'mdxTextExpressionChunk', // Text chunk. | ||
// JSX: | ||
'mdxJsxTextTagExpressionAttributeValue', | ||
'mdxJsxTextTagAttributeValueExpressionValue', | ||
'mdxJsxFlowTagExpressionAttributeValue', | ||
'mdxJsxFlowTagAttributeValueExpressionValue', | ||
// ESM: | ||
'mdxjsEsmData' | ||
]) | ||
while (++index < events.length) { | ||
const [kind, token, context] = events[index] | ||
const source = collection.value | ||
// Assume only void events (and `enter` followed immediately by an `exit`). | ||
if (kind === 'exit') { | ||
chunks.push(context.sliceSerialize(token)) | ||
setPoint(token.start) | ||
setPoint(token.end) | ||
} | ||
} | ||
const source = chunks.join('') | ||
const value = prefix + source + suffix | ||
const isEmptyExpression = options.expression && empty(source) | ||
const place = location(source) | ||
@@ -112,2 +151,4 @@ if (isEmptyExpression && !options.allowEmpty) { | ||
error.message = String(error.message).replace(/ \(\d+:\d+\)$/, '') | ||
// Always defined in our unist points that come from micromark. | ||
assert(point.offset, 'expected `offset`') | ||
error.pos = point.offset | ||
@@ -142,7 +183,10 @@ error.loc = {line: point.line, column: point.column - 1} | ||
const point = parseOffsetToUnistPoint(estree.end) | ||
exception = new Error('Unexpected content after expression') | ||
// @ts-expect-error: acorn exception. | ||
exception.pos = point.offset | ||
// @ts-expect-error: acorn exception. | ||
exception.loc = {line: point.line, column: point.column - 1} | ||
const error = /** @type {AcornError} */ ( | ||
new Error('Unexpected content after expression') | ||
) | ||
// Always defined in our unist points that come from micromark. | ||
assert(point.offset, 'expected `offset`') | ||
error.pos = point.offset | ||
error.loc = {line: point.line, column: point.column - 1} | ||
exception = error | ||
estree = undefined | ||
@@ -199,2 +243,10 @@ } | ||
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) | ||
@@ -230,2 +282,5 @@ | ||
const pointEnd = parseOffsetToUnistPoint(nodeOrToken.end) | ||
// Always defined in our unist points that come from micromark. | ||
assert(pointStart.offset, 'expected `offset`') | ||
assert(pointEnd.offset, 'expected `offset`') | ||
nodeOrToken.start = pointStart.offset | ||
@@ -253,3 +308,3 @@ nodeOrToken.end = pointEnd.offset | ||
* @param {number} acornOffset | ||
* @returns {Point} | ||
* @returns {UnistPoint} | ||
*/ | ||
@@ -265,25 +320,17 @@ function parseOffsetToUnistPoint(acornOffset) { | ||
const pointInSource = place.toPoint(sourceOffset) | ||
assert( | ||
typeof startLine === 'number', | ||
'expected `startLine` to be found or given ' | ||
) | ||
const line = startLine + (pointInSource.line - 1) | ||
assert(line in lines, 'expected line to be defined') | ||
const column = lines[line].column + (pointInSource.column - 1) | ||
const offset = lines[line].offset + (pointInSource.column - 1) | ||
return /** @type {Point} */ ({line, column, offset}) | ||
} | ||
let point = relativeToPoint(collection.stops, sourceOffset) | ||
/** @param {Point} point */ | ||
function setPoint(point) { | ||
// Not passed by `micromark-extension-mdxjs-esm` | ||
/* c8 ignore next 3 */ | ||
if (!startLine || point.line < startLine) { | ||
startLine = point.line | ||
if (!point) { | ||
assert( | ||
options.start, | ||
'empty expressions are need `options.start` being passed' | ||
) | ||
point = { | ||
line: options.start.line, | ||
column: options.start.column, | ||
offset: options.start.offset | ||
} | ||
} | ||
if (!(point.line in lines) || lines[point.line].offset > point.offset) { | ||
lines[point.line] = point | ||
} | ||
return point | ||
} | ||
@@ -307,1 +354,134 @@ } | ||
} | ||
// Port from <https://github.com/wooorm/markdown-rs/blob/e692ab0/src/util/mdx_collect.rs#L15>. | ||
/** | ||
* @param {Array<Event>} events | ||
* @param {Array<string>} names | ||
* @returns {Collection} | ||
*/ | ||
function collect(events, names) { | ||
/** @type {Collection} */ | ||
const result = {value: '', stops: []} | ||
let index = -1 | ||
while (++index < events.length) { | ||
const event = events[index] | ||
// Assume void. | ||
if (event[0] === 'enter' && names.includes(event[1].type)) { | ||
const chunks = event[2].sliceStream(event[1]) | ||
// Drop virtual spaces. | ||
while (chunks.length > 0 && chunks[0] === codes.virtualSpace) { | ||
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 codes.carriageReturn: { | ||
value = values.cr | ||
break | ||
} | ||
case codes.lineFeed: { | ||
value = values.lf | ||
break | ||
} | ||
case codes.carriageReturnLineFeed: { | ||
value = values.cr + values.lf | ||
break | ||
} | ||
case codes.horizontalTab: { | ||
value = values.ht | ||
break | ||
} | ||
/* c8 ignore next 6 */ | ||
case codes.virtualSpace: { | ||
if (atTab) continue | ||
value = values.space | ||
break | ||
} | ||
default: { | ||
assert(typeof chunk === 'number', 'expected number') | ||
// Currently only replacement character. | ||
// eslint-disable-next-line unicorn/prefer-code-point | ||
value = String.fromCharCode(chunk) | ||
} | ||
} | ||
atTab = chunk === codes.horizontalTab | ||
result.push(value) | ||
} | ||
return result.join('') | ||
} |
129
index.d.ts
@@ -5,38 +5,101 @@ /** | ||
* @param {Array<Event>} events | ||
* Events. | ||
* @param {Options} options | ||
* @returns {{estree: Program | undefined, error: Error | undefined, swallow: boolean}} | ||
* Configuration. | ||
* @returns {Result} | ||
* Result. | ||
*/ | ||
export function eventsToAcorn(events: Array<import("micromark-util-types").Event>, options: Options): { | ||
estree: Program | undefined; | ||
error: Error | undefined; | ||
swallow: boolean; | ||
}; | ||
export type Event = import('micromark-util-types').Event; | ||
export type Point = import('micromark-util-types').Point; | ||
export type AcornOptions = import('acorn').Options; | ||
export type Comment = import('acorn').Comment; | ||
export type Token = import('acorn').Token; | ||
export type AcornNode = import('acorn').Node; | ||
export type Program = import('estree').Program; | ||
export type EstreeNode = import('estree').Node; | ||
export function eventsToAcorn( | ||
events: Array<import('micromark-util-types').Event>, | ||
options: Options | ||
): Result | ||
export type Comment = import('acorn').Comment | ||
export type AcornNode = import('acorn').Node | ||
export type AcornOptions = import('acorn').Options | ||
export type Token = import('acorn').Token | ||
export type EstreeNode = import('estree').Node | ||
export type Program = import('estree').Program | ||
export type Chunk = import('micromark-util-types').Chunk | ||
export type Event = import('micromark-util-types').Event | ||
export type MicromarkPoint = import('micromark-util-types').Point | ||
export type UnistPoint = import('unist').Point | ||
/** | ||
* Acorn-like interface. | ||
*/ | ||
export type Acorn = { | ||
parse: typeof import("acorn").parse; | ||
parseExpressionAt: typeof import("acorn").parseExpressionAt; | ||
}; | ||
export type AcornError = Error & { | ||
raisedAt: number; | ||
pos: number; | ||
loc: { | ||
line: number; | ||
column: number; | ||
}; | ||
}; | ||
/** | ||
* Parse a program. | ||
*/ | ||
parse: typeof import('acorn').parse | ||
/** | ||
* Parse an expression. | ||
*/ | ||
parseExpressionAt: typeof import('acorn').parseExpressionAt | ||
} | ||
export type AcornLoc = { | ||
line: number | ||
column: number | ||
} | ||
export type AcornErrorFields = { | ||
raisedAt: number | ||
pos: number | ||
loc: AcornLoc | ||
} | ||
export type AcornError = Error & AcornErrorFields | ||
/** | ||
* Configuration. | ||
*/ | ||
export type Options = { | ||
acorn: Acorn; | ||
acornOptions?: AcornOptions | null | undefined; | ||
start?: Point | null | undefined; | ||
prefix?: string | null | undefined; | ||
suffix?: string | null | undefined; | ||
expression?: boolean | null | undefined; | ||
allowEmpty?: boolean | null | undefined; | ||
}; | ||
/** | ||
* Typically `acorn`, object with `parse` and `parseExpressionAt` fields. | ||
*/ | ||
acorn: Acorn | ||
/** | ||
* Configuration for `acorn`. | ||
*/ | ||
acornOptions?: AcornOptions | null | undefined | ||
/** | ||
* Place where events start. | ||
*/ | ||
start?: MicromarkPoint | null | undefined | ||
/** | ||
* Text to place before events. | ||
*/ | ||
prefix?: string | null | undefined | ||
/** | ||
* Text to place after events. | ||
*/ | ||
suffix?: string | null | undefined | ||
/** | ||
* Whether this is a program or expression. | ||
*/ | ||
expression?: boolean | null | undefined | ||
/** | ||
* Whether an empty expression is allowed (programs are always allowed to | ||
* be empty). | ||
*/ | ||
allowEmpty?: boolean | null | undefined | ||
} | ||
/** | ||
* Result. | ||
*/ | ||
export type Result = { | ||
/** | ||
* Program. | ||
*/ | ||
estree: Program | undefined | ||
/** | ||
* Error if unparseable | ||
*/ | ||
error: AcornError | undefined | ||
/** | ||
* Whether the error, if there is one, can be swallowed and more JavaScript | ||
* could be valid. | ||
*/ | ||
swallow: boolean | ||
} | ||
export type Stop = [number, MicromarkPoint] | ||
export type Collection = { | ||
value: string | ||
stops: Array<Stop> | ||
} |
440
index.js
/** | ||
* @typedef {import('micromark-util-types').Event} Event | ||
* @typedef {import('micromark-util-types').Point} Point | ||
* @typedef {import('acorn').Comment} Comment | ||
* @typedef {import('acorn').Node} AcornNode | ||
* @typedef {import('acorn').Options} AcornOptions | ||
* @typedef {import('acorn').Comment} Comment | ||
* @typedef {import('acorn').Token} Token | ||
* @typedef {import('acorn').Node} AcornNode | ||
* @typedef {import('estree').Node} EstreeNode | ||
* @typedef {import('estree').Program} Program | ||
* @typedef {import('estree').Node} EstreeNode | ||
* @typedef {import('micromark-util-types').Chunk} Chunk | ||
* @typedef {import('micromark-util-types').Event} Event | ||
* @typedef {import('micromark-util-types').Point} MicromarkPoint | ||
* @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 {{parse: import('acorn').parse, parseExpressionAt: import('acorn').parseExpressionAt}} Acorn | ||
* @typedef {Error & {raisedAt: number, pos: number, loc: {line: number, column: number}}} AcornError | ||
* @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. | ||
* @property {AcornOptions | null | undefined} [acornOptions] | ||
* @property {Point | null | undefined} [start] | ||
* Configuration for `acorn`. | ||
* @property {MicromarkPoint | null | undefined} [start] | ||
* Place where events start. | ||
* @property {string | null | undefined} [prefix=''] | ||
* Text to place before events. | ||
* @property {string | null | undefined} [suffix=''] | ||
* Text to place after events. | ||
* @property {boolean | null | undefined} [expression=false] | ||
* Whether this is a program or expression. | ||
* @property {boolean | null | undefined} [allowEmpty=false] | ||
* Whether an empty expression is allowed (programs are always allowed to | ||
* be empty). | ||
* | ||
* @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'; | ||
import { location } from 'vfile-location'; | ||
import {visit} from 'estree-util-visit' | ||
import {VFileMessage} from 'vfile-message' | ||
@@ -32,28 +75,24 @@ /** | ||
* @param {Array<Event>} events | ||
* Events. | ||
* @param {Options} options | ||
* @returns {{estree: Program | undefined, error: Error | undefined, swallow: boolean}} | ||
* Configuration. | ||
* @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); | ||
const prefix = options.prefix || '' | ||
const suffix = options.suffix || '' | ||
const acornOptions = Object.assign({}, options.acornOptions) | ||
/** @type {Array<Comment>} */ | ||
const comments = []; | ||
const comments = [] | ||
/** @type {Array<Token>} */ | ||
const tokens = []; | ||
const onComment = acornOptions.onComment; | ||
const onToken = acornOptions.onToken; | ||
/** @type {Array<string>} */ | ||
const chunks = []; | ||
/** @type {Record<string, Point>} */ | ||
const lines = {}; | ||
let index = -1; | ||
let swallow = false; | ||
const tokens = [] | ||
const onComment = acornOptions.onComment | ||
const onToken = acornOptions.onToken | ||
let swallow = false | ||
/** @type {AcornNode | undefined} */ | ||
let estree; | ||
/** @type {Error | undefined} */ | ||
let exception; | ||
/** @type {number} */ | ||
let startLine; | ||
let estree | ||
/** @type {AcornError | undefined} */ | ||
let exception | ||
/** @type {AcornOptions} */ | ||
@@ -63,45 +102,54 @@ const acornConfig = Object.assign({}, acornOptions, { | ||
preserveParens: true | ||
}); | ||
}) | ||
if (onToken) { | ||
acornConfig.onToken = tokens; | ||
acornConfig.onToken = tokens | ||
} | ||
// We use `events` to detect everything, however, it could be empty. | ||
// In that case, we need `options.start` to make sense of positional info. | ||
if (options.start) { | ||
startLine = options.start.line; | ||
lines[startLine] = options.start; | ||
} | ||
while (++index < events.length) { | ||
const [kind, token, context] = events[index]; | ||
// Assume only void events (and `enter` followed immediately by an `exit`). | ||
if (kind === 'exit') { | ||
chunks.push(context.sliceSerialize(token)); | ||
setPoint(token.start); | ||
setPoint(token.end); | ||
} | ||
} | ||
const source = chunks.join(''); | ||
const value = prefix + source + suffix; | ||
const isEmptyExpression = options.expression && empty(source); | ||
const place = location(source); | ||
const collection = collect(events, [ | ||
'lineEnding', | ||
// To do: these should be passed by users in parameters. | ||
'expressionChunk', | ||
// From tests. | ||
'mdxFlowExpressionChunk', | ||
// Flow chunk. | ||
'mdxTextExpressionChunk', | ||
// Text chunk. | ||
// JSX: | ||
'mdxJsxTextTagExpressionAttributeValue', | ||
'mdxJsxTextTagAttributeValueExpressionValue', | ||
'mdxJsxFlowTagExpressionAttributeValue', | ||
'mdxJsxFlowTagAttributeValueExpressionValue', | ||
// ESM: | ||
'mdxjsEsmData' | ||
]) | ||
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', parseOffsetToUnistPoint(0), 'micromark-extension-mdx-expression:unexpected-empty-expression'); | ||
throw new VFileMessage( | ||
'Unexpected empty expression', | ||
parseOffsetToUnistPoint(0), | ||
'micromark-extension-mdx-expression:unexpected-empty-expression' | ||
) | ||
} | ||
try { | ||
estree = options.expression && !isEmptyExpression ? options.acorn.parseExpressionAt(value, 0, acornConfig) : options.acorn.parse(value, acornConfig); | ||
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+\)$/, ''); | ||
error.pos = point.offset; | ||
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'; | ||
} | ||
exception = error | ||
swallow = | ||
error.raisedAt >= prefix.length + source.length || | ||
// Broken comments are raised at their start, not their end. | ||
error.message === 'Unterminated comment' | ||
} | ||
@@ -115,22 +163,27 @@ if (estree && options.expression && !isEmptyExpression) { | ||
// @ts-expect-error: It’s good. | ||
body: [{ | ||
type: 'ExpressionStatement', | ||
expression: estree, | ||
start: 0, | ||
end: prefix.length + source.length | ||
}], | ||
body: [ | ||
{ | ||
type: 'ExpressionStatement', | ||
expression: estree, | ||
start: 0, | ||
end: prefix.length + source.length | ||
} | ||
], | ||
sourceType: 'module', | ||
comments: [] | ||
}; | ||
} | ||
} else { | ||
const point = parseOffsetToUnistPoint(estree.end); | ||
exception = new Error('Unexpected content after expression'); | ||
// @ts-expect-error: acorn exception. | ||
exception.pos = point.offset; | ||
// @ts-expect-error: acorn exception. | ||
exception.loc = { | ||
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 | ||
}; | ||
estree = undefined; | ||
} | ||
exception = error | ||
estree = undefined | ||
} | ||
@@ -140,10 +193,11 @@ } | ||
// @ts-expect-error: acorn *does* allow comments | ||
estree.comments = comments; | ||
estree.comments = comments | ||
// @ts-expect-error: acorn looks enough like estree. | ||
visit(estree, (esnode, field, index, parents) => { | ||
let context = /** @type {AcornNode | Array<AcornNode>} */ | ||
parents[parents.length - 1]; | ||
let context = | ||
/** @type {AcornNode | Array<AcornNode>} */ | ||
parents[parents.length - 1] | ||
/** @type {string | number | null} */ | ||
let prop = field; | ||
let prop = field | ||
@@ -156,24 +210,38 @@ // Remove non-standard `ParenthesizedExpression`. | ||
// @ts-expect-error: indexable. | ||
context = context[prop]; | ||
prop = index; | ||
context = context[prop] | ||
prop = index | ||
} | ||
// @ts-expect-error: indexable. | ||
context[prop] = esnode.expression; | ||
context[prop] = esnode.expression | ||
} | ||
fixPosition(esnode); | ||
}); | ||
fixPosition(esnode) | ||
}) | ||
// Comment positions are fixed by `visit` because they’re in the tree. | ||
if (Array.isArray(onComment)) { | ||
onComment.push(...comments); | ||
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); | ||
onComment( | ||
comment.type === 'Block', | ||
comment.value, | ||
comment.start, | ||
comment.end, | ||
comment.loc.start, | ||
comment.loc.end | ||
) | ||
} | ||
} | ||
for (const token of tokens) { | ||
fixPosition(token); | ||
// 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); | ||
onToken.push(token) | ||
} else { | ||
@@ -183,3 +251,3 @@ // `tokens` are not added if `onToken` is not defined, so it must be a | ||
onToken(token); | ||
onToken(token) | ||
} | ||
@@ -194,3 +262,3 @@ } | ||
swallow | ||
}; | ||
} | ||
@@ -204,6 +272,8 @@ /** | ||
function fixPosition(nodeOrToken) { | ||
const pointStart = parseOffsetToUnistPoint(nodeOrToken.start); | ||
const pointEnd = parseOffsetToUnistPoint(nodeOrToken.end); | ||
nodeOrToken.start = pointStart.offset; | ||
nodeOrToken.end = pointEnd.offset; | ||
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 = { | ||
@@ -220,4 +290,4 @@ start: { | ||
} | ||
}; | ||
nodeOrToken.range = [nodeOrToken.start, nodeOrToken.end]; | ||
} | ||
nodeOrToken.range = [nodeOrToken.start, nodeOrToken.end] | ||
} | ||
@@ -230,33 +300,20 @@ | ||
* @param {number} acornOffset | ||
* @returns {Point} | ||
* @returns {UnistPoint} | ||
*/ | ||
function parseOffsetToUnistPoint(acornOffset) { | ||
let sourceOffset = acornOffset - prefix.length; | ||
let sourceOffset = acornOffset - prefix.length | ||
if (sourceOffset < 0) { | ||
sourceOffset = 0; | ||
sourceOffset = 0 | ||
} else if (sourceOffset > source.length) { | ||
sourceOffset = source.length; | ||
sourceOffset = source.length | ||
} | ||
const pointInSource = place.toPoint(sourceOffset); | ||
const line = startLine + (pointInSource.line - 1); | ||
const column = lines[line].column + (pointInSource.column - 1); | ||
const offset = lines[line].offset + (pointInSource.column - 1); | ||
return (/** @type {Point} */{ | ||
line, | ||
column, | ||
offset | ||
let point = relativeToPoint(collection.stops, sourceOffset) | ||
if (!point) { | ||
point = { | ||
line: options.start.line, | ||
column: options.start.column, | ||
offset: options.start.offset | ||
} | ||
); | ||
} | ||
/** @param {Point} point */ | ||
function setPoint(point) { | ||
// Not passed by `micromark-extension-mdxjs-esm` | ||
/* c8 ignore next 3 */ | ||
if (!startLine || point.line < startLine) { | ||
startLine = point.line; | ||
} | ||
if (!(point.line in lines) || lines[point.line].offset > point.offset) { | ||
lines[point.line] = point; | ||
} | ||
return point | ||
} | ||
@@ -270,9 +327,128 @@ } | ||
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, '')); | ||
} | ||
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<string>} names | ||
* @returns {Collection} | ||
*/ | ||
function collect(events, names) { | ||
/** @type {Collection} */ | ||
const result = { | ||
value: '', | ||
stops: [] | ||
} | ||
let index = -1 | ||
while (++index < events.length) { | ||
const event = events[index] | ||
// Assume void. | ||
if (event[0] === 'enter' && names.includes(event[1].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('') | ||
} |
{ | ||
"name": "micromark-util-events-to-acorn", | ||
"version": "1.2.1", | ||
"version": "1.2.2", | ||
"description": "micromark utility to try and parse events w/ acorn", | ||
@@ -44,10 +44,11 @@ "license": "MIT", | ||
"@types/estree": "^1.0.0", | ||
"@types/unist": "^2.0.0", | ||
"estree-util-visit": "^1.0.0", | ||
"micromark-util-symbol": "^1.0.0", | ||
"micromark-util-types": "^1.0.0", | ||
"uvu": "^0.5.0", | ||
"vfile-location": "^4.0.0", | ||
"vfile-message": "^3.0.0" | ||
}, | ||
"scripts": { | ||
"build": "tsc --build --clean && tsc && type-coverage && micromark-build" | ||
"build": "micromark-build" | ||
}, | ||
@@ -54,0 +55,0 @@ "xo": false, |
122
readme.md
@@ -11,3 +11,3 @@ # micromark-util-events-to-acorn | ||
micromark utility to try and parse events w/ acorn. | ||
[micromark][] utility to try and parse events with acorn. | ||
@@ -20,3 +20,6 @@ ## Contents | ||
* [`eventsToAcorn(events, options)`](#eventstoacornevents-options) | ||
* [`Options`](#options) | ||
* [`Result`](#result) | ||
* [Types](#types) | ||
* [Compatibility](#compatibility) | ||
* [Security](#security) | ||
@@ -29,3 +32,3 @@ * [Contribute](#contribute) | ||
This package is [ESM only][esm]. | ||
In Node.js (version 12.20+, 14.14+, 16.0+, or 18.0+), install with [npm][]: | ||
In Node.js (version 16+), install with [npm][]: | ||
@@ -39,3 +42,3 @@ ```sh | ||
```js | ||
import {mdxExpression} from 'https://esm.sh/micromark-util-events-to-acorn@1' | ||
import {eventsToAcorn} from 'https://esm.sh/micromark-util-events-to-acorn@1' | ||
``` | ||
@@ -47,3 +50,3 @@ | ||
<script type="module"> | ||
import {mdxExpression} from 'https://esm.sh/micromark-util-events-to-acorn@1?bundle' | ||
import {eventsToAcorn} from 'https://esm.sh/micromark-util-events-to-acorn@1?bundle' | ||
</script> | ||
@@ -64,22 +67,16 @@ ``` | ||
/** @type {State} */ | ||
atClosingBrace(code) { | ||
// … | ||
// Gnostic mode: parse w/ acorn. | ||
const result = eventsToAcorn( | ||
self.events.slice(eventStart), | ||
const result = eventsToAcorn(this.events.slice(eventStart), { | ||
acorn, | ||
acornOptions, | ||
{ | ||
start: startPosition, | ||
expression: true, | ||
allowEmpty, | ||
prefix: spread ? '({' : '', | ||
suffix: spread ? '})' : '' | ||
} | ||
) | ||
start: pointStart, | ||
expression: true, | ||
allowEmpty, | ||
prefix: spread ? '({' : '', | ||
suffix: spread ? '})' : '' | ||
}) | ||
// … | ||
} | ||
@@ -92,6 +89,6 @@ // … | ||
This module exports the identifier `eventsToAcorn`. | ||
This module exports the identifier [`eventsToAcorn`][api-events-to-acorn]. | ||
There is no default export. | ||
The export map supports the endorsed [`development` condition][condition]. | ||
The export map supports the [`development` condition][development]. | ||
Run `node --conditions development module.js` to get instrumented dev code. | ||
@@ -106,24 +103,40 @@ Without this condition, production code is loaded. | ||
— events | ||
* `options.acorn` (`Acorn`, required) | ||
— object with `acorn.parse` and | ||
`acorn.parseExpressionAt` | ||
* `options.acornOptions` ([`AcornOptions`][acorn-options]) | ||
— configuration for acorn | ||
* `options.start` (`Point`, optional) | ||
* `options` ([`Options`][api-options]) | ||
— configuration | ||
###### Returns | ||
Result ([`Result`][api-result]). | ||
### `Options` | ||
Configuration (TypeScript type). | ||
###### Fields | ||
* `acorn` ([`Acorn`][acorn], required) | ||
— typically `acorn`, object with `parse` and `parseExpressionAt` fields | ||
* `acornOptions` ([`AcornOptions`][acorn-options], optional) | ||
— configuration for `acorn` | ||
* `start` (`Point`, optional) | ||
— place where events start | ||
* `options.prefix` (`string`, default: `''`) | ||
* `prefix` (`string`, default: `''`) | ||
— text to place before events | ||
* `options.suffix` (`string`, default: `''`) | ||
* `suffix` (`string`, default: `''`) | ||
— text to place after events | ||
* `options.expression` (`boolean`, default: `false`) | ||
* `expression` (`boolean`, default: `false`) | ||
— whether this is a program or expression | ||
* `options.allowEmpty` (`boolean`, default: `false`) | ||
— whether an empty expression is allowed (programs are always allowed to | ||
be empty) | ||
* `allowEmpty` (`boolean`, default: `false`) | ||
— whether an empty expression is allowed (programs are always allowed to be | ||
empty) | ||
###### Returns | ||
### `Result` | ||
* `estree` ([`Program?`][program]) | ||
— estree node | ||
* `error` (`Error?`) | ||
Result (TypeScript type). | ||
###### Fields | ||
* `estree` ([`Program`][program] or `undefined`) | ||
— Program | ||
* `error` (`Error` or `undefined`) | ||
— error if unparseable | ||
@@ -137,9 +150,18 @@ * `swallow` (`boolean`) | ||
This package is fully typed with [TypeScript][]. | ||
It exports the additional types `Acorn`, `AcornOptions`, `Options`, `Point`, | ||
and `Program`. | ||
It exports the additional types [`Acorn`][acorn], | ||
[`AcornOptions`][acorn-options], [`Options`][api-options], and | ||
[`Result`][api-result]. | ||
## Compatibility | ||
Projects maintained by the unified collective are compatible with all maintained | ||
versions of Node.js. | ||
As of now, that is Node.js 16+. | ||
Our projects sometimes work with older versions, but this is not guaranteed. | ||
These extensions work with `micromark` version 3+. | ||
## Security | ||
See [`security.md`][securitymd] in [`micromark/.github`][health] for how to | ||
submit a security report. | ||
This package is safe. | ||
@@ -198,10 +220,8 @@ ## Contribute | ||
[securitymd]: https://github.com/micromark/.github/blob/HEAD/security.md | ||
[contributing]: https://github.com/micromark/.github/blob/main/contributing.md | ||
[contributing]: https://github.com/micromark/.github/blob/HEAD/contributing.md | ||
[support]: https://github.com/micromark/.github/blob/main/support.md | ||
[support]: https://github.com/micromark/.github/blob/HEAD/support.md | ||
[coc]: https://github.com/micromark/.github/blob/main/code-of-conduct.md | ||
[coc]: https://github.com/micromark/.github/blob/HEAD/code-of-conduct.md | ||
[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c | ||
@@ -211,6 +231,16 @@ | ||
[condition]: https://nodejs.org/api/packages.html#packages_resolving_user_conditions | ||
[development]: https://nodejs.org/api/packages.html#packages_resolving_user_conditions | ||
[acorn]: https://github.com/acornjs/acorn | ||
[acorn-options]: https://github.com/acornjs/acorn/blob/96c721dbf89d0ccc3a8c7f39e69ef2a6a3c04dfa/acorn/dist/acorn.d.ts#L16 | ||
[micromark]: https://github.com/micromark/micromark | ||
[program]: https://github.com/estree/estree/blob/master/es2015.md#programs | ||
[acorn-options]: https://github.com/acornjs/acorn/tree/master/acorn#interface | ||
[api-events-to-acorn]: #eventstoacornevents-options | ||
[api-options]: #options | ||
[api-result]: #result |
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
38341
1054
236
8
1
+ Added@types/unist@^2.0.0
+ Addedmicromark-util-symbol@^1.0.0
+ Addedmicromark-util-symbol@1.1.0(transitive)
- Removedvfile-location@^4.0.0
- Removedis-buffer@2.0.5(transitive)
- Removedvfile@5.3.7(transitive)
- Removedvfile-location@4.1.0(transitive)