/* ***** BEGIN LICENSE BLOCK ***** * Distributed under the BSD license: * * Copyright (c) 2012, Ajax.org B.V. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Ajax.org B.V. nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * ***** END LICENSE BLOCK ***** */ // [WIP] define(function(require, exports, module) { "use strict"; var TokenIterator = require("../token_iterator").TokenIterator; function is(token, type) { return token.type.lastIndexOf(type + ".xml") > -1; } // do not indent after singleton tags or exports.singletonTags = ["area", "base", "br", "col", "command", "embed", "hr", "html", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]; // insert a line break after block level tags exports.blockTags = ["article", "aside", "blockquote", "body", "div", "dl", "fieldset", "footer", "form", "head", "header", "html", "nav", "ol", "p", "script", "section", "style", "table", "tbody", "tfoot", "thead", "ul"]; exports.beautify = function(session) { var iterator = new TokenIterator(session, 0, 0); var token = iterator.getCurrentToken(); var tabString = session.getTabString(); var singletonTags = exports.singletonTags; var blockTags = exports.blockTags; var nextToken; var breakBefore = false; var spaceBefore = false; var spaceAfter = false; var code = ""; var value = ""; var tagName = ""; var depth = 0; var lastDepth = 0; var lastIndent = 0; var indent = 0; var unindent = 0; var roundDepth = 0; var curlyDepth = 0; var row; var curRow = 0; var rowsToAdd = 0; var rowTokens = []; var abort = false; var i; var indentNextLine = false; var inTag = false; var inCSS = false; var inBlock = false; var levels = {0: 0}; var parents = []; var trimNext = function() { if (nextToken && nextToken.value && nextToken.type !== 'string.regexp') nextToken.value = nextToken.value.trim(); }; var trimLine = function() { code = code.replace(/ +$/, ""); }; var trimCode = function() { code = code.trimRight(); breakBefore = false; }; while (token !== null) { curRow = iterator.getCurrentTokenRow(); rowTokens = iterator.$rowTokens; nextToken = iterator.stepForward(); if (typeof token !== "undefined") { value = token.value; unindent = 0; // mode inCSS = (tagName === "style" || session.$modeId === "ace/mode/css"); // in tag if (is(token, "tag-open")) { inTag = true; // are we in a block tag if (nextToken) inBlock = (blockTags.indexOf(nextToken.value) !== -1); // html indentation if (value === " 0; rowsToAdd--) code += "\n"; breakBefore = true; // trim value if not in a comment or string if (!is(token, "comment") && !token.type.match(/^(comment|string)$/)) value = value.trimLeft(); } if (value) { // whitespace if (token.type === "keyword" && value.match(/^(if|else|elseif|for|foreach|while|switch)$/)) { parents[depth] = value; trimNext(); spaceAfter = true; // space before else, elseif if (value.match(/^(else|elseif)$/)) { if (code.match(/\}[\s]*$/)) { trimCode(); spaceBefore = true; } } // trim value after opening paren } else if (token.type === "paren.lparen") { trimNext(); // whitespace after { if (value.substr(-1) === "{") { spaceAfter = true; indentNextLine = false; if(!inTag) rowsToAdd = 1; } // ensure curly brace is preceeded by whitespace if (value.substr(0, 1) === "{") { spaceBefore = true; // collapse square and curly brackets together if (code.substr(-1) !== '[' && code.trimRight().substr(-1) === '[') { trimCode(); spaceBefore = false; } else if (code.trimRight().substr(-1) === ')') { trimCode(); } else { trimLine(); } } // remove space before closing paren } else if (token.type === "paren.rparen") { unindent = 1; // ensure curly brace is preceeded by whitespace if (value.substr(0, 1) === "}") { if (parents[depth-1] === 'case') unindent++; if (code.trimRight().substr(-1) === '{') { trimCode(); } else { spaceBefore = true; if (inCSS) rowsToAdd+=2; } } // collapse square and curly brackets together if (value.substr(0, 1) === "]") { if (code.substr(-1) !== '}' && code.trimRight().substr(-1) === '}') { spaceBefore = false; indent++; trimCode(); } } // collapse round brackets together if (value.substr(0, 1) === ")") { if (code.substr(-1) !== '(' && code.trimRight().substr(-1) === '(') { spaceBefore = false; indent++; trimCode(); } } trimLine(); // add spaces around conditional operators } else if ((token.type === "keyword.operator" || token.type === "keyword") && value.match(/^(=|==|===|!=|!==|&&|\|\||and|or|xor|\+=|.=|>|>=|<|<=|=>)$/)) { trimCode(); trimNext(); spaceBefore = true; spaceAfter = true; // remove space before semicolon } else if (token.type === "punctuation.operator" && value === ';') { trimCode(); trimNext(); spaceAfter = true; if (inCSS) rowsToAdd++; // space after colon or comma } else if (token.type === "punctuation.operator" && value.match(/^(:|,)$/)) { trimCode(); trimNext(); // line break after commas in curly block if (value.match(/^(,)$/) && curlyDepth>0 && roundDepth===0) { rowsToAdd++; } else { spaceAfter = true; breakBefore = false; } // ensure space before php closing tag } else if (token.type === "support.php_tag" && value === "?>" && !breakBefore) { trimCode(); spaceBefore = true; // remove excess space before HTML attribute } else if (is(token, "attribute-name") && code.substr(-1).match(/^\s$/)) { spaceBefore = true; // remove space around attribute equals } else if (is(token, "attribute-equals")) { trimLine(); trimNext(); // remove space before HTML closing tag } else if (is(token, "tag-close")) { trimLine(); if(value === "/>") spaceBefore = true; } // add indent to code unless multiline string or comment if (breakBefore && !(token.type.match(/^(comment)$/) && !value.substr(0, 1).match(/^[/#]$/)) && !(token.type.match(/^(string)$/) && !value.substr(0, 1).match(/^['"]$/))) { indent = lastIndent; if(depth > lastDepth) { indent++; for (i=depth; i > lastDepth; i--) levels[i] = indent; } else if(depth < lastDepth) indent = levels[depth]; lastDepth = depth; lastIndent = indent; if(unindent) indent -= unindent; if (indentNextLine && !roundDepth) { indent++; indentNextLine = false; } for (i = 0; i < indent; i++) code += tabString; } if (token.type === "keyword" && value.match(/^(case|default)$/)) { parents[depth] = value; depth++; } if (token.type === "keyword" && value.match(/^(break)$/)) { if(parents[depth-1] && parents[depth-1].match(/^(case|default)$/)) { depth--; } } // indent one line after if or else if (token.type === "paren.lparen") { roundDepth += (value.match(/\(/g) || []).length; curlyDepth += (value.match(/\{/g) || []).length; depth += value.length; } if (token.type === "keyword" && value.match(/^(if|else|elseif|for|while)$/)) { indentNextLine = true; roundDepth = 0; } else if (!roundDepth && value.trim() && token.type !== "comment") indentNextLine = false; if (token.type === "paren.rparen") { roundDepth -= (value.match(/\)/g) || []).length; curlyDepth -= (value.match(/\}/g) || []).length; for (i = 0; i < value.length; i++) { depth--; if(value.substr(i, 1)==='}' && parents[depth]==='case') { depth--; } } } // add to code if (spaceBefore && !breakBefore) { trimLine(); if (code.substr(-1) !== "\n") code += " "; } code += value; if (spaceAfter) code += " "; breakBefore = false; spaceBefore = false; spaceAfter = false; // line break after block tag or doctype if ((is(token, "tag-close") && (inBlock || blockTags.indexOf(tagName) !== -1)) || (is(token, "doctype") && value === ">")) { // undo linebreak if tag is immediately closed if (inBlock && nextToken && nextToken.value === "" && singletonTags.indexOf(tagName) === -1){ depth--; } row = curRow; } } token = nextToken; } code = code.trim(); session.doc.setValue(code); }; exports.commands = [{ name: "beautify", description: "Format selection (Beautify)", exec: function(editor) { exports.beautify(editor.session); }, bindKey: "Ctrl-Shift-B" }]; });