/** 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', '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection, .cm-searchMatch.cm-searchMatch.cm-searchMatch-selected', ], [ '.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(/\*+ BEGIN LICENSE BLOCK .+? END LICENSE BLOCK \*+/s) if (match) { const license = match[0].replace(/\n \* ?/g, '\n') const output = `Conversion by Overleaf from Ace to CodeMirror 6.\n\nSource: https://github.com/ajaxorg/ace/\n\nThe 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}`) } }