2022-12-20 22:08:15 +00:00
import { rehypeHeadingIds } from '@astrojs/markdown-remark' ;
2023-01-03 21:33:40 +00:00
import {
InvalidAstroDataError ,
safelyGetAstroData ,
} from '@astrojs/markdown-remark/dist/internal.js' ;
2022-09-26 22:23:47 +00:00
import { nodeTypes } from '@mdx-js/mdx' ;
import type { PluggableList } from '@mdx-js/mdx/lib/core.js' ;
import type { Options as MdxRollupPluginOptions } from '@mdx-js/rollup' ;
2023-01-03 21:31:19 +00:00
import type { AstroConfig } from 'astro' ;
2022-09-26 22:25:48 +00:00
import type { Literal , MemberExpression } from 'estree' ;
import { visit as estreeVisit } from 'estree-util-visit' ;
2022-09-26 22:23:47 +00:00
import { bold , yellow } from 'kleur/colors' ;
2022-12-22 15:40:34 +00:00
import type { Image } from 'mdast' ;
import { pathToFileURL } from 'node:url' ;
2022-09-26 22:23:47 +00:00
import rehypeRaw from 'rehype-raw' ;
import remarkGfm from 'remark-gfm' ;
2023-01-06 14:26:02 +00:00
import remarkSmartypants from 'remark-smartypants' ;
2022-12-22 15:40:34 +00:00
import { visit } from 'unist-util-visit' ;
2023-01-03 21:33:40 +00:00
import type { VFile } from 'vfile' ;
2022-09-26 22:25:48 +00:00
import { MdxOptions } from './index.js' ;
2022-12-20 22:08:15 +00:00
import { rehypeInjectHeadingsExport } from './rehype-collect-headings.js' ;
2022-11-09 13:32:13 +00:00
import rehypeMetaString from './rehype-meta-string.js' ;
2022-09-26 22:23:47 +00:00
import remarkPrism from './remark-prism.js' ;
import remarkShiki from './remark-shiki.js' ;
2022-12-22 15:40:34 +00:00
import { isRelativePath , jsToTreeNode } from './utils.js' ;
2022-09-26 22:23:47 +00:00
export function recmaInjectImportMetaEnvPlugin ( {
importMetaEnv ,
} : {
importMetaEnv : Record < string , any > ;
} ) {
return ( tree : any ) = > {
estreeVisit ( tree , ( node ) = > {
if ( node . type === 'MemberExpression' ) {
// attempt to get "import.meta.env" variable name
const envVarName = getImportMetaEnvVariableName ( node as MemberExpression ) ;
if ( typeof envVarName === 'string' ) {
// clear object keys to replace with envVarLiteral
for ( const key in node ) {
delete ( node as any ) [ key ] ;
}
const envVarLiteral : Literal = {
type : 'Literal' ,
value : importMetaEnv [ envVarName ] ,
raw : JSON.stringify ( importMetaEnv [ envVarName ] ) ,
} ;
Object . assign ( node , envVarLiteral ) ;
}
}
} ) ;
} ;
}
2023-01-03 21:31:19 +00:00
export function rehypeApplyFrontmatterExport() {
2022-09-26 22:23:47 +00:00
return function ( tree : any , vfile : VFile ) {
2023-01-03 21:31:19 +00:00
const astroData = safelyGetAstroData ( vfile . data ) ;
if ( astroData instanceof InvalidAstroDataError )
throw new Error (
// Copied from Astro core `errors-data`
// TODO: find way to import error data from core
'[MDX] A remark or rehype plugin attempted to inject invalid frontmatter. Ensure "astro.frontmatter" is set to a valid JSON object that is not `null` or `undefined`.'
) ;
const { frontmatter } = astroData ;
2022-09-26 22:23:47 +00:00
const exportNodes = [
2023-01-03 21:31:19 +00:00
jsToTreeNode ( ` export const frontmatter = ${ JSON . stringify ( frontmatter ) } ; ` ) ,
2022-09-26 22:23:47 +00:00
] ;
if ( frontmatter . layout ) {
// NOTE(bholmesdev) 08-22-2022
// Using an async layout import (i.e. `const Layout = (await import...)`)
// Preserves the dev server import cache when globbing a large set of MDX files
// Full explanation: 'https://github.com/withastro/astro/pull/4428'
exportNodes . unshift (
jsToTreeNode (
/** @see 'vite-plugin-markdown' for layout props reference */
` import { jsx as layoutJsx } from 'astro/jsx-runtime';
2022-12-16 19:19:53 +00:00
2022-09-26 22:23:47 +00:00
export default async function ( { children } ) {
const Layout = ( await import ( $ { JSON . stringify ( frontmatter . layout ) } ) ) . default ;
const { layout , . . . content } = frontmatter ;
content . file = file ;
content . url = url ;
return layoutJsx ( Layout , {
file ,
url ,
content ,
frontmatter : content ,
headings : getHeadings ( ) ,
'server:root' : true ,
children ,
} ) ;
} ; `
)
) ;
}
tree . children = exportNodes . concat ( tree . children ) ;
} ;
}
2022-12-22 15:38:03 +00:00
/ * *
* ` src/content/ ` does not support relative image paths .
* This plugin throws an error if any are found
* /
function toRemarkContentRelImageError ( { srcDir } : { srcDir : URL } ) {
const contentDir = new URL ( 'content/' , srcDir ) ;
return function remarkContentRelImageError() {
return ( tree : any , vfile : VFile ) = > {
const isContentFile = pathToFileURL ( vfile . path ) . href . startsWith ( contentDir . href ) ;
if ( ! isContentFile ) return ;
const relImagePaths = new Set < string > ( ) ;
visit ( tree , 'image' , function raiseError ( node : Image ) {
if ( isRelativePath ( node . url ) ) {
relImagePaths . add ( node . url ) ;
}
} ) ;
if ( relImagePaths . size === 0 ) return ;
const errorMessage =
` Relative image paths are not supported in the content/ directory. Place local images in the public/ directory and use absolute paths (see https://docs.astro.build/en/guides/images/#in-markdown-files): \ n ` +
[ . . . relImagePaths ] . map ( ( path ) = > JSON . stringify ( path ) ) . join ( ',\n' ) ;
throw new Error ( errorMessage ) ;
} ;
} ;
}
2022-09-26 22:23:47 +00:00
export async function getRemarkPlugins (
mdxOptions : MdxOptions ,
config : AstroConfig
) : Promise < MdxRollupPluginOptions [ ' remarkPlugins ' ] > {
2023-01-03 21:31:19 +00:00
let remarkPlugins : PluggableList = [ ] ;
2023-01-31 22:46:59 +00:00
2023-01-03 22:12:47 +00:00
if ( mdxOptions . gfm ) {
remarkPlugins . push ( remarkGfm ) ;
}
2023-01-06 14:26:02 +00:00
if ( mdxOptions . smartypants ) {
remarkPlugins . push ( remarkSmartypants ) ;
}
2022-09-26 22:23:47 +00:00
2023-01-03 22:12:47 +00:00
remarkPlugins = [ . . . remarkPlugins , . . . ignoreStringPlugins ( mdxOptions . remarkPlugins ) ] ;
2022-12-22 15:38:03 +00:00
2023-01-31 22:46:59 +00:00
// Apply syntax highlighters after user plugins to match `markdown/remark` behavior
if ( mdxOptions . syntaxHighlight === 'shiki' ) {
remarkPlugins . push ( [ await remarkShiki ( mdxOptions . shikiConfig ) ] ) ;
}
if ( mdxOptions . syntaxHighlight === 'prism' ) {
remarkPlugins . push ( remarkPrism ) ;
}
2022-12-22 15:38:03 +00:00
// Apply last in case user plugins resolve relative image paths
2023-01-11 17:51:31 +00:00
remarkPlugins . push ( toRemarkContentRelImageError ( config ) ) ;
2022-09-26 22:23:47 +00:00
return remarkPlugins ;
}
2023-01-03 22:12:47 +00:00
export function getRehypePlugins ( mdxOptions : MdxOptions ) : MdxRollupPluginOptions [ 'rehypePlugins' ] {
2022-09-26 22:23:47 +00:00
let rehypePlugins : PluggableList = [
2022-11-09 13:32:13 +00:00
// ensure `data.meta` is preserved in `properties.metastring` for rehype syntax highlighters
rehypeMetaString ,
2022-09-26 22:23:47 +00:00
// rehypeRaw allows custom syntax highlighters to work without added config
[ rehypeRaw , { passThrough : nodeTypes } ] as any ,
] ;
2022-12-20 22:08:15 +00:00
rehypePlugins = [
. . . rehypePlugins ,
2023-01-03 22:12:47 +00:00
. . . ignoreStringPlugins ( mdxOptions . rehypePlugins ) ,
2022-12-20 22:08:15 +00:00
// getHeadings() is guaranteed by TS, so this must be included.
// We run `rehypeHeadingIds` _last_ to respect any custom IDs set by user plugins.
rehypeHeadingIds ,
rehypeInjectHeadingsExport ,
2023-01-03 21:31:19 +00:00
// computed from `astro.data.frontmatter` in VFile data
rehypeApplyFrontmatterExport ,
2022-12-20 22:08:15 +00:00
] ;
2022-09-26 22:23:47 +00:00
return rehypePlugins ;
}
function ignoreStringPlugins ( plugins : any [ ] ) {
let validPlugins : PluggableList = [ ] ;
let hasInvalidPlugin = false ;
for ( const plugin of plugins ) {
if ( typeof plugin === 'string' ) {
console . warn ( yellow ( ` [MDX] ${ bold ( plugin ) } not applied. ` ) ) ;
hasInvalidPlugin = true ;
} else if ( Array . isArray ( plugin ) && typeof plugin [ 0 ] === 'string' ) {
console . warn ( yellow ( ` [MDX] ${ bold ( plugin [ 0 ] ) } not applied. ` ) ) ;
hasInvalidPlugin = true ;
} else {
validPlugins . push ( plugin ) ;
}
}
if ( hasInvalidPlugin ) {
console . warn (
` To inherit Markdown plugins in MDX, please use explicit imports in your config instead of "strings." See Markdown docs: https://docs.astro.build/en/guides/markdown-content/#markdown-plugins `
) ;
}
return validPlugins ;
}
/ * *
* Check if estree entry is "import.meta.env.VARIABLE"
* If it is , return the variable name ( i . e . "VARIABLE" )
* /
function getImportMetaEnvVariableName ( node : MemberExpression ) : string | Error {
try {
// check for ".[ANYTHING]"
if ( node . object . type !== 'MemberExpression' || node . property . type !== 'Identifier' )
return new Error ( ) ;
const nestedExpression = node . object ;
// check for ".env"
if ( nestedExpression . property . type !== 'Identifier' || nestedExpression . property . name !== 'env' )
return new Error ( ) ;
const envExpression = nestedExpression . object ;
// check for ".meta"
if (
envExpression . type !== 'MetaProperty' ||
envExpression . property . type !== 'Identifier' ||
envExpression . property . name !== 'meta'
)
return new Error ( ) ;
// check for "import"
if ( envExpression . meta . name !== 'import' ) return new Error ( ) ;
return node . property . name ;
} catch ( e ) {
if ( e instanceof Error ) {
return e ;
}
return new Error ( 'Unknown parsing error' ) ;
}
}