Merge pull request #119 from tophf/beautifier

Editor: CSS-beautifier
This commit is contained in:
Jason Barnabe 2015-07-03 22:45:45 -05:00
commit b727e9128f
5 changed files with 640 additions and 30 deletions

View File

@ -261,6 +261,10 @@
"message": "Regexp is invalid.",
"description": "Validation message for a bad regexp in a style"
},
"styleBeautify": {
"message": "Beautify",
"description": "Label for the CSS-beautifier button on the edit style page"
},
"styleCancelEditLabel": {
"message": "Back to manage",
"description": "Label for cancel button for style editing"
@ -315,6 +319,14 @@
"message": "(Stylish does not work on pages like this.)",
"description": "Note in the toolbar pop-up when on a URL Stylish can't affect"
},
"undo": {
"message": "Undo",
"description": "Button label"
},
"undoGlobal": {
"message": "Undo (global)",
"description": "CSS-beautify global Undo button label"
},
"updateCheckFailBadResponseCode": {
"message": "Update failed - server responded with code $code$.",
"description": "Text that displays when an update check failed because the response code indicates an error",

464
beautify/beautify-css.js Normal file
View File

@ -0,0 +1,464 @@
/*jshint curly:true, eqeqeq:true, laxbreak:true, noempty:false */
/*
The MIT License (MIT)
Copyright (c) 2007-2013 Einar Lielmanis and contributors.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
CSS Beautifier
---------------
Written by Harutyun Amirjanyan, (amirjanyan@gmail.com)
Based on code initially developed by: Einar Lielmanis, <einar@jsbeautifier.org>
http://jsbeautifier.org/
Usage:
css_beautify(source_text);
css_beautify(source_text, options);
The options are (default in brackets):
indent_size (4) indentation size,
indent_char (space) character to indent with,
selector_separator_newline (true) - separate selectors with newline or
not (e.g. "a,\nbr" or "a, br")
end_with_newline (false) - end with a newline
newline_between_rules (true) - add a new line after every css rule
e.g
css_beautify(css_source_text, {
'indent_size': 1,
'indent_char': '\t',
'selector_separator': ' ',
'end_with_newline': false,
'newline_between_rules': true
});
*/
// http://www.w3.org/TR/CSS21/syndata.html#tokenization
// http://www.w3.org/TR/css3-syntax/
(function() {
function css_beautify(source_text, options) {
function defaultOption(opt, defaultValue) {
return opt === undefined ? defaultValue : opt;
}
options = options || {};
var indentSize = options.indent_size || 4;
var indentCharacter = options.indent_char || ' ';
var selectorSeparatorNewline = defaultOption(options.selector_separator_newline, true);
var end_with_newline = defaultOption(options.end_with_newline, false);
var newline_between_rules = defaultOption(options.newline_between_rules, true);
var newline_between_properties = defaultOption(options.newline_between_properties, true);
var newline_before_open_brace = defaultOption(options.newline_before_open_brace, false);
var newline_after_open_brace = defaultOption(options.newline_after_open_brace, true);
var newline_before_close_brace = defaultOption(options.newline_before_close_brace, true);
// compatibility
if (typeof indentSize === "string") {
indentSize = parseInt(indentSize, 10);
}
// tokenizer
var whiteRe = /^\s+$/;
var wordRe = /[\w$\-_]/;
var pos = -1,
ch;
var parenLevel = 0;
function next() {
ch = source_text.charAt(++pos);
return ch || '';
}
function peek(skipWhitespace) {
var prev_pos = pos;
if (skipWhitespace) {
eatWhitespace();
}
result = source_text.charAt(pos + 1) || '';
pos = prev_pos - 1;
next();
return result;
}
function eatString(endChars) {
var start = pos;
while (next()) {
if (ch === "\\") {
next();
} else if (endChars.indexOf(ch) !== -1) {
break;
} else if (ch === "\n") {
break;
}
}
return source_text.substring(start, pos + 1);
}
function peekString(endChar) {
var prev_pos = pos;
var str = eatString(endChar);
pos = prev_pos - 1;
next();
return str;
}
function eatWhitespace() {
var result = '';
while (whiteRe.test(peek())) {
next();
result += ch;
}
return result;
}
function skipWhitespace() {
var result = '';
if (ch && whiteRe.test(ch)) {
result = ch;
}
while (whiteRe.test(next())) {
result += ch;
}
return result;
}
function eatComment(singleLine) {
var start = pos;
singleLine = peek() === "/";
next();
while (next()) {
if (!singleLine && ch === "*" && peek() === "/") {
next();
break;
} else if (singleLine && ch === "\n") {
return source_text.substring(start, pos);
}
}
return source_text.substring(start, pos) + ch;
}
function lookBack(str) {
return source_text.substring(pos - str.length, pos).toLowerCase() ===
str;
}
// Nested pseudo-class if we are insideRule
// and the next special character found opens
// a new block
function foundNestedPseudoClass() {
for (var i = pos + 1; i < source_text.length; i++) {
var ch = source_text.charAt(i);
if (ch === "{") {
return true;
} else if (ch === ";" || ch === "}" || ch === ")") {
return false;
}
}
return false;
}
// printer
var basebaseIndentString = source_text.match(/^[\t ]*/)[0];
var singleIndent = new Array(indentSize + 1).join(indentCharacter);
var indentLevel = 0;
var nestedLevel = 0;
function indent() {
indentLevel++;
basebaseIndentString += singleIndent;
}
function outdent() {
indentLevel--;
basebaseIndentString = basebaseIndentString.slice(0, -indentSize);
}
var print = {};
print["{"] = function(ch) {
newline_before_open_brace ? output.push('\n') : print.singleSpace();
output.push(ch);
newline_after_open_brace ? print.newLine() : print.singleSpace();
};
print["}"] = function(ch) {
newline_before_close_brace ? print.newLine() : print.singleSpace();
output.push(ch);
print.newLine();
};
print._lastCharWhitespace = function() {
return whiteRe.test(output[output.length - 1]);
};
print.newLine = function(keepWhitespace) {
if (!keepWhitespace) {
print.trim();
}
if (output.length) {
output.push('\n');
}
if (basebaseIndentString) {
output.push(basebaseIndentString);
}
};
print.singleSpace = function() {
if (output.length && !print._lastCharWhitespace()) {
output.push(' ');
}
};
print.trim = function() {
while (print._lastCharWhitespace()) {
output.pop();
}
};
var output = [];
if (basebaseIndentString) {
output.push(basebaseIndentString);
}
/*_____________________--------------------_____________________*/
var insideRule = false;
var enteringConditionalGroup = false;
var top_ch = '';
var last_top_ch = '';
while (true) {
var whitespace = skipWhitespace();
var isAfterSpace = whitespace !== '';
var isAfterNewline = whitespace.indexOf('\n') !== -1;
last_top_ch = top_ch;
top_ch = ch;
if (!ch) {
break;
} else if (ch === '/' && peek() === '*') { /* css comment */
var header = lookBack("");
print.newLine();
output.push(eatComment());
print.newLine();
if (header) {
print.newLine(true);
}
} else if (ch === '/' && peek() === '/') { // single line comment
if (!isAfterNewline && last_top_ch !== '{') {
print.trim();
}
print.singleSpace();
output.push(eatComment());
print.newLine();
} else if (ch === '@') {
// pass along the space we found as a separate item
if (isAfterSpace) {
print.singleSpace();
}
output.push(ch);
// strip trailing space, if present, for hash property checks
var variableOrRule = peekString(": ,;{}()[]/='\"");
if (variableOrRule.match(/[ :]$/)) {
// we have a variable or pseudo-class, add it and insert one space before continuing
next();
variableOrRule = eatString(": ").replace(/\s$/, '');
output.push(variableOrRule);
print.singleSpace();
}
variableOrRule = variableOrRule.replace(/\s$/, '')
// might be a nesting at-rule
if (variableOrRule in css_beautify.NESTED_AT_RULE) {
nestedLevel += 1;
if (variableOrRule in css_beautify.CONDITIONAL_GROUP_RULE) {
enteringConditionalGroup = true;
}
}
} else if (ch === '{') {
if (peek(true) === '}') {
eatWhitespace();
next();
print.singleSpace();
output.push("{}");
print.newLine();
if (newline_between_rules && indentLevel === 0) {
print.newLine(true);
}
} else {
indent();
print["{"](ch);
// when entering conditional groups, only rulesets are allowed
if (enteringConditionalGroup) {
enteringConditionalGroup = false;
insideRule = (indentLevel > nestedLevel);
} else {
// otherwise, declarations are also allowed
insideRule = (indentLevel >= nestedLevel);
}
}
} else if (ch === '}') {
outdent();
print["}"](ch);
insideRule = false;
if (nestedLevel) {
nestedLevel--;
}
if (newline_between_rules && indentLevel === 0) {
print.newLine(true);
}
} else if (ch === ":") {
eatWhitespace();
if ((insideRule || enteringConditionalGroup) &&
!(lookBack("&") || foundNestedPseudoClass())) {
// 'property: value' delimiter
// which could be in a conditional group query
output.push(':');
print.singleSpace();
} else {
// sass/less parent reference don't use a space
// sass nested pseudo-class don't use a space
if (peek() === ":") {
// pseudo-element
next();
output.push("::");
} else {
// pseudo-class
output.push(':');
}
}
} else if (ch === '"' || ch === '\'') {
if (isAfterSpace) {
print.singleSpace();
}
output.push(eatString(ch));
} else if (ch === ';') {
output.push(ch);
newline_between_properties ? print.newLine() : print.singleSpace();
} else if (ch === '(') { // may be a url
if (lookBack("url")) {
output.push(ch);
eatWhitespace();
if (next()) {
if (ch !== ')' && ch !== '"' && ch !== '\'') {
output.push(eatString(')'));
} else {
pos--;
}
}
} else {
parenLevel++;
if (isAfterSpace) {
print.singleSpace();
}
output.push(ch);
eatWhitespace();
}
} else if (ch === ')') {
output.push(ch);
parenLevel--;
} else if (ch === ',') {
output.push(ch);
eatWhitespace();
if (!insideRule && selectorSeparatorNewline && parenLevel < 1) {
print.newLine();
} else {
print.singleSpace();
}
} else if (ch === ']') {
output.push(ch);
} else if (ch === '[') {
if (isAfterSpace) {
print.singleSpace();
}
output.push(ch);
} else if (ch === '=') { // no whitespace before or after
eatWhitespace()
ch = '=';
output.push(ch);
} else {
if (isAfterSpace) {
print.singleSpace();
}
output.push(ch);
}
}
var sweetCode = output.join('').replace(/[\r\n\t ]+$/, '');
// establish end_with_newline
if (end_with_newline) {
sweetCode += "\n";
}
return sweetCode;
}
// https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
css_beautify.NESTED_AT_RULE = {
"@page": true,
"@font-face": true,
"@keyframes": true,
// also in CONDITIONAL_GROUP_RULE below
"@media": true,
"@supports": true,
"@document": true
};
css_beautify.CONDITIONAL_GROUP_RULE = {
"@media": true,
"@supports": true,
"@document": true
};
/*global define */
if (typeof define === "function" && define.amd) {
// Add support for AMD ( https://github.com/amdjs/amdjs-api/wiki/AMD#defineamd-property- )
define([], function() {
return {
css_beautify: css_beautify
};
});
} else if (typeof exports !== "undefined") {
// Add support for CommonJS. Just put this file somewhere on your require.paths
// and you will be able to `var html_beautify = require("beautify").html_beautify`.
exports.css_beautify = css_beautify;
} else if (typeof window !== "undefined") {
// If we're running a web page and don't have either of the above, add our one global
window.css_beautify = css_beautify;
} else if (typeof global !== "undefined") {
// If we don't even have window, try global.
global.css_beautify = css_beautify;
}
}());

View File

@ -124,7 +124,7 @@
#sections > div:only-of-type .remove-section {
display: none;
}
#sections > div .add-section {
#sections > div > button:not(:first-of-type) {
margin-left: 0.4rem;
}
.dirty > label::before {
@ -248,10 +248,11 @@
cursor: pointer;
width: 8px;
height: 8px;
border: 8px solid transparent;
position: absolute;
right: 0.5rem;
top: 0.75rem;
background: linear-gradient(-45deg, transparent 5px, black 5px, black 6px, transparent 6.5px), linear-gradient(45deg, transparent 5px, black 5px, black 6px, transparent 6.5px);
right: 4px;
top: 4px;
background: linear-gradient(-45deg, transparent 5px, black 5px, black 6px, transparent 6.5px) no-repeat, linear-gradient(45deg, transparent 5px, black 5px, black 6px, transparent 6.5px) no-repeat;
}
.keymap-list {
@ -316,6 +317,34 @@
text-align: left;
}
/************ CSS beautifier ************/
.beautify-options {
white-space: nowrap;
font-family: monospace;
}
.beautify-options div {
float: left;
}
.beautify-options div[newline="true"] + div {
clear: left;
}
.beautify-options div[newline="true"] + div span[indent] {
padding-left: 2rem;
}
.beautify-options:after {
clear: both;
display: block;
content: " ";
height: 1rem;
}
.beautify-options span {
font-weight: bold;
}
.beautify-options select {
border: none;
background-color: rgba(0, 0, 0, 0.05);
}
/************ reponsive layouts ************/
@media(max-width:737px) {
#header {
@ -425,7 +454,7 @@
</div>
</section>
<section id="actions">
<div><button id="to-mozilla" i18n-text="styleToMozillaFormat"></button><img id="to-mozilla-help" src="help.png"></div>
<div><button id="beautify" i18n-text="styleBeautify"></button> <button id="to-mozilla" i18n-text="styleToMozillaFormat"></button><img id="to-mozilla-help" src="help.png"></div>
<div><a href="manage.html"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a></div>
<div><button id="save-button" title="Ctrl-S" i18n-text="styleSaveLabel"></button></div>
</section>

86
edit.js
View File

@ -44,6 +44,7 @@ var sectionTemplate = tHTML('\
</div>\
<button class="remove-section" i18n-text="sectionRemove"></button>\
<button class="add-section" i18n-text="sectionAdd"></button>\
<button class="beautify-section" i18n-text="styleBeautify"></button>\
</div>\
');
@ -483,6 +484,7 @@ function addSection(event, section) {
div.querySelector(".applies-to-help").addEventListener("click", showAppliesToHelp, false);
div.querySelector(".remove-section").addEventListener("click", removeSection, false);
div.querySelector(".add-section").addEventListener("click", addSection, false);
div.querySelector(".beautify-section").addEventListener("click", beautify);
var codeElement = div.querySelector(".code");
var appliesTo = div.querySelector(".applies-to-list");
@ -825,6 +827,72 @@ function gotoLintIssue(event) {
});
}
function beautify(event) {
var undoCount = 1;
if (exports.css_beautify) { // thanks to csslint's definition of 'exports'
doBeautify();
} else {
var script = document.head.appendChild(document.createElement("script"));
script.src = "beautify/beautify-css.js";
script.onload = doBeautify;
}
function doBeautify() {
var tabs = prefs.getPref("editor.indentWithTabs");
var options = prefs.getPref("editor.beautify");
options.indent_size = tabs ? 1 : prefs.getPref("editor.tabSize");
options.indent_char = tabs ? "\t" : " ";
var section = querySelectorParent(event.target, "#sections > div");
var scope = section ? [getCodeMirrorForSection(section)] : editors;
scope.forEach(function(cm) {
setTimeout(function() {
var text = cm.getValue();
var newText = exports.css_beautify(text, options);
if (newText != text) {
cm.setValue(newText);
}
}, 0);
});
showHelp(t("styleBeautify"), "<div class='beautify-options'>" +
optionHtml(".selector1,", "selector_separator_newline") +
optionHtml(".selector2,", "newline_before_open_brace") +
optionHtml("{", "newline_after_open_brace") +
optionHtml("border: none;", "newline_between_properties", true) +
optionHtml("display: block;", "newline_before_close_brace", true) +
optionHtml("}", "newline_between_rules") +
"</div>" +
"<div><button role='undo'></button></div>");
var undoButton = document.querySelector("#help-popup button[role='undo']");
undoButton.textContent = t(scope.length == 1 ? "undo" : "undoGlobal");
undoButton.addEventListener("click", function() {
scope.forEach(CodeMirror.commands.undo);
undoButton.disabled = --undoCount == 0;
});
document.querySelector(".beautify-options").addEventListener("change", function(event) {
var value = event.target.selectedIndex > 0;
options[event.target.dataset.option] = value;
prefs.setPref("editor.beautify", options);
event.target.parentNode.setAttribute("newline", value.toString());
doBeautify();
undoCount++;
undoButton.disabled = false;
});
function optionHtml(label, optionName, indent) {
var value = options[optionName];
return "<div newline='" + value.toString() + "'>" +
"<span" + (indent ? " indent" : "") + ">" + label + "</span>" +
"<select data-option='" + optionName + "'>" +
"<option" + (value ? "" : " selected") + ">&nbsp;</option>" +
"<option" + (value ? " selected" : "") + ">\\n</option>" +
"</select></div>";
}
}
}
window.addEventListener("load", init, false);
function init() {
@ -868,11 +936,20 @@ function initWithStyle(style) {
document.querySelectorAll("#sections > div").forEach(function(div) {
div.parentNode.removeChild(div);
});
(style.sections.length == 0 ? [{code: ""}] : style.sections).forEach(function(section) {
setTimeout(function() {
maximizeCodeHeight(addSection(null, section), editors.length == style.sections.length);
var queue = style.sections.length ? style.sections : [{code: ""}];
var queueStart = new Date().getTime();
// after 200ms the sections will be added asynchronously
while (new Date().getTime() - queueStart <= 200 && queue.length) {
maximizeCodeHeight(addSection(null, queue.shift()), !queue.length);
}
if (queue.length) {
setTimeout(function processQueue() {
maximizeCodeHeight(addSection(null, queue.shift()), !queue.length);
if (queue.length) {
setTimeout(processQueue, 0);
}
}, 0);
});
}
initHooks();
}
@ -883,6 +960,7 @@ function initHooks() {
});
document.getElementById("to-mozilla").addEventListener("click", showMozillaFormat, false);
document.getElementById("to-mozilla-help").addEventListener("click", showToMozillaHelp, false);
document.getElementById("beautify").addEventListener("click", beautify);
document.getElementById("save-button").addEventListener("click", save, false);
document.getElementById("sections-help").addEventListener("click", showSectionHelp, false);
document.getElementById("keyMap-help").addEventListener("click", showKeyMapHelp, false);

View File

@ -173,50 +173,51 @@ var prefs = {
"manage.onlyEnabled": false, // display only enabled styles
"manage.onlyEdited": false, // display only styles created locally
"editor.options": null, // CodeMirror.defaults.*
"editor.options": {}, // CodeMirror.defaults.*
"editor.lineWrapping": true, // word wrap
"editor.smartIndent": true, // "smart" indent
"editor.indentWithTabs": false,// smart indent with tabs
"editor.tabSize": 4, // tab width, in spaces
"editor.keyMap": navigator.appVersion.indexOf("Windows") > 0 ? "sublime" : "default",
"editor.theme": "default", // CSS theme
"editor.beautify": { // CSS beautifier
selector_separator_newline: true,
newline_before_open_brace: false,
newline_after_open_brace: true,
newline_between_properties: true,
newline_before_close_brace: true,
newline_between_rules: false,
end_with_newline: false
},
NO_DEFAULT_PREFERENCE: "No default preference for '%s'",
UNHANDLED_DATA_TYPE: "Default '%s' is of type '%s' - what should be done with it?",
getPref: function(key, ifUndefined) {
// Returns localStorage[key], ifUndefined, this[key], or undefined
// as type of ifUndefined, this[key], or localStorage[key]
if (ifUndefined === undefined) ifUndefined = this[key]; // default value
getPref: function(key, defaultValue) {
// Returns localStorage[key], defaultValue, this[key], or undefined
// as type of defaultValue, this[key], or localStorage[key]
var value = localStorage[key];
if (undefined === value) { // no user preference
if (ifUndefined === undefined) console.error(this.NO_DEFAULT_PREFERENCE, key);
return ifUndefined;
if (value === undefined) {
return defaultValue === undefined ? shallowCopy(this[key]) : defaultValue;
}
switch (typeof ifUndefined) {
switch (typeof (defaultValue === undefined ? this[key] : defaultValue)) {
case "boolean": return value.toLowerCase() === "true";
case "number": return Number(value);
case "object": return JSON.parse(value);
case "string": break;
case "undefined": console.warn(this.NO_DEFAULT_PREFERENCE, key); break;
default: console.error(UNHANDLED_DATA_TYPE, key, typeof ifUndefined);
default: console.error(UNHANDLED_DATA_TYPE, key, typeof defaultValue);
}
return value;
},
setPref: function(key, value) {
if (!(key in this)) console.warn(this.NO_DEFAULT_PREFERENCE, key);
var oldValue = this.getPref(key);
if (undefined === value || this[key] === value) {
localStorage.removeItem(key); // (deleted || default)
var oldValue = localStorage[key];
if (value === undefined || equal(value, this[key])) {
delete localStorage[key];
} else {
var strValue = ("string" === typeof value ||
undefined === value) ? value : JSON.stringify(value);
localStorage.setItem(key, strValue);
localStorage[key] = typeof value == "string" ? value : JSON.stringify(value);
}
var newValue = this.getPref(key);
if (newValue !== oldValue) {
if (!equal(value, oldValue === undefined ? this[key] : oldValue)) {
var message = {method: "prefChanged", prefName: key, value: value};
notifyAllTabs(message);
chrome.extension.sendMessage(message);
@ -257,3 +258,29 @@ function sessionStorageHash(name) {
Object.defineProperty(hash, "name", {value: name});
return hash;
}
function shallowCopy(obj) {
if (typeof obj != "object") {
return obj;
}
var copy = {};
for (var k in obj) {
copy[k] = obj[k];
}
return copy;
}
function equal(a, b) {
if (!a || !b || typeof a != "object") {
return a === b;
}
if (Object.keys(a).length != Object.keys(b).length) {
return false;
}
for (var k in a) {
if (a[k] !== b[k]) {
return false;
}
}
return true;
}