kjelsrud.dev/node_modules/micromark-extension-mdxjs-esm/dev/lib/syntax.js

305 lines
7.9 KiB
JavaScript
Raw Normal View History

2023-07-19 21:31:30 +02:00
/**
* @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)
}
}