michael@0: /* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const cssTokenizer = require("devtools/sourceeditor/css-tokenizer"); michael@0: michael@0: /** michael@0: * Returns the string enclosed in quotes michael@0: */ michael@0: function quoteString(string) { michael@0: let hasDoubleQuotes = string.contains('"'); michael@0: let hasSingleQuotes = string.contains("'"); michael@0: michael@0: if (hasDoubleQuotes && !hasSingleQuotes) { michael@0: // In this case, no escaping required, just enclose in single-quotes michael@0: return "'" + string + "'"; michael@0: } michael@0: michael@0: // In all other cases, enclose in double-quotes, and escape any double-quote michael@0: // that may be in the string michael@0: return '"' + string.replace(/"/g, '\"') + '"'; michael@0: } michael@0: michael@0: /** michael@0: * Returns an array of CSS declarations given an string. michael@0: * For example, parseDeclarations("width: 1px; height: 1px") would return michael@0: * [{name:"width", value: "1px"}, {name: "height", "value": "1px"}] michael@0: * michael@0: * The input string is assumed to only contain declarations so { and } characters michael@0: * will be treated as part of either the property or value, depending where it's michael@0: * found. michael@0: * michael@0: * @param {string} inputString michael@0: * An input string of CSS michael@0: * @return {Array} an array of objects with the following signature: michael@0: * [{"name": string, "value": string, "priority": string}, ...] michael@0: */ michael@0: function parseDeclarations(inputString) { michael@0: let tokens = cssTokenizer(inputString); michael@0: michael@0: let declarations = [{name: "", value: "", priority: ""}]; michael@0: michael@0: let current = "", hasBang = false, lastProp; michael@0: for (let token of tokens) { michael@0: lastProp = declarations[declarations.length - 1]; michael@0: michael@0: if (token.tokenType === ":") { michael@0: if (!lastProp.name) { michael@0: // Set the current declaration name if there's no name yet michael@0: lastProp.name = current.trim(); michael@0: current = ""; michael@0: hasBang = false; michael@0: } else { michael@0: // Otherwise, just append ':' to the current value (declaration value michael@0: // with colons) michael@0: current += ":"; michael@0: } michael@0: } else if (token.tokenType === ";") { michael@0: lastProp.value = current.trim(); michael@0: current = ""; michael@0: hasBang = false; michael@0: declarations.push({name: "", value: "", priority: ""}); michael@0: } else { michael@0: switch(token.tokenType) { michael@0: case "IDENT": michael@0: if (token.value === "important" && hasBang) { michael@0: lastProp.priority = "important"; michael@0: hasBang = false; michael@0: } else { michael@0: if (hasBang) { michael@0: current += "!"; michael@0: } michael@0: current += token.value; michael@0: } michael@0: break; michael@0: case "WHITESPACE": michael@0: current += " "; michael@0: break; michael@0: case "DIMENSION": michael@0: current += token.repr; michael@0: break; michael@0: case "HASH": michael@0: current += "#" + token.value; michael@0: break; michael@0: case "URL": michael@0: current += "url(" + quoteString(token.value) + ")"; michael@0: break; michael@0: case "FUNCTION": michael@0: current += token.value + "("; michael@0: break; michael@0: case ")": michael@0: current += token.tokenType; michael@0: break; michael@0: case "EOF": michael@0: break; michael@0: case "DELIM": michael@0: if (token.value === "!") { michael@0: hasBang = true; michael@0: } else { michael@0: current += token.value; michael@0: } michael@0: break; michael@0: case "STRING": michael@0: current += quoteString(token.value); michael@0: break; michael@0: case "{": michael@0: case "}": michael@0: current += token.tokenType; michael@0: break; michael@0: default: michael@0: current += token.value; michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Handle whatever trailing properties or values might still be there michael@0: if (current) { michael@0: if (!lastProp.name) { michael@0: // Trailing property found, e.g. p1:v1;p2:v2;p3 michael@0: lastProp.name = current.trim(); michael@0: } else { michael@0: // Trailing value found, i.e. value without an ending ; michael@0: lastProp.value += current.trim(); michael@0: } michael@0: } michael@0: michael@0: // Remove declarations that have neither a name nor a value michael@0: declarations = declarations.filter(prop => prop.name || prop.value); michael@0: michael@0: return declarations; michael@0: }; michael@0: exports.parseDeclarations = parseDeclarations; michael@0: michael@0: /** michael@0: * Expects a single CSS value to be passed as the input and parses the value michael@0: * and priority. michael@0: * michael@0: * @param {string} value The value from the text editor. michael@0: * @return {object} an object with 'value' and 'priority' properties. michael@0: */ michael@0: function parseSingleValue(value) { michael@0: let declaration = parseDeclarations("a: " + value + ";")[0]; michael@0: return { michael@0: value: declaration ? declaration.value : "", michael@0: priority: declaration ? declaration.priority : "" michael@0: }; michael@0: }; michael@0: exports.parseSingleValue = parseSingleValue;