/* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns * DS104: Avoid inline assignments * DS205: Consider reworking code to avoid use of IIFEs * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ import path from "path" import url from "url" import zlib from "zlib" import * as _ from "@aminya/underscore-plus" import CSON from "season" import plist from "@atom/plist" import { ScopeSelector } from "first-mate" import tar from "tar" import temp from "temp" import fs from "./fs" import * as request from "./request" // Convert a TextMate bundle to an Atom package export default class PackageConverter { sourcePath: string destinationPath: string plistExtensions: string[] directoryMappings: { Preferences: string; Snippets: string; Syntaxes: string } constructor(sourcePath: string, destinationPath: string) { this.sourcePath = sourcePath this.destinationPath = path.resolve(destinationPath) this.plistExtensions = [".plist", ".tmCommand", ".tmLanguage", ".tmMacro", ".tmPreferences", ".tmSnippet"] this.directoryMappings = { Preferences: "settings", Snippets: "snippets", Syntaxes: "grammars", } } convert(callback: Function) { const { protocol } = url.parse(this.sourcePath) if (protocol === "http:" || protocol === "https:") { return this.downloadBundle(callback) } else { return this.copyDirectories(this.sourcePath, callback) } } getDownloadUrl() { let downloadUrl = this.sourcePath downloadUrl = downloadUrl.replace(/(\.git)?\/*$/, "") return (downloadUrl += "/archive/master.tar.gz") } downloadBundle(callback: Function) { const tempPath = temp.mkdirSync("atom-bundle-") const requestOptions = { url: this.getDownloadUrl() } return request.createReadStream(requestOptions, (readStream) => { readStream.on("response", function ({ headers, statusCode }) { if (statusCode !== 200) { return callback(`Download failed (${headers.status})`) } }) return readStream .pipe(zlib.createGunzip()) .pipe(tar.extract({ cwd: tempPath })) .on("error", (error) => callback(error)) .on("end", () => { const sourcePath = path.join(tempPath, fs.readdirSync(tempPath)[0]) return this.copyDirectories(sourcePath, callback) }) }) } copyDirectories(sourcePath: string, callback: Function) { let packageName: string sourcePath = path.resolve(sourcePath) try { packageName = JSON.parse(fs.readFileSync(path.join(sourcePath, "package.json")))?.packageName } catch (error) { /* ignore error */ } if (packageName == null) { packageName = path.basename(this.destinationPath) } this.convertSnippets(packageName, sourcePath) this.convertPreferences(packageName, sourcePath) this.convertGrammars(sourcePath) return callback() } filterObject(object) { delete object.uuid return delete object.keyEquivalent } convertSettings(settings: { shellVariables: { name: any; value: any }[] | {} increaseIndentPattern: any decreaseIndentPattern: any foldingStopMarker: any completions: any }) { if (settings.shellVariables) { const shellVariables = {} for (const { name, value } of settings.shellVariables) { shellVariables[name] = value } settings.shellVariables = shellVariables } const editorProperties = _.compactObject({ commentStart: _.valueForKeyPath(settings, "shellVariables.TM_COMMENT_START"), commentEnd: _.valueForKeyPath(settings, "shellVariables.TM_COMMENT_END"), increaseIndentPattern: settings.increaseIndentPattern, decreaseIndentPattern: settings.decreaseIndentPattern, foldEndPattern: settings.foldingStopMarker, completions: settings.completions, }) if (!_.isEmpty(editorProperties)) { return { editor: editorProperties } } } readFileSync(filePath: string) { if (this.plistExtensions.includes(path.extname(filePath))) { return plist.parseFileSync(filePath) } else if ([".json", ".cson"].includes(path.extname(filePath))) { return CSON.readFileSync(filePath) } } writeFileSync(filePath: string, object = {}) { this.filterObject(object) if (Object.keys(object).length > 0) { return CSON.writeFileSync(filePath, object) } } convertFile(sourcePath: string, destinationDir: string) { let contents: {} const extension = path.extname(sourcePath) let destinationName = `${path.basename(sourcePath, extension)}.cson` destinationName = destinationName.toLowerCase() const destinationPath = path.join(destinationDir, destinationName) if (this.plistExtensions.includes(path.extname(sourcePath))) { contents = plist.parseFileSync(sourcePath) } else if ([".json", ".cson"].includes(path.extname(sourcePath))) { contents = CSON.readFileSync(sourcePath) } return this.writeFileSync(destinationPath, contents) } normalizeFilenames(directoryPath: string) { if (!fs.isDirectorySync(directoryPath)) { return } return (() => { const result = [] for (const child of fs.readdirSync(directoryPath)) { const childPath = path.join(directoryPath, child) // Invalid characters taken from http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx let convertedFileName = child.replace(/["*/:<>?\\|]+/g, "-") if (child === convertedFileName) { continue } convertedFileName = convertedFileName.replace(/[\s-]+/g, "-") let convertedPath = path.join(directoryPath, convertedFileName) let suffix = 1 while (fs.existsSync(convertedPath) || fs.existsSync(convertedPath.toLowerCase())) { const extension = path.extname(convertedFileName) convertedFileName = `${path.basename(convertedFileName, extension)}-${suffix}${extension}` convertedPath = path.join(directoryPath, convertedFileName) suffix++ } result.push(fs.renameSync(childPath, convertedPath)) } return result })() } convertSnippets(packageName: string, source: string) { let sourceSnippets = path.join(source, "snippets") if (!fs.isDirectorySync(sourceSnippets)) { sourceSnippets = path.join(source, "Snippets") } if (!fs.isDirectorySync(sourceSnippets)) { return } const snippetsBySelector = {} const destination = path.join(this.destinationPath, "snippets") for (const child of fs.readdirSync(sourceSnippets)) { let left, selector const snippet = (left = this.readFileSync(path.join(sourceSnippets, child))) != null ? left : {} const { scope, tabTrigger } = snippet let { name, content } = snippet if (!tabTrigger || !content) { continue } // Replace things like '${TM_C_POINTER: *}' with ' *' content = content.replace(/\${TM_[A-Z_]+:([^}]+)}/g, "$1") // Replace things like '${1:${TM_FILENAME/(\\w+)*/(?1:$1:NSObject)/}}' // with '$1' content = content.replace(/\${(\d)+:\s*\${TM_[^}]+\s*}\s*}/g, "$$1") // Unescape escaped dollar signs $ content = content.replace(/\\\$/g, "$") if (name == null) { const extension = path.extname(child) name = path.basename(child, extension) } try { if (scope) { selector = new ScopeSelector(scope).toCssSelector() } } catch (e) { e.message = `In file ${e.fileName} at ${JSON.stringify(scope)}: ${e.message}` throw e } if (selector == null) { selector = "*" } if (snippetsBySelector[selector] == null) { snippetsBySelector[selector] = {} } snippetsBySelector[selector][name] = { prefix: tabTrigger, body: content } } this.writeFileSync(path.join(destination, `${packageName}.cson`), snippetsBySelector) return this.normalizeFilenames(destination) } convertPreferences(packageName: string, source: string) { let sourcePreferences = path.join(source, "preferences") if (!fs.isDirectorySync(sourcePreferences)) { sourcePreferences = path.join(source, "Preferences") } if (!fs.isDirectorySync(sourcePreferences)) { return } const preferencesBySelector = {} const destination = path.join(this.destinationPath, "settings") for (const child of fs.readdirSync(sourcePreferences)) { let left, properties: { [x: string]: any; editor?: any } const { scope, settings } = (left = this.readFileSync(path.join(sourcePreferences, child))) != null ? left : {} if (!scope || !settings) { continue } if ((properties = this.convertSettings(settings))) { let selector: string try { selector = new ScopeSelector(scope).toCssSelector() } catch (e) { e.message = `In file ${e.fileName} at ${JSON.stringify(scope)}: ${e.message}` throw e } for (const key in properties) { const value = properties[key] if (preferencesBySelector[selector] == null) { preferencesBySelector[selector] = {} } if (preferencesBySelector[selector][key] != null) { preferencesBySelector[selector][key] = { ...value, ...preferencesBySelector[selector][key] } } else { preferencesBySelector[selector][key] = value } } } } this.writeFileSync(path.join(destination, `${packageName}.cson`), preferencesBySelector) return this.normalizeFilenames(destination) } convertGrammars(source: string) { let sourceSyntaxes = path.join(source, "syntaxes") if (!fs.isDirectorySync(sourceSyntaxes)) { sourceSyntaxes = path.join(source, "Syntaxes") } if (!fs.isDirectorySync(sourceSyntaxes)) { return } const destination = path.join(this.destinationPath, "grammars") for (const child of fs.readdirSync(sourceSyntaxes)) { const childPath = path.join(sourceSyntaxes, child) if (fs.isFileSync(childPath)) { this.convertFile(childPath, destination) } } return this.normalizeFilenames(destination) } }