2021-04-14 04:24:52 -04:00
/ * *
* Copyright ( c ) Facebook , Inc . and its affiliates .
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree .
* /
2022-05-14 11:39:50 -04:00
import type { CSSProperties } from 'react' ;
2021-11-12 10:43:40 -05:00
import rangeParser from 'parse-numeric-range' ;
2023-10-06 13:15:14 -04:00
import type { PrismTheme , PrismThemeEntry } from 'prism-react-renderer' ;
2021-11-12 10:43:40 -05:00
2022-02-01 04:43:15 -05:00
const codeBlockTitleRegex = /title=(?<quote>["'])(?<title>.*?)\1/ ;
2022-05-04 06:31:13 -04:00
const metastringLinesRangeRegex = /\{(?<range>[\d,-]+)\}/ ;
2021-11-12 10:43:40 -05:00
// Supported types of highlight comments
2023-12-27 17:02:48 -05:00
const popularCommentPatterns = {
2022-03-23 09:39:19 -04:00
js : { start : '\\/\\/' , end : '' } ,
jsBlock : { start : '\\/\\*' , end : '\\*\\/' } ,
jsx : { start : '\\{\\s*\\/\\*' , end : '\\*\\/\\s*\\}' } ,
2022-03-28 21:23:03 -04:00
bash : { start : '#' , end : '' } ,
2022-03-23 09:39:19 -04:00
html : { start : '<!--' , end : '-->' } ,
2023-12-27 17:02:48 -05:00
} as const ;
const commentPatterns = {
. . . popularCommentPatterns , // shallow copy is sufficient
// minor comment styles
2023-04-14 06:01:28 -04:00
lua : { start : '--' , end : '' } ,
wasm : { start : '\\;\\;' , end : '' } ,
2023-05-30 09:28:28 -04:00
tex : { start : '%' , end : '' } ,
2023-12-27 17:02:48 -05:00
vb : { start : "['‘ ’ ]" , end : '' } ,
2024-01-12 15:13:24 -05:00
vbnet : { start : "(?:_\\s*)?['‘ ’ ]" , end : '' } , // Visual Studio 2019 or later
2023-12-27 17:02:48 -05:00
rem : { start : '[Rr][Ee][Mm]\\b' , end : '' } ,
f90 : { start : '!' , end : '' } , // Free format only
ml : { start : '\\(\\*' , end : '\\*\\)' } ,
cobol : { start : '\\*>' , end : '' } , // Free format only
} as const ;
2021-11-12 10:43:40 -05:00
2022-03-23 09:39:19 -04:00
type CommentType = keyof typeof commentPatterns ;
2023-12-27 17:02:48 -05:00
const popularCommentTypes = Object . keys (
popularCommentPatterns ,
) as CommentType [ ] ;
2022-03-23 09:39:19 -04:00
2022-05-04 06:31:13 -04:00
export type MagicCommentConfig = {
className : string ;
line? : string ;
block ? : { start : string ; end : string } ;
} ;
2021-11-12 10:43:40 -05:00
2022-05-04 06:31:13 -04:00
function getCommentPattern (
languages : CommentType [ ] ,
magicCommentDirectives : MagicCommentConfig [ ] ,
) {
2022-04-17 04:39:11 -04:00
// To be more reliable, the opening and closing comment must match
2021-11-12 10:43:40 -05:00
const commentPattern = languages
. map ( ( lang ) = > {
const { start , end } = commentPatterns [ lang ] ;
2022-05-04 06:31:13 -04:00
return ` (?: ${ start } \\ s*( ${ magicCommentDirectives
. flatMap ( ( d ) = > [ d . line , d . block ? . start , d . block ? . end ] . filter ( Boolean ) )
. join ( '|' ) } ) \ \ s * $ { end } ) ` ;
2021-11-12 10:43:40 -05:00
} )
. join ( '|' ) ;
2022-04-17 04:39:11 -04:00
// White space is allowed, but otherwise it should be on it's own line
2021-11-12 10:43:40 -05:00
return new RegExp ( ` ^ \\ s*(?: ${ commentPattern } ) \\ s* $ ` ) ;
2022-03-23 09:39:19 -04:00
}
2021-11-12 10:43:40 -05:00
2022-03-23 09:39:19 -04:00
/ * *
* Select comment styles based on language
* /
2022-05-04 06:31:13 -04:00
function getAllMagicCommentDirectiveStyles (
lang : string ,
magicCommentDirectives : MagicCommentConfig [ ] ,
) {
2021-11-12 10:43:40 -05:00
switch ( lang ) {
case 'js' :
case 'javascript' :
case 'ts' :
case 'typescript' :
2022-05-04 06:31:13 -04:00
return getCommentPattern ( [ 'js' , 'jsBlock' ] , magicCommentDirectives ) ;
2021-11-12 10:43:40 -05:00
case 'jsx' :
case 'tsx' :
2022-05-04 06:31:13 -04:00
return getCommentPattern (
[ 'js' , 'jsBlock' , 'jsx' ] ,
magicCommentDirectives ,
) ;
2021-11-12 10:43:40 -05:00
case 'html' :
2022-05-04 06:31:13 -04:00
return getCommentPattern (
[ 'js' , 'jsBlock' , 'html' ] ,
magicCommentDirectives ,
) ;
2021-11-12 10:43:40 -05:00
case 'python' :
case 'py' :
2022-03-28 21:23:03 -04:00
case 'bash' :
2022-05-04 06:31:13 -04:00
return getCommentPattern ( [ 'bash' ] , magicCommentDirectives ) ;
2021-11-12 10:43:40 -05:00
2022-03-27 21:57:04 -04:00
case 'markdown' :
case 'md' :
2022-03-28 21:23:03 -04:00
// Text uses HTML, front matter uses bash
2022-05-04 06:31:13 -04:00
return getCommentPattern ( [ 'html' , 'jsx' , 'bash' ] , magicCommentDirectives ) ;
2022-03-27 21:57:04 -04:00
2023-05-30 09:28:28 -04:00
case 'tex' :
case 'latex' :
case 'matlab' :
return getCommentPattern ( [ 'tex' ] , magicCommentDirectives ) ;
2023-04-14 06:01:28 -04:00
case 'lua' :
case 'haskell' :
case 'sql' :
return getCommentPattern ( [ 'lua' ] , magicCommentDirectives ) ;
case 'wasm' :
return getCommentPattern ( [ 'wasm' ] , magicCommentDirectives ) ;
2023-12-27 17:02:48 -05:00
case 'vb' :
case 'vba' :
case 'visual-basic' :
return getCommentPattern ( [ 'vb' , 'rem' ] , magicCommentDirectives ) ;
2024-01-12 15:13:24 -05:00
case 'vbnet' :
return getCommentPattern ( [ 'vbnet' , 'rem' ] , magicCommentDirectives ) ;
2023-12-27 17:02:48 -05:00
case 'batch' :
return getCommentPattern ( [ 'rem' ] , magicCommentDirectives ) ;
case 'basic' : // https://github.com/PrismJS/prism/blob/master/components/prism-basic.js#L3
return getCommentPattern ( [ 'rem' , 'f90' ] , magicCommentDirectives ) ;
case 'fsharp' :
return getCommentPattern ( [ 'js' , 'ml' ] , magicCommentDirectives ) ;
case 'ocaml' :
case 'sml' :
return getCommentPattern ( [ 'ml' ] , magicCommentDirectives ) ;
case 'fortran' :
return getCommentPattern ( [ 'f90' ] , magicCommentDirectives ) ;
case 'cobol' :
return getCommentPattern ( [ 'cobol' ] , magicCommentDirectives ) ;
2021-11-12 10:43:40 -05:00
default :
2023-12-27 17:02:48 -05:00
// All popular comment types
return getCommentPattern ( popularCommentTypes , magicCommentDirectives ) ;
2021-11-12 10:43:40 -05:00
}
2022-03-23 09:39:19 -04:00
}
2021-04-14 04:24:52 -04:00
export function parseCodeBlockTitle ( metastring? : string ) : string {
2022-02-01 04:43:15 -05:00
return metastring ? . match ( codeBlockTitleRegex ) ? . groups ! . title ? ? '' ;
2021-04-14 04:24:52 -04:00
}
2021-11-12 10:43:40 -05:00
2022-04-13 08:42:35 -04:00
export function containsLineNumbers ( metastring? : string ) : boolean {
2022-05-24 07:19:24 -04:00
return Boolean ( metastring ? . includes ( 'showLineNumbers' ) ) ;
2022-04-13 08:42:35 -04:00
}
2022-03-23 09:39:19 -04:00
/ * *
* Gets the language name from the class name ( set by MDX ) .
* e . g . ` "language-javascript" ` = > ` "javascript" ` .
* Returns undefined if there is no language class name .
* /
2021-12-29 11:17:09 -05:00
export function parseLanguage ( className : string ) : string | undefined {
2021-11-12 10:43:40 -05:00
const languageClassName = className
2021-12-29 11:17:09 -05:00
. split ( ' ' )
2021-11-12 10:43:40 -05:00
. find ( ( str ) = > str . startsWith ( 'language-' ) ) ;
2021-12-29 11:17:09 -05:00
return languageClassName ? . replace ( /language-/ , '' ) ;
2021-11-12 10:43:40 -05:00
}
/ * *
2022-03-23 09:39:19 -04:00
* Parses the code content , strips away any magic comments , and returns the
* clean content and the highlighted lines marked by the comments or metastring .
*
2022-05-04 06:31:13 -04:00
* If the metastring contains a range , the ` content ` will be returned as - is
* without any parsing . The returned ` lineClassNames ` will be a map from that
* number range to the first magic comment config entry ( which _should_ be for
* line highlight directives . )
2022-03-23 09:39:19 -04:00
*
* @param content The raw code with magic comments . Trailing newline will be
* trimmed upfront .
2022-05-04 06:31:13 -04:00
* @param options Options for parsing behavior .
2021-11-12 10:43:40 -05:00
* /
export function parseLines (
content : string ,
2022-05-04 06:31:13 -04:00
options : {
/ * *
* The full metastring , as received from MDX . Line ranges declared here
* start at 1 .
* /
metastring : string | undefined ;
/ * *
* Language of the code block , used to determine which kinds of magic
* comment styles to enable .
* /
language : string | undefined ;
/ * *
* Magic comment types that we should try to parse . Each entry would
* correspond to one class name to apply to each line .
* /
magicComments : MagicCommentConfig [ ] ;
} ,
2021-11-12 10:43:40 -05:00
) : {
2022-03-23 09:39:19 -04:00
/ * *
2022-05-04 06:31:13 -04:00
* The highlighted lines , 0 - indexed . e . g . ` { 0: ["highlight", "sample"] } `
* means the 1 st line should have ` highlight ` and ` sample ` as class names .
2022-03-23 09:39:19 -04:00
* /
2022-05-04 06:31:13 -04:00
lineClassNames : { [ lineIndex : number ] : string [ ] } ;
2022-03-23 09:39:19 -04:00
/ * *
2022-05-04 06:31:13 -04:00
* If there ' s number range declared in the metastring , the code block is
* returned as - is ( no parsing ) ; otherwise , this is the clean code with all
* magic comments stripped away .
2022-03-23 09:39:19 -04:00
* /
2021-11-12 10:43:40 -05:00
code : string ;
} {
let code = content . replace ( /\n$/ , '' ) ;
2022-05-04 06:31:13 -04:00
const { language , magicComments , metastring } = options ;
2021-11-12 10:43:40 -05:00
// Highlighted lines specified in props: don't parse the content
2022-05-04 06:31:13 -04:00
if ( metastring && metastringLinesRangeRegex . test ( metastring ) ) {
const linesRange = metastring . match ( metastringLinesRangeRegex ) ! . groups !
. range ! ;
if ( magicComments . length === 0 ) {
throw new Error (
` A highlight range has been given in code block's metastring ( \` \` \` ${ metastring } ), but no magic comment config is available. Docusaurus applies the first magic comment entry's className for metastring ranges. ` ,
) ;
}
const metastringRangeClassName = magicComments [ 0 ] ! . className ;
const lines = rangeParser ( linesRange )
2021-11-12 10:43:40 -05:00
. filter ( ( n ) = > n > 0 )
2022-05-24 03:40:26 -04:00
. map ( ( n ) = > [ n - 1 , [ metastringRangeClassName ] ] as [ number , string [ ] ] ) ;
2022-05-04 06:31:13 -04:00
return { lineClassNames : Object.fromEntries ( lines ) , code } ;
2021-11-12 10:43:40 -05:00
}
if ( language === undefined ) {
2022-05-04 06:31:13 -04:00
return { lineClassNames : { } , code } ;
2021-11-12 10:43:40 -05:00
}
2022-05-04 06:31:13 -04:00
const directiveRegex = getAllMagicCommentDirectiveStyles (
language ,
magicComments ,
) ;
2022-04-17 04:39:11 -04:00
// Go through line by line
2021-11-12 10:43:40 -05:00
const lines = code . split ( '\n' ) ;
2022-05-04 06:31:13 -04:00
const blocks = Object . fromEntries (
magicComments . map ( ( d ) = > [ d . className , { start : 0 , range : '' } ] ) ,
) ;
const lineToClassName : { [ comment : string ] : string } = Object . fromEntries (
magicComments
. filter ( ( d ) = > d . line )
2022-05-24 03:40:26 -04:00
. map ( ( { className , line } ) = > [ line ! , className ] as [ string , string ] ) ,
2022-05-04 06:31:13 -04:00
) ;
const blockStartToClassName : { [ comment : string ] : string } = Object . fromEntries (
magicComments
. filter ( ( d ) = > d . block )
. map ( ( { className , block } ) = > [ block ! . start , className ] ) ,
) ;
const blockEndToClassName : { [ comment : string ] : string } = Object . fromEntries (
magicComments
. filter ( ( d ) = > d . block )
. map ( ( { className , block } ) = > [ block ! . end , className ] ) ,
) ;
2021-11-12 10:43:40 -05:00
for ( let lineNumber = 0 ; lineNumber < lines . length ; ) {
2022-03-06 00:09:10 -05:00
const line = lines [ lineNumber ] ! ;
2021-11-12 10:43:40 -05:00
const match = line . match ( directiveRegex ) ;
2022-04-05 02:09:19 -04:00
if ( ! match ) {
2022-04-17 04:39:11 -04:00
// Lines without directives are unchanged
2021-11-12 10:43:40 -05:00
lineNumber += 1 ;
2022-04-05 02:09:19 -04:00
continue ;
2021-11-12 10:43:40 -05:00
}
2022-05-24 07:19:24 -04:00
const directive = match
. slice ( 1 )
. find ( ( item : string | undefined ) = > item !== undefined ) ! ;
2022-05-04 06:31:13 -04:00
if ( lineToClassName [ directive ] ) {
blocks [ lineToClassName [ directive ] ! ] ! . range += ` ${ lineNumber } , ` ;
} else if ( blockStartToClassName [ directive ] ) {
blocks [ blockStartToClassName [ directive ] ! ] ! . start = lineNumber ;
} else if ( blockEndToClassName [ directive ] ) {
blocks [ blockEndToClassName [ directive ] ! ] ! . range += ` ${
blocks [ blockEndToClassName [ directive ] ! ] ! . start
} - $ { lineNumber - 1 } , ` ;
2022-04-05 02:09:19 -04:00
}
lines . splice ( lineNumber , 1 ) ;
2021-11-12 10:43:40 -05:00
}
code = lines . join ( '\n' ) ;
2022-05-04 06:31:13 -04:00
const lineClassNames : { [ lineIndex : number ] : string [ ] } = { } ;
Object . entries ( blocks ) . forEach ( ( [ className , { range } ] ) = > {
rangeParser ( range ) . forEach ( ( l ) = > {
lineClassNames [ l ] ? ? = [ ] ;
lineClassNames [ l ] ! . push ( className ) ;
} ) ;
} ) ;
return { lineClassNames , code } ;
2021-11-12 10:43:40 -05:00
}
2022-04-14 12:16:39 -04:00
export function getPrismCssVariables ( prismTheme : PrismTheme ) : CSSProperties {
2023-10-06 13:15:14 -04:00
const mapping : PrismThemeEntry = {
2022-04-14 12:16:39 -04:00
color : '--prism-color' ,
backgroundColor : '--prism-background-color' ,
} ;
2022-04-15 05:58:12 -04:00
const properties : { [ key : string ] : string } = { } ;
2022-04-14 12:16:39 -04:00
Object . entries ( prismTheme . plain ) . forEach ( ( [ key , value ] ) = > {
2023-10-06 13:15:14 -04:00
const varName = mapping [ key as keyof PrismThemeEntry ] ;
2022-04-14 12:16:39 -04:00
if ( varName && typeof value === 'string' ) {
properties [ varName ] = value ;
}
} ) ;
return properties ;
}