/* global atom */ 'use strict'; var beautifyJS = require('js-beautify'); var beautifyHTML = require('js-beautify').html; var beautifyCSS = require('js-beautify').css; var fs = require('fs'); var path = require('path'); var nopt = require('nopt'); var extend = require('extend'); var _ = require('lodash'); var shjs = require('shelljs'); // TODO: Copied from jsbeautify, please update it from time to time var knownOpts = { // Beautifier 'indent_size': Number, 'indent_char': String, 'indent_level': Number, 'indent_with_tabs': Boolean, 'preserve_newlines': Boolean, 'max_preserve_newlines': Number, 'space_in_paren': Boolean, 'jslint_happy': Boolean, // TODO: expand-strict is obsolete, now identical to expand. Remove in future version 'brace_style': ['collapse', 'expand', 'end-expand', 'expand-strict'], 'break_chained_methods': Boolean, 'keep_array_indentation': Boolean, 'unescape_strings': Boolean, 'wrap_line_length': Number, 'e4x': Boolean, // HTML-only 'max_char': Number, // obsolete since 1.3.5 'unformatted': [String, Array], 'indent_inner_html': [Boolean], 'indent_scripts': ['keep', 'separate', 'normal'], // CLI 'version': Boolean, 'help': Boolean, 'files': [path, Array], 'outfile': path, 'replace': Boolean, 'quiet': Boolean, 'type': ['js', 'css', 'html'], 'config': path }; var Subscriber = require('emissary').Subscriber; var plugin = module.exports; Subscriber.extend(plugin); function getUserHome() { return process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE; } function cleanOptions(data, types) { nopt.clean(data, types); return data; } function getCursors(editor) { var cursors = editor.getCursors(); var posArray = []; for (var idx = 0; idx < cursors.length; idx++) { var cursor = cursors[idx]; var bufferPosition = cursor.getBufferPosition(); posArray.push([bufferPosition.row, bufferPosition.column]); } return posArray; } function setCursors(editor, posArray) { for (var idx = 0; idx < posArray.length; idx++) { var bufferPosition = posArray[idx]; if (idx === 0) { editor.setCursorBufferPosition(bufferPosition); continue; } editor.addCursorAtBufferPosition(bufferPosition); } } // Storage for memoized results from find file // Should prevent lots of directory traversal & // lookups when liniting an entire project var findFileResults = {}; /** * Searches for a file with a specified name starting with * 'dir' and going all the way up either until it finds the file * or hits the root. * * @param {string} name filename to search for (e.g. .jshintrc) * @param {string} dir directory to start search from (default: * current working directory) * * @returns {string} normalized filename */ function findFile(name, dir) { dir = dir || process.cwd(); var filename = path.normalize(path.join(dir, name)); if (findFileResults[filename] !== undefined) { return findFileResults[filename]; } var parent = path.resolve(dir, '../'); if (shjs.test('-e', filename)) { findFileResults[filename] = filename; return filename; } if (dir === parent) { findFileResults[filename] = null; return null; } return findFile(name, parent); } /** * Tries to find a configuration file in either project directory * or in the home directory. Configuration files are named * '.jshintrc'. * * @param {string} config name of the configuration file * @param {string} file path to the file to be linted * @returns {string} a path to the config file */ function findConfig(config, file) { var dir = path.dirname(path.resolve(file)); var envs = getUserHome(); var home = path.normalize(path.join(envs, config)); var proj = findFile(config, dir); if (proj) { return proj; } if (shjs.test('-e', home)) { return home; } return null; } function beautify() { console.log('Beautify!!!'); var text; var editor = atom.workspace.getActiveEditor(); var isSelection = !! editor.getSelectedText(); var softTabs = editor.softTabs; var tabLength = editor.getTabLength(); var beautifyOptions = { 'indent_size': softTabs ? tabLength : 1, 'indent_char': softTabs ? ' ' : '\t', 'indent_with_tabs': !softTabs }; // Look for .jsbeautifierrc in file and home path, check env variables var editedFilePath = editor.getPath(); // Get the path to the config file var configPath = findConfig('.jsbeautifyrc', editedFilePath); var externalOptions; if (configPath) { var strip = require('strip-json-comments'); try { externalOptions = JSON.parse(strip(fs.readFileSync(configPath, { encoding: 'utf8' }))); } catch (e) { externalOptions = {}; } } else { externalOptions = {}; } var containsNested = false; var collectedConfig = {}; var key; // Check to see if config file uses nested object format to split up js/css/html options for (key in externalOptions) { if (typeof externalOptions[key] === 'object') { containsNested = true; } } // Create a flat object of config options if nested format was used if (!containsNested) { collectedConfig = externalOptions; } else { for (key in externalOptions) { _.merge(collectedConfig, externalOptions[key]); } } beautifyOptions = extend(collectedConfig, beautifyOptions); beautifyOptions = cleanOptions(beautifyOptions, knownOpts); if (isSelection) { text = editor.getSelectedText(); } else { text = editor.getText(); } var oldText = text; switch (editor.getGrammar().name) { case 'JavaScript': text = beautifyJS(text, beautifyOptions); break; case 'HTML (Liquid)': case 'HTML': case 'XML': text = beautifyHTML(text, beautifyOptions); break; case 'CSS': text = beautifyCSS(text, beautifyOptions); break; default: return; } if (oldText !== text) { var posArray = getCursors(editor); var origScrollTop = editor.getScrollTop(); if (isSelection) { editor.setTextInBufferRange( editor.getSelectedBufferRange(), text ); } else { editor.setText(text); } setCursors(editor, posArray); // Let the scrollTop setting run after all the save related stuff is run, // otherwise setScrollTop is not working, probably because the cursor // addition happens asynchronously setTimeout(function () { editor.setScrollTop(origScrollTop); }, 0); } } function handleSafeEvent() { atom.workspace.eachEditor(function (editor) { var buffer = editor.getBuffer(); plugin.unsubscribe(buffer); if (atom.config.get('atom-beautify.beautifyOnSave')) { var events = 'will-be-saved'; plugin.subscribe(buffer, events, beautify); } }); } plugin.configDefaults = { beautifyOnSave: false }; plugin.activate = function () { handleSafeEvent(); plugin.subscribe(atom.config.observe( 'atom-beautify.beautifyOnSave', handleSafeEvent)); return atom.workspaceView.command('beautify', beautify); };