kjelsrud.dev/node_modules/micromark-extension-mdxjs-esm/dev/lib/syntax.js
2023-07-19 21:31:30 +02:00

304 lines
7.9 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('micromark-util-events-to-acorn').Acorn} Acorn
* @typedef {import('micromark-util-events-to-acorn').AcornOptions} AcornOptions
* @typedef {import('micromark-util-types').Extension} Extension
* @typedef {import('micromark-util-types').State} State
* @typedef {import('micromark-util-types').TokenizeContext} TokenizeContext
* @typedef {import('micromark-util-types').Tokenizer} Tokenizer
*/
/**
* @typedef Options
* Configuration (required).
* @property {Acorn} acorn
* Acorn parser to use (required).
* @property {AcornOptions | null | undefined} [acornOptions]
* Configuration for acorn (default: `{ecmaVersion: 2020, locations: true,
* sourceType: 'module'}`).
* all fields except `locations` can be set
* @property {boolean | null | undefined} [addResult=false]
* Whether to add `estree` fields to tokens with results from acorn.
*/
import {ok as assert} from 'uvu/assert'
import {blankLine} from 'micromark-core-commonmark'
import {asciiAlpha, markdownLineEnding} from 'micromark-util-character'
import {eventsToAcorn} from 'micromark-util-events-to-acorn'
import {codes} from 'micromark-util-symbol/codes.js'
import {types} from 'micromark-util-symbol/types.js'
import {positionFromEstree} from 'unist-util-position-from-estree'
import {VFileMessage} from 'vfile-message'
const blankLineBefore = {tokenize: tokenizeNextBlank, partial: true}
const allowedAcornTypes = new Set([
'ExportAllDeclaration',
'ExportDefaultDeclaration',
'ExportNamedDeclaration',
'ImportDeclaration'
])
/**
* Create an extension for `micromark` to enable MDX ESM syntax.
*
* @param {Options} options
* Configuration (required).
* @returns {Extension}
* Extension for `micromark` that can be passed in `extensions` to enable MDX
* ESM syntax.
*/
export function mdxjsEsm(options) {
const exportImportConstruct = {tokenize: tokenizeExportImport, concrete: true}
if (!options || !options.acorn || !options.acorn.parse) {
throw new Error('Expected an `acorn` instance passed in as `options.acorn`')
}
const acorn = options.acorn
const acornOptions = Object.assign(
{ecmaVersion: 2020, sourceType: 'module'},
options.acornOptions
)
return {
flow: {
[codes.lowercaseE]: exportImportConstruct,
[codes.lowercaseI]: exportImportConstruct
}
}
/**
* @this {TokenizeContext}
* @type {Tokenizer}
*/
function tokenizeExportImport(effects, ok, nok) {
const self = this
/** @type {Array<string>} */
const definedModuleSpecifiers =
self.parser.definedModuleSpecifiers ||
(self.parser.definedModuleSpecifiers = [])
const eventStart = this.events.length + 1 // Add the main `mdxjsEsm` token
let buffer = ''
return self.interrupt ? nok : start
/**
* Start of MDX ESM.
*
* ```markdown
* > | import a from 'b'
* ^
* ```
*
* @type {State}
*/
function start(code) {
assert(
code === codes.lowercaseE || code === codes.lowercaseI,
'expected `e` or `i`'
)
// Only at the start of a line, not at whitespace or in a container.
if (self.now().column > 1) return nok(code)
effects.enter('mdxjsEsm')
effects.enter('mdxjsEsmData')
effects.consume(code)
// eslint-disable-next-line unicorn/prefer-code-point
buffer += String.fromCharCode(code)
return word
}
/**
* In keyword.
*
* ```markdown
* > | import a from 'b'
* ^^^^^^
* ```
*
* @type {State}
*/
function word(code) {
if (asciiAlpha(code)) {
effects.consume(code)
// @ts-expect-error: definitely a number.
// eslint-disable-next-line unicorn/prefer-code-point
buffer += String.fromCharCode(code)
return word
}
if (
(buffer === 'import' || buffer === 'export') &&
code === codes.space
) {
effects.consume(code)
return inside
}
return nok(code)
}
/**
* In data.
*
* ```markdown
* > | import a from 'b'
* ^
* ```
*
* @type {State}
*/
function inside(code) {
if (code === codes.eof || markdownLineEnding(code)) {
effects.exit('mdxjsEsmData')
return lineStart(code)
}
effects.consume(code)
return inside
}
/**
* At line ending.
*
* ```markdown
* > | import a from 'b'
* ^
* | export {a}
* ```
*
* @type {State}
*/
function lineStart(code) {
if (code === codes.eof) {
return atEnd(code)
}
if (markdownLineEnding(code)) {
return effects.check(blankLineBefore, atEnd, continuationStart)(code)
}
effects.enter('mdxjsEsmData')
return inside(code)
}
/**
* At line ending that continues.
*
* ```markdown
* > | import a from 'b'
* ^
* | export {a}
* ```
*
* @type {State}
*/
function continuationStart(code) {
assert(markdownLineEnding(code))
effects.enter(types.lineEnding)
effects.consume(code)
effects.exit(types.lineEnding)
return lineStart
}
/**
* At end of line (blank or eof).
*
* ```markdown
* > | import a from 'b'
* ^
* ```
*
* @type {State}
*/
function atEnd(code) {
const result = eventsToAcorn(self.events.slice(eventStart), {
acorn,
acornOptions,
prefix:
definedModuleSpecifiers.length > 0
? 'var ' + definedModuleSpecifiers.join(',') + '\n'
: ''
})
if (result.error) {
// Theres an error, which could be solved with more content, and there
// is more content.
if (code !== codes.eof && result.swallow) {
return continuationStart(code)
}
throw new VFileMessage(
'Could not parse import/exports with acorn: ' + String(result.error),
{
line: result.error.loc.line,
column: result.error.loc.column + 1,
offset: result.error.pos
},
'micromark-extension-mdxjs-esm:acorn'
)
}
assert(result.estree, 'expected `estree` to be defined')
// Remove the `VariableDeclaration`.
if (definedModuleSpecifiers.length > 0) {
const declaration = result.estree.body.shift()
assert(declaration)
assert(declaration.type === 'VariableDeclaration')
}
let index = -1
while (++index < result.estree.body.length) {
const node = result.estree.body[index]
if (!allowedAcornTypes.has(node.type)) {
throw new VFileMessage(
'Unexpected `' +
node.type +
'` in code: only import/exports are supported',
positionFromEstree(node),
'micromark-extension-mdxjs-esm:non-esm'
)
}
// Otherwise, when were not interrupting (hacky, because `interrupt` is
// used to parse containers and “sniff” if this is ESM), collect all the
// local values that are imported.
if (node.type === 'ImportDeclaration' && !self.interrupt) {
let index = -1
while (++index < node.specifiers.length) {
const specifier = node.specifiers[index]
definedModuleSpecifiers.push(specifier.local.name)
}
}
}
Object.assign(
effects.exit('mdxjsEsm'),
options.addResult ? {estree: result.estree} : undefined
)
return ok(code)
}
}
}
/** @type {Tokenizer} */
function tokenizeNextBlank(effects, ok, nok) {
return start
/**
* @type {State}
*/
function start(code) {
assert(markdownLineEnding(code))
effects.enter(types.lineEndingBlank)
effects.consume(code)
effects.exit(types.lineEndingBlank)
return effects.attempt(blankLine, ok, nok)
}
}