From 0ead2ef2590e193f7af7407107d6eadaa9e1a7e3 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 13 Jul 2015 20:44:46 +0300 Subject: [PATCH 1/3] Editor: paste from Mozilla format; export in styled popup --- _locales/en/messages.json | 40 +++++++- edit.html | 28 +++++- edit.js | 187 ++++++++++++++++++++++++++++++++++++-- storage.js | 16 ++-- 4 files changed, 246 insertions(+), 25 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 580ef244..76952976 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -165,6 +165,10 @@ "message": "Enable", "description": "Label for the button to enable a style" }, + "exportLabel": { + "message": "Export", + "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" + }, "findStylesForSite": { "message": "Find more styles for this site.", "description": "Text for a link that gets a list of styles for the current site" @@ -181,6 +185,26 @@ "message": "Type a command name", "description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short" }, + "importLabel": { + "message": "Import", + "description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)" + }, + "importAppendLabel": { + "message": "Append to style", + "description": "Label for the button to import a style and append to the existing sections" + }, + "importAppendTooltip": { + "message": "Append the imported style to current style", + "description": "Tooltip for the button to import a style and append to the existing sections" + }, + "importReplaceLabel": { + "message": "Overwrite style", + "description": "Label for the button to import and overwrite current style" + }, + "importReplaceTooltip": { + "message": "Discard contents of current style and overwrite it with the imported style", + "description": "Label for the button to import and overwrite current style" + }, "installUpdate": { "message": "Install update", "description": "Label for the button to install an update for a single style" @@ -322,13 +346,21 @@ "message": "Sections", "description": "Title for the style sections section" }, - "styleToMozillaFormat": { - "message": "To Mozilla format", - "description": "Label for the button that converts the code to Mozilla format" + "styleMozillaFormatHeading": { + "message": "Mozilla Format", + "description": "Heading for the section with buttons to import/export Mozilla format of the style" + }, + "styleFromMozillaFormatPrompt": { + "message": "Paste the Mozilla-format code", + "description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button" + }, + "styleToMozillaFormatTitle": { + "message": "Style in Mozilla format", + "description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page" }, "styleToMozillaFormatHelp": { "message": "The Mozilla format of the code can be used with Stylish for Firefox and can be submitted to userstyles.org.", - "description": "Help info for the button that converts the code to Mozilla format" + "description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format" }, "styleUpdate": { "message": "Are you sure you want to update '$stylename$'?", diff --git a/edit.html b/edit.html index 8542773c..32c037b1 100644 --- a/edit.html +++ b/edit.html @@ -241,7 +241,7 @@ background-color: white; box-shadow: 3px 3px 30px rgba(0, 0, 0, 0.5); padding: 0.5rem; - z-index: 9999; + z-index: 99; } #help-popup .title { font-weight: bold; @@ -282,6 +282,19 @@ padding-right: 0.5rem; } + #help-popup button[name^="import"] { + line-height: 1.5rem; + padding: 0 0.5rem; + margin: 0.5rem 0 0 0.5rem; + pointer-events: none; + opacity: 0.5; + float: right; + } + #help-popup.ready button[name^="import"] { + pointer-events: all; + opacity: 1.0; + } + /************ lint ************/ #lint { display: none; @@ -528,9 +541,16 @@
-
-
-
+
+ + + +
+
+

+ + +

diff --git a/edit.js b/edit.js index 6f6dc026..9e0e0b10 100644 --- a/edit.js +++ b/edit.js @@ -24,6 +24,8 @@ Array.prototype.rotate = function(amount) { // negative amount == rotate left return r; } +Object.defineProperty(Array.prototype, "last", {get: function() { return this[this.length - 1]; }}); + // reroute handling to nearest editor when keypress resolves to one of these commands var hotkeyRerouter = { commands: { @@ -134,13 +136,8 @@ function initCodeMirror() { "Alt-PageUp": "prevEditor" } } - mergeOptions(stylishOptions, CM.defaults); - mergeOptions(userOptions, CM.defaults); - - function mergeOptions(source, target) { - for (var key in source) target[key] = source[key]; - return target; - } + shallowMerge(stylishOptions, CM.defaults); + shallowMerge(userOptions, CM.defaults); // additional commands CM.commands.jumpToLine = jumpToLine; @@ -353,7 +350,7 @@ function indicateCodeChange(cm) { } function getSectionForCodeMirror(cm) { - return cm.getTextArea().parentNode; + return cm.display.wrapper.parentNode; } function getCodeMirrorForSection(section) { @@ -1086,6 +1083,7 @@ function initHooks() { }); document.getElementById("to-mozilla").addEventListener("click", showMozillaFormat, false); document.getElementById("to-mozilla-help").addEventListener("click", showToMozillaHelp, false); + document.getElementById("from-mozilla").addEventListener("click", fromMozillaFormat); document.getElementById("beautify").addEventListener("click", beautify); document.getElementById("save-button").addEventListener("click", save, false); document.getElementById("sections-help").addEventListener("click", showSectionHelp, false); @@ -1253,7 +1251,9 @@ function saveComplete(style) { } function showMozillaFormat() { - window.open("data:text/plain;charset=UTF-8," + encodeURIComponent(toMozillaFormat())); + var popup = showCodeMirrorPopup(t("styleToMozillaFormatTitle"), "", {readOnly: true}); + popup.codebox.setValue(toMozillaFormat()); + popup.codebox.execCommand("selectAll"); } function toMozillaFormat() { @@ -1270,6 +1270,147 @@ function toMozillaFormat() { }).join("\n\n"); } +function fromMozillaFormat() { + var popup = showCodeMirrorPopup(t("styleFromMozillaFormatPrompt"), tHTML("
\ + \ + \ +
").innerHTML); + + var contents = popup.querySelector(".contents"); + contents.insertBefore(popup.codebox.display.wrapper, contents.firstElementChild); + popup.codebox.focus(); + + popup.querySelector("[name='import-append']").addEventListener("click", doImport); + popup.querySelector("[name='import-replace']").addEventListener("click", doImport); + + popup.codebox.on("change", function() { + clearTimeout(popup.mozillaTimeout); + popup.mozillaTimeout = setTimeout(function() { + popup.classList.toggle("ready", trimNewLines(popup.codebox.getValue())); + }, 100); + }); + + function doImport() { + var replaceOldStyle = this.name == "import-replace"; + popup.querySelector(".close-icon").click(); + var mozStyle = trimNewLines(popup.codebox.getValue()); + var parser = new exports.css.Parser(), lines = mozStyle.split("\n"); + var sectionStack = [{code: "", cursor: {line: 1, col: 1}}]; + var errors = "", oldSectionCount = editors.length; + + parser.addListener("startdocument", function(e) { + var outerText = getRange(sectionStack.last.cursor, (--e.col, e)); + var gapComment = outerText.match(/(\/\*[\s\S]*?\*\/)[\s\n]*$/); + var section = {code: "", cursor: backtrackTo(this, exports.css.Tokens.LBRACE, "end")}; + // move last comment before @-moz-document inside the section + if (gapComment && !gapComment[1].match(/\/\*\s*AGENT_SHEET\s*\*\//)) { + section.code = gapComment[1] + "\n"; + outerText = trimNewLines(outerText.substring(0, gapComment.index)); + } + addContinuation(sectionStack.last, outerText); + e.functions.forEach(function(f) { + var m = f.match(/^(url|url-prefix|domain|regexp)\((['"]?)(.+?)\2?\)$/); + var aType = CssToProperty[m[1]]; + var aValue = aType != "regexps" ? m[3] : m[3].replace(/\\\\/g, "\\"); + (section[aType] = section[aType] || []).push(aValue); + }); + sectionStack.push(section); + }); + + parser.addListener("enddocument", function(e) { + var cursor = backtrackTo(this, exports.css.Tokens.RBRACE, "start"); + var section = sectionStack.pop(); + addContinuation(section, getRange(section.cursor, cursor)); + sectionStack.last.cursor = (++cursor.col, cursor); + doAddSection(section); + }); + + parser.addListener("endstylesheet", function() { + // add nonclosed (broken) outer sections except for the global one + sectionStack.slice(1).forEach(doAddSection); + + if (!replaceOldStyle) { + var lastOldCM = editors[oldSectionCount - 1]; + var lastOldSection = getSectionForCodeMirror(lastOldCM); + var addAfter = {target: lastOldSection.querySelector(".add-section")}; + + if (oldSectionCount < editors.length + && lastOldCM.getValue() == "" + && lastOldSection.querySelector(".applies-to-everything")) { + removeSection(addAfter); + oldSectionCount--; + } + } else { + var addAfter = {target: getSectionForCodeMirror(editors[0]).previousElementSibling.firstElementChild}; + } + + var globalSection = sectionStack[0]; + addContinuation(globalSection, + getRange(sectionStack.last.cursor, {line: lines.length, col: lines.last.length + 1})); + // only add global section if it contains actual code + if (globalSection.code + .replace("@namespace url(http://www.w3.org/1999/xhtml);", "") /* strip boilerplate NS */ + .replace(/\/\*[\s\S]*?\*\//g, "") /* strip comments */ + .replace(/[\s\n]/g, "")) { /* strip all whitespace including new lines */ + setCleanItem(addSection(addAfter, {code: globalSection.code}), false); + } + + delete maximizeCodeHeight.stats; + editors.forEach(function(cm, i) { + maximizeCodeHeight(getSectionForCodeMirror(cm), i == editors.length - 1); + }); + + makeSectionVisible(editors[oldSectionCount]); + editors[oldSectionCount].focus(); + if (errors) { + showHelp(t("issues"), errors); + } + }); + + parser.addListener("error", function(e) { + errors += e.line + ":" + e.col + " " + e.message.replace(/ at line \d.+$/, "") + "
"; + }); + + parser.parse(mozStyle); + + function getRange(start, end) { + if (start.line == end.line) { + return lines[start.line - 1].substring(start.col - 1, end.col - 1).trim(); + } else { + return trimNewLines(lines[start.line - 1].substr(start.col - 1) + "\n" + + lines.slice(start.line, end.line - 1).join("\n") + + "\n" + lines[end.line - 1].substring(0, end.col - 1)); + } + } + function doAddSection(section) { + if (replaceOldStyle && oldSectionCount > 0) { + oldSectionCount = 0; + editors.slice(0).reverse().forEach(function(cm) { + removeSection({target: getSectionForCodeMirror(cm).firstElementChild}); + }); + } + setCleanItem(addSection(null, section), false); + } + } + function backtrackTo(parser, tokenType, startEnd) { + var tokens = parser._tokenStream._lt; + for (var i = tokens.length - 2; i >= 0; --i) { + if (tokens[i].type == tokenType) { + return {line: tokens[i][startEnd+"Line"], col: tokens[i][startEnd+"Col"]}; + } + } + } + function trimNewLines(s) { + return s.replace(/^[\s\n]+/, "").replace(/[\s\n]+$/, ""); + } + function addContinuation(section, addendum) { + section.code = section.code && addendum + ? section.code + "\n/**************************/\n" + addendum + : section.code || addendum; + return section.code; + } +} + function showSectionHelp() { showHelp(t("styleSectionsTitle"), t("sectionHelp")); } @@ -1279,7 +1420,7 @@ function showAppliesToHelp() { } function showToMozillaHelp() { - showHelp(t("styleToMozillaFormat"), t("styleToMozillaFormatHelp")); + showHelp(t("styleMozillaFormatHeading"), t("styleToMozillaFormatHelp")); } function showKeyMapHelp() { @@ -1371,6 +1512,7 @@ function showLintHelp() { function showHelp(title, text) { var div = document.getElementById("help-popup"); + div.style.cssText = ""; div.querySelector(".contents").innerHTML = text; div.querySelector(".title").innerHTML = title; @@ -1380,15 +1522,40 @@ function showHelp(title, text) { } div.style.display = "block"; + return div; function closeHelp(e) { if (e.type == "click" || (e.keyCode == 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)) { div.style.display = ""; + document.querySelector(".contents").innerHTML = ""; document.removeEventListener("keydown", closeHelp); } } } +function showCodeMirrorPopup(title, html, options) { + var popup = showHelp(title, html); + popup.style.width = popup.style.maxWidth = "calc(100vw - 7rem)"; + + popup.codebox = CodeMirror(popup.querySelector(".contents"), shallowMerge(options, { + mode: "css", + lineNumbers: true, + lineWrapping: true, + foldGutter: true, + gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"], + matchBrackets: true, + lint: {getAnnotations: CodeMirror.lint.css, delay: 0}, + styleActiveLine: true, + theme: prefs.getPref("editor.theme"), + keyMap: prefs.getPref("editor.keyMap") + })); + popup.codebox.focus(); + popup.codebox.setSize(null, "70vh"); + popup.codebox.on("focus", function() { hotkeyRerouter.setState(false) }); + popup.codebox.on("blur", function() { hotkeyRerouter.setState(true) }); + return popup; +} + function getParams() { var params = {}; var urlParts = location.href.split("?", 2); diff --git a/storage.js b/storage.js index 3f8d2d02..5826f0f3 100644 --- a/storage.js +++ b/storage.js @@ -262,14 +262,16 @@ function sessionStorageHash(name) { } function shallowCopy(obj) { - if (typeof obj != "object") { - return obj; + return typeof obj == "object" ? shallowMerge(obj, {}) : obj; +} + +function shallowMerge(from, to) { + if (typeof from == "object" && typeof to == "object") { + for (var k in from) { + to[k] = from[k]; + } } - var copy = {}; - for (var k in obj) { - copy[k] = obj[k]; - } - return copy; + return to; } function equal(a, b) { From 5802cc4ae63c1fabefa98c1f2f7f3c09d14a1e82 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 16 Jul 2015 22:31:03 +0300 Subject: [PATCH 2/3] refactor/deduplicate "#sections > div" handling --- edit.js | 52 +++++++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/edit.js b/edit.js index 9e0e0b10..0294365a 100644 --- a/edit.js +++ b/edit.js @@ -18,6 +18,12 @@ var CssToProperty = {"url": "urls", "url-prefix": "urlPrefixes", "domain": "doma // Chrome pre-34 Element.prototype.matches = Element.prototype.matches || Element.prototype.webkitMatchesSelector; +// Chrome pre-41 polyfill +Element.prototype.closest = Element.prototype.closest || function(selector) { + for (var e = this; e && !e.matches(selector); e = e.parentElement) {} + return e; +}; + Array.prototype.rotate = function(amount) { // negative amount == rotate left var r = this.slice(-amount, this.length); Array.prototype.push.apply(r, this.slice(0, this.length - r.length)); @@ -353,13 +359,18 @@ function getSectionForCodeMirror(cm) { return cm.display.wrapper.parentNode; } +function getSectionForChild(e) { + return e.closest("#sections > div"); +} + +function getSections() { + return document.querySelectorAll("#sections > div"); +} + function getCodeMirrorForSection(section) { // #header section has no codemirror var wrapper = section.querySelector(".CodeMirror"); - if (wrapper) { - return wrapper.CodeMirror; - } - return null; + return wrapper && wrapper.CodeMirror; } // remind Chrome to repaint a previously invisible editor box by toggling any element's transform @@ -487,7 +498,7 @@ function addSection(event, section) { if (event) { var clickedSection = event.target.parentNode; sections.insertBefore(div, clickedSection.nextElementSibling); - var newIndex = document.querySelectorAll("#sections > div").indexOf(clickedSection) + 1; + var newIndex = getSections().indexOf(clickedSection) + 1; var cm = setupCodeMirror(codeElement, newIndex); makeSectionVisible(cm); cm.focus() @@ -512,7 +523,7 @@ function removeAppliesTo(event) { function removeSection(event) { var section = event.target.parentNode; - var cm = section.querySelector(".CodeMirror").CodeMirror; + var cm = getCodeMirrorForSection(section); removeAreaAndSetDirty(section); editors.splice(editors.indexOf(cm), 1); renderLintReport(); @@ -789,7 +800,7 @@ function getEditorInSight(nearbyElement) { // priority: 1. associated CM for applies-to element 2. last active if visible 3. first visible var cm; if (nearbyElement && nearbyElement.className.indexOf("applies-") >= 0) { - cm = getCodeMirrorForSection(querySelectorParent(nearbyElement, "#sections > div")); + cm = getCodeMirrorForSection(getSectionForChild(nearbyElement)); } else { cm = editors.lastActive; } @@ -922,11 +933,11 @@ function resizeLintReport(event, content) { } function gotoLintIssue(event) { - var issue = querySelectorParent(event.target, "tr"); + var issue = event.target.closest("tr"); if (!issue) { return; } - var block = querySelectorParent(issue, "table"); + var block = issue.closest("table"); makeSectionVisible(block.cm); block.cm.focus(); block.cm.setSelection({ @@ -949,7 +960,7 @@ function beautify(event) { options.indent_size = tabs ? 1 : prefs.getPref("editor.tabSize"); options.indent_char = tabs ? "\t" : " "; - var section = querySelectorParent(event.target, "#sections > div"); + var section = getSectionForChild(event.target); var scope = section ? [getCodeMirrorForSection(section)] : editors; showHelp(t("styleBeautify"), "
" + @@ -1052,9 +1063,7 @@ function initWithStyle(style) { document.getElementById("enabled").checked = style.enabled == "true"; document.getElementById("url").href = style.url; // if this was done in response to an update, we need to clear existing sections - document.querySelectorAll("#sections > div").forEach(function(div) { - div.parentNode.removeChild(div); - }); + getSections().forEach(function(div) { div.remove(); }); var queue = style.sections.length ? style.sections : [{code: ""}]; var queueStart = new Date().getTime(); // after 100ms the sections will be added asynchronously @@ -1203,16 +1212,16 @@ function save() { id: styleId, name: name, enabled: enabled, - sections: getSections() + sections: getSectionsHashes() }; chrome.extension.sendMessage(request, saveComplete); } -function getSections() { +function getSectionsHashes() { var sections = []; - document.querySelectorAll("#sections > div").forEach(function(div) { + getSections().forEach(function(div) { var meta = getMeta(div); - var code = div.querySelector(".CodeMirror").CodeMirror.getValue(); + var code = getCodeMirrorForSection(div).getValue(); if (/^\s*$/.test(code) && Object.keys(meta).length == 0) { return; } @@ -1257,7 +1266,7 @@ function showMozillaFormat() { } function toMozillaFormat() { - return getSections().map(function(section) { + return getSectionsHashes().map(function(section) { var cssMds = []; for (var i in propertyToCss) { if (section[i]) { @@ -1589,13 +1598,6 @@ chrome.extension.onMessage.addListener(function(request, sender, sendResponse) { } }); -function querySelectorParent(node, selector) { - var parent = node.parentNode; - while (parent && parent.matches && !parent.matches(selector)) - parent = parent.parentNode; - return parent.matches ? parent : null; // null for the root document.DOCUMENT_NODE -} - function stringAsRegExp(s, flags) { return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=*!|]/g, "\\$&"), flags); } From c2cb4527839508f4a97483a3b92fb35f296145e2 Mon Sep 17 00:00:00 2001 From: tophf Date: Wed, 15 Jul 2015 17:17:24 +0300 Subject: [PATCH 3/3] fix CSSLint's parser-lib @document{} --- csslint/csslint.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/csslint/csslint.js b/csslint/csslint.js index f392ff6f..0a31bb19 100644 --- a/csslint/csslint.js +++ b/csslint/csslint.js @@ -2061,21 +2061,24 @@ Parser.prototype = function(){ col: token.startCol }); - while(true) { - if (tokenStream.peek() == Tokens.PAGE_SYM){ - this._page(); - } else if (tokenStream.peek() == Tokens.FONT_FACE_SYM){ - this._font_face(); - } else if (tokenStream.peek() == Tokens.VIEWPORT_SYM){ - this._viewport(); - } else if (tokenStream.peek() == Tokens.MEDIA_SYM){ - this._media(); - } else if (!this._ruleset()){ - break; + var ok = true; + while(ok) { + switch (tokenStream.peek()) { + case Tokens.PAGE_SYM: this._page(); break; + case Tokens.FONT_FACE_SYM: this._font_face(); break; + case Tokens.VIEWPORT_SYM: this._viewport(); break; + case Tokens.MEDIA_SYM: this._media(); break; + case Tokens.KEYFRAMES_SYM: this._keyframes(); break; + case Tokens.DOCUMENT_SYM: this._document(); break; + default: + if (!this._ruleset()) { + ok = false; + } } } tokenStream.mustMatch(Tokens.RBRACE); + token = tokenStream.token(); this._readWhitespace(); this.fire({ @@ -3259,6 +3262,7 @@ Parser.prototype = function(){ this._readWhitespace(); tokenStream.mustMatch(Tokens.RBRACE); + this._readWhitespace(); },