2023-04-13 04:21:25 -04:00
/ * *
Convert Ace themes to CodeMirror 6
Tokens :
https : //github.com/ajaxorg/ace/wiki/Creating-or-Extending-an-Edit-Mode#common-tokens
Highlight Rules :
https : //github.com/overleaf/ace/blob/overleaf/lib/ace/mode/latex_highlight_rules.js
Conversion of TextMate themes to Ace :
https : //github.com/ajaxorg/ace/wiki/Importing-.tmtheme-and-.tmlanguage-Files-into-Ace
https : //github.com/ajaxorg/ace/blob/master/tool/tmtheme.js
* /
const fs = require ( 'fs' )
const globby = require ( 'globby' )
const mensch = require ( 'mensch' )
const path = require ( 'path' )
const overrides = require ( './overrides.json' )
const { merge } = require ( 'lodash' )
// CSS files from https://github.com/overleaf/ace/tree/overleaf/lib/ace/theme copied into the "ace" folder
const themePaths = globby . sync ( [ 'ace/*.css' ] , { cwd : _ _dirname } )
const outputDir = path . join ( _ _dirname , 'cm6' )
// from js/ide.js
const darkThemes = [
'ambiance' ,
'chaos' ,
'clouds_midnight' ,
'cobalt' ,
'dracula' ,
'gob' ,
'gruvbox' ,
'idle_fingers' ,
'kr_theme' ,
'merbivore' ,
'merbivore_soft' ,
'mono_industrial' ,
'monokai' ,
'nord_dark' ,
'pastel_on_dark' ,
'solarized_dark' ,
'terminal' ,
'tomorrow_night' ,
'tomorrow_night_blue' ,
'tomorrow_night_bright' ,
'tomorrow_night_eighties' ,
'twilight' ,
'vibrant_ink' ,
]
// manual mapping of Ace selectors to CM6 theme selectors
const themeMapping = new Map ( [
[ '.ace_gutter' , '.cm-gutters' ] ,
[ '.ace_cursor' , '.cm-cursor, .cm-dropCursor' ] ,
[
'.ace_marker-layer .ace_selection' ,
2023-04-20 05:58:22 -04:00
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection, .cm-searchMatch.cm-searchMatch.cm-searchMatch-selected' ,
2023-04-13 04:21:25 -04:00
] ,
[
'.ace_marker-layer .ace_selected-word' ,
'.cm-selectionMatch.cm-selectionMatch, .cm-searchMatch.cm-searchMatch' , // doubled to increase specificity over defaults
] ,
[
'.ace_marker-layer .ace_bracket' ,
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket' ,
] ,
[ '.ace_marker-layer .ace_bracket-unmatched' , '.cm-nonmatchingBracket' ] ,
[ '.ace_marker-layer .ace_active-line' , '.cm-activeLine' ] ,
[ '.ace_gutter-active-line' , '.cm-activeLineGutter' ] ,
[ '.ace_fold' , '.cm-foldPlaceholder' ] ,
] )
const propertyRemapping = new Map ( [
[
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket' ,
[ [ 'border' , 'outline' ] ] ,
] ,
[
'.cm-selectionMatch.cm-selectionMatch, .cm-searchMatch.cm-searchMatch' ,
[ [ 'border' , 'outline' ] ] ,
] ,
] )
function remap ( selector , rule ) {
const remappings = propertyRemapping . get ( selector ) ? ? [ ]
for ( const [ oldKey , newKey ] of remappings ) {
if ( newKey in rule ) {
throw new Error (
` Invalid remapping. Property ${ newKey } already exists in rule ' ${ selector } ' `
)
}
if ( oldKey in rule ) {
rule [ newKey ] = rule [ oldKey ]
delete rule [ oldKey ]
}
}
return rule
}
// manual mapping of Ace selectors to CM6 highlight style selectors
// https://codemirror.net/6/docs/ref/#highlight.tags
// (the classHighlightStyle extension adds the class names for styling)
const highlightStyleMapping = new Map ( [
// ['.ace_support.ace_type', '.tok-typeName'],
[ '.ace_class' , '.tok-class' ] ,
[ '.ace_comment' , '.tok-comment' ] ,
[ '.ace_constant' , '.tok-labelName' ] ,
[ '.ace_constant.ace_character' , '.tok-literal' ] ,
[ '.ace_constant.ace_character.ace_escape' , '.tok-literal' ] , // escape
[ '.ace_constant.ace_language' , '.tok-literal' ] , // constant
[ '.ace_constant.ace_numeric' , '.tok-literal' ] , // number
[ '.ace_constant.ace_other' , '.tok-literal' ] , // constant
[ '.ace_entity.ace_name.ace_function' , '.tok-function' ] ,
[ '.ace_entity.ace_name.ace_tag' , '.tok-tagName' ] ,
[ '.ace_entity.ace_other.ace_attribute-name' , '.tok-attributeName' ] ,
[ '.ace_heading' , '.tok-heading' ] ,
[ '.ace_identifier' , '.tok-string' ] , // TODO: identifier?
[ '.ace_invalid' , '.tok-invalid' ] ,
[ '.ace_keyword' , '.tok-keyword' ] , // typeName?
[ '.ace_keyword.ace_operator' , '.tok-operator' ] ,
[ '.ace_list' , '.tok-list' ] ,
[ '.ace_lparen' , '.tok-paren' ] ,
[ '.ace_markup.ace_heading' , '.tok-heading' ] ,
[ '.ace_markup.ace_list' , '.tok-list' ] ,
[ '.ace_numeric' , '.tok-number' ] ,
[ '.ace_punctuation' , '.tok-punctuation' ] ,
[ '.ace_regexp' , '.tok-string2' ] ,
[ '.ace_rparen' , '.tok-paren' ] ,
[ '.ace_storage' , '.tok-typeName' ] ,
[ '.ace_storage.ace_type' , '.tok-typeName' ] ,
[ '.ace_string' , '.tok-string' ] ,
[ '.ace_string.ace_regexp' , '.tok-regexp' ] ,
// ['.ace_support.ace_class', '.tok-className'],
// ['.ace_support.ace_constant', '.tok-constant'],
// ['.ace_support.ace_function', '.tok-function'],
// ['.ace_support.ace_type', '.tok-function'],
[ '.ace_type' , '.tok-typeName' ] ,
[ '.ace_variable' , '.tok-attributeValue' ] , // keyword // variableName
[ '.ace_variable.ace_language' , '.tok-variableName' ] ,
[ '.ace_variable.ace_parameter' , '.tok-attributeValue' ] , // string
] )
for ( const themePath of themePaths ) {
console . log ( themePath )
const input = fs . readFileSync ( path . join ( _ _dirname , themePath ) , 'utf-8' )
const ast = mensch . parse ( input )
const themeStyles = {
// these styles should only be set if they're defined in a theme
'.cm-gutters' : {
backgroundColor : 'transparent' ,
borderRightColor : 'transparent' ,
} ,
}
const highlightStyles = { }
for ( const rule of ast . stylesheet . rules ) {
const declarations = { }
rule . declarations
. filter ( item => item . type === 'property' )
. forEach ( declaration => {
// convert CSS property to snake case
const property = declaration . name . replace ( /-(\w)/g , ( _ , letter ) => {
return letter . toUpperCase ( )
} )
declarations [ property ] = declaration . value
} )
for ( const item of rule . selectors ) {
// ignore the first selector, which is the theme class
const selector = item . split ( /\s+/ ) . slice ( 1 ) . join ( ' ' )
// an empty selector was the theme class selector for the whole editor
if ( selector === '' ) {
themeStyles [ '&' ] = {
... themeStyles [ '&' ] ,
... declarations ,
}
continue
}
if ( themeMapping . has ( selector ) ) {
const key = themeMapping . get ( selector )
themeStyles [ key ] = remap ( key , {
... themeStyles [ key ] ,
... declarations ,
} )
} else if ( highlightStyleMapping . has ( selector ) ) {
const key = highlightStyleMapping . get ( selector )
highlightStyles [ key ] = remap ( key , {
... highlightStyles [ key ] ,
... declarations ,
} )
}
}
}
console . log ( 'theme' , themeStyles )
console . log ( 'highlight' , highlightStyles )
const basename = path . basename ( themePath , '.css' )
const themeOverrides = merge ( { } , overrides . all , overrides [ basename ] )
const theme = merge ( { } , themeStyles , themeOverrides . theme )
const highlightStyle = merge (
{ } ,
highlightStyles ,
themeOverrides . highlightStyle
)
const dark = darkThemes . includes ( basename )
const output = JSON . stringify ( { theme , highlightStyle , dark } , null , 2 )
const outputPath = path . join ( outputDir , ` ${ basename } .json ` )
fs . writeFileSync ( outputPath , output + '\n' )
if ( basename !== 'overleaf' ) {
copyLicense ( basename )
}
}
function copyLicense ( basename ) {
const jsFilePath = path . join ( _ _dirname , 'ace' , ` ${ basename } .js ` )
if ( fs . existsSync ( jsFilePath ) ) {
const js = fs . readFileSync ( jsFilePath , 'utf-8' )
const match = js . match ( / \ * + B E G I N L I C E N S E B L O C K . + ? E N D L I C E N S E B L O C K \ * + / s )
if ( match ) {
const license = match [ 0 ] . replace ( /\n \* ?/g , '\n' )
const output = ` Conversion by Overleaf from Ace to CodeMirror 6. \n \n Source: https://github.com/ajaxorg/ace/ \n \n The theme's original license is copied below: \n \n ${ license } `
const licenseOutputPath = path . join ( outputDir , ` ${ basename } -license.txt ` )
fs . writeFileSync ( licenseOutputPath , output )
} else {
console . warn ( ` No license in ${ jsFilePath } ` )
}
} else {
console . warn ( ` No license file for ${ basename } ` )
}
}