2013-10-18 14:01:20 +00:00
|
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
2016-05-19 14:35:45 +00:00
|
|
|
|
const {Cc, Ci} = require("chrome");
|
2016-05-05 18:21:17 +00:00
|
|
|
|
const {angleUtils} = require("devtools/client/shared/css-angle");
|
2016-04-28 14:41:40 +00:00
|
|
|
|
const {colorUtils} = require("devtools/client/shared/css-color");
|
2016-04-29 17:21:22 +00:00
|
|
|
|
const {getCSSLexer} = require("devtools/shared/css-lexer");
|
2016-03-28 19:01:32 +00:00
|
|
|
|
const EventEmitter = require("devtools/shared/event-emitter");
|
2013-10-18 14:01:20 +00:00
|
|
|
|
|
2013-11-11 15:17:41 +00:00
|
|
|
|
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
|
|
|
|
|
2015-06-08 16:26:00 +00:00
|
|
|
|
const BEZIER_KEYWORDS = ["linear", "ease-in-out", "ease-in", "ease-out",
|
|
|
|
|
"ease"];
|
2014-12-26 11:40:35 +00:00
|
|
|
|
|
2015-05-19 15:56:16 +00:00
|
|
|
|
// Functions that accept a color argument.
|
|
|
|
|
const COLOR_TAKING_FUNCTIONS = ["linear-gradient",
|
2015-07-15 14:37:22 +00:00
|
|
|
|
"-moz-linear-gradient",
|
2015-05-19 15:56:16 +00:00
|
|
|
|
"repeating-linear-gradient",
|
2015-07-15 14:37:22 +00:00
|
|
|
|
"-moz-repeating-linear-gradient",
|
2015-05-19 15:56:16 +00:00
|
|
|
|
"radial-gradient",
|
2015-07-15 14:37:22 +00:00
|
|
|
|
"-moz-radial-gradient",
|
2015-05-19 15:56:16 +00:00
|
|
|
|
"repeating-radial-gradient",
|
2015-07-15 14:37:22 +00:00
|
|
|
|
"-moz-repeating-radial-gradient",
|
2015-06-08 16:26:00 +00:00
|
|
|
|
"drop-shadow"];
|
2013-10-18 14:01:20 +00:00
|
|
|
|
|
2016-03-08 22:04:54 +00:00
|
|
|
|
// Functions that accept an angle argument.
|
|
|
|
|
const ANGLE_TAKING_FUNCTIONS = ["linear-gradient",
|
|
|
|
|
"-moz-linear-gradient",
|
|
|
|
|
"repeating-linear-gradient",
|
|
|
|
|
"-moz-repeating-linear-gradient",
|
|
|
|
|
"rotate",
|
|
|
|
|
"rotateX",
|
|
|
|
|
"rotateY",
|
|
|
|
|
"rotateZ",
|
|
|
|
|
"rotate3d",
|
|
|
|
|
"skew",
|
|
|
|
|
"skewX",
|
|
|
|
|
"skewY",
|
|
|
|
|
"hue-rotate"];
|
|
|
|
|
|
2016-05-17 18:25:54 +00:00
|
|
|
|
loader.lazyGetter(this, "DOMUtils", function () {
|
2013-10-18 14:01:20 +00:00
|
|
|
|
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* This module is used to process text for output by developer tools. This means
|
|
|
|
|
* linking JS files with the debugger, CSS files with the style editor, JS
|
|
|
|
|
* functions with the debugger, placing color swatches next to colors and
|
|
|
|
|
* adding doorhanger previews where possible (images, angles, lengths,
|
|
|
|
|
* border radius, cubic-bezier etc.).
|
|
|
|
|
*
|
|
|
|
|
* Usage:
|
2015-08-06 12:38:10 +00:00
|
|
|
|
* const {require} =
|
2015-10-13 23:18:43 +00:00
|
|
|
|
* Cu.import("resource://devtools/shared/Loader.jsm", {});
|
2015-11-11 07:58:00 +00:00
|
|
|
|
* const {OutputParser} = require("devtools/client/shared/output-parser");
|
2013-10-18 14:01:20 +00:00
|
|
|
|
*
|
2015-07-28 13:36:00 +00:00
|
|
|
|
* let parser = new OutputParser(document);
|
2013-10-18 14:01:20 +00:00
|
|
|
|
*
|
|
|
|
|
* parser.parseCssProperty("color", "red"); // Returns document fragment.
|
|
|
|
|
*/
|
2015-07-28 13:36:00 +00:00
|
|
|
|
function OutputParser(document) {
|
2013-10-18 14:01:20 +00:00
|
|
|
|
this.parsed = [];
|
2015-07-28 13:36:00 +00:00
|
|
|
|
this.doc = document;
|
2015-04-17 10:53:06 +00:00
|
|
|
|
this.colorSwatches = new WeakMap();
|
2016-03-08 22:04:54 +00:00
|
|
|
|
this.angleSwatches = new WeakMap();
|
|
|
|
|
this._onColorSwatchMouseDown = this._onColorSwatchMouseDown.bind(this);
|
|
|
|
|
this._onAngleSwatchMouseDown = this._onAngleSwatchMouseDown.bind(this);
|
2013-10-18 14:01:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
exports.OutputParser = OutputParser;
|
|
|
|
|
|
|
|
|
|
OutputParser.prototype = {
|
|
|
|
|
/**
|
|
|
|
|
* Parse a CSS property value given a property name.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} name
|
|
|
|
|
* CSS Property Name
|
|
|
|
|
* @param {String} value
|
|
|
|
|
* CSS Property value
|
|
|
|
|
* @param {Object} [options]
|
|
|
|
|
* Options object. For valid options and default values see
|
|
|
|
|
* _mergeOptions().
|
|
|
|
|
* @return {DocumentFragment}
|
|
|
|
|
* A document fragment containing color swatches etc.
|
|
|
|
|
*/
|
2016-05-17 18:25:54 +00:00
|
|
|
|
parseCssProperty: function (name, value, options = {}) {
|
2013-10-18 14:01:20 +00:00
|
|
|
|
options = this._mergeOptions(options);
|
|
|
|
|
|
2015-05-11 10:33:00 +00:00
|
|
|
|
options.expectCubicBezier =
|
|
|
|
|
safeCssPropertySupportsType(name, DOMUtils.TYPE_TIMING_FUNCTION);
|
2015-04-09 09:00:42 +00:00
|
|
|
|
options.expectFilter = name === "filter";
|
2015-05-11 10:33:00 +00:00
|
|
|
|
options.supportsColor =
|
|
|
|
|
safeCssPropertySupportsType(name, DOMUtils.TYPE_COLOR) ||
|
|
|
|
|
safeCssPropertySupportsType(name, DOMUtils.TYPE_GRADIENT);
|
2015-04-09 09:00:42 +00:00
|
|
|
|
|
2015-10-26 05:22:00 +00:00
|
|
|
|
// The filter property is special in that we want to show the
|
|
|
|
|
// swatch even if the value is invalid, because this way the user
|
|
|
|
|
// can easily use the editor to fix it.
|
|
|
|
|
if (options.expectFilter || this._cssPropertySupportsValue(name, value)) {
|
2013-11-11 15:17:41 +00:00
|
|
|
|
return this._parse(value, options);
|
|
|
|
|
}
|
|
|
|
|
this._appendTextNode(value);
|
2013-10-18 14:01:20 +00:00
|
|
|
|
|
2013-11-11 15:17:41 +00:00
|
|
|
|
return this._toDOM();
|
2013-10-18 14:01:20 +00:00
|
|
|
|
},
|
|
|
|
|
|
2014-02-05 10:53:46 +00:00
|
|
|
|
/**
|
2015-05-19 15:56:16 +00:00
|
|
|
|
* Given an initial FUNCTION token, read tokens from |tokenStream|
|
|
|
|
|
* and collect all the (non-comment) text. Return the collected
|
|
|
|
|
* text. The function token and the close paren are included in the
|
|
|
|
|
* result.
|
|
|
|
|
*
|
|
|
|
|
* @param {CSSToken} initialToken
|
|
|
|
|
* The FUNCTION token.
|
|
|
|
|
* @param {String} text
|
|
|
|
|
* The original CSS text.
|
|
|
|
|
* @param {CSSLexer} tokenStream
|
|
|
|
|
* The token stream from which to read.
|
|
|
|
|
* @return {String}
|
|
|
|
|
* The text of body of the function call.
|
2014-02-05 10:53:46 +00:00
|
|
|
|
*/
|
2016-05-17 18:25:54 +00:00
|
|
|
|
_collectFunctionText: function (initialToken, text, tokenStream) {
|
2015-05-19 15:56:16 +00:00
|
|
|
|
let result = text.substring(initialToken.startOffset,
|
|
|
|
|
initialToken.endOffset);
|
|
|
|
|
let depth = 1;
|
|
|
|
|
while (depth > 0) {
|
|
|
|
|
let token = tokenStream.nextToken();
|
|
|
|
|
if (!token) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (token.tokenType === "comment") {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
result += text.substring(token.startOffset, token.endOffset);
|
|
|
|
|
if (token.tokenType === "symbol") {
|
|
|
|
|
if (token.text === "(") {
|
|
|
|
|
++depth;
|
|
|
|
|
} else if (token.text === ")") {
|
|
|
|
|
--depth;
|
|
|
|
|
}
|
|
|
|
|
} else if (token.tokenType === "function") {
|
|
|
|
|
++depth;
|
|
|
|
|
}
|
2014-02-05 10:53:46 +00:00
|
|
|
|
}
|
2015-05-19 15:56:16 +00:00
|
|
|
|
return result;
|
2014-02-05 10:53:46 +00:00
|
|
|
|
},
|
|
|
|
|
|
2013-10-18 14:01:20 +00:00
|
|
|
|
/**
|
|
|
|
|
* Parse a string.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} text
|
|
|
|
|
* Text to parse.
|
|
|
|
|
* @param {Object} [options]
|
|
|
|
|
* Options object. For valid options and default values see
|
|
|
|
|
* _mergeOptions().
|
|
|
|
|
* @return {DocumentFragment}
|
2013-11-11 15:17:41 +00:00
|
|
|
|
* A document fragment.
|
2013-10-18 14:01:20 +00:00
|
|
|
|
*/
|
2016-05-17 18:25:54 +00:00
|
|
|
|
_parse: function (text, options = {}) {
|
2013-10-18 14:01:20 +00:00
|
|
|
|
text = text.trim();
|
|
|
|
|
this.parsed.length = 0;
|
2015-05-17 23:54:49 +00:00
|
|
|
|
|
2016-04-29 17:21:22 +00:00
|
|
|
|
let tokenStream = getCSSLexer(text);
|
2015-06-08 16:26:00 +00:00
|
|
|
|
let parenDepth = 0;
|
|
|
|
|
let outerMostFunctionTakesColor = false;
|
2013-11-18 16:07:24 +00:00
|
|
|
|
|
2016-05-17 18:25:54 +00:00
|
|
|
|
let colorOK = function () {
|
2015-06-08 16:26:00 +00:00
|
|
|
|
return options.supportsColor ||
|
|
|
|
|
(options.expectFilter && parenDepth === 1 &&
|
|
|
|
|
outerMostFunctionTakesColor);
|
|
|
|
|
};
|
2015-05-17 23:54:49 +00:00
|
|
|
|
|
2016-05-17 18:25:54 +00:00
|
|
|
|
let angleOK = function (angle) {
|
2016-03-08 22:04:54 +00:00
|
|
|
|
return /^-?\d+\.?\d*(deg|rad|grad|turn)$/gi.test(angle);
|
|
|
|
|
};
|
|
|
|
|
|
2015-06-08 16:26:00 +00:00
|
|
|
|
while (true) {
|
|
|
|
|
let token = tokenStream.nextToken();
|
|
|
|
|
if (!token) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (token.tokenType === "comment") {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2015-05-19 15:56:16 +00:00
|
|
|
|
|
2015-06-08 16:26:00 +00:00
|
|
|
|
switch (token.tokenType) {
|
|
|
|
|
case "function": {
|
2016-03-08 22:04:54 +00:00
|
|
|
|
if (COLOR_TAKING_FUNCTIONS.includes(token.text) ||
|
|
|
|
|
ANGLE_TAKING_FUNCTIONS.includes(token.text)) {
|
|
|
|
|
// The function can accept a color or an angle argument, and we know
|
|
|
|
|
// it isn't special in some other way. So, we let it
|
|
|
|
|
// through to the ordinary parsing loop so that the value
|
2015-06-08 16:26:00 +00:00
|
|
|
|
// can be handled in a single place.
|
|
|
|
|
this._appendTextNode(text.substring(token.startOffset,
|
|
|
|
|
token.endOffset));
|
|
|
|
|
if (parenDepth === 0) {
|
2016-03-08 22:04:54 +00:00
|
|
|
|
outerMostFunctionTakesColor = COLOR_TAKING_FUNCTIONS.includes(
|
|
|
|
|
token.text);
|
2015-06-08 16:26:00 +00:00
|
|
|
|
}
|
|
|
|
|
++parenDepth;
|
|
|
|
|
} else {
|
|
|
|
|
let functionText = this._collectFunctionText(token, text,
|
|
|
|
|
tokenStream);
|
|
|
|
|
|
|
|
|
|
if (options.expectCubicBezier && token.text === "cubic-bezier") {
|
|
|
|
|
this._appendCubicBezier(functionText, options);
|
2016-04-28 14:46:19 +00:00
|
|
|
|
} else if (colorOK() && colorUtils.isValidCSSColor(functionText)) {
|
2015-06-08 16:26:00 +00:00
|
|
|
|
this._appendColor(functionText, options);
|
2015-05-19 15:56:16 +00:00
|
|
|
|
} else {
|
2015-06-08 16:26:00 +00:00
|
|
|
|
this._appendTextNode(functionText);
|
2015-05-19 15:56:16 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2015-06-08 16:26:00 +00:00
|
|
|
|
break;
|
|
|
|
|
}
|
2015-05-19 15:56:16 +00:00
|
|
|
|
|
2015-06-08 16:26:00 +00:00
|
|
|
|
case "ident":
|
|
|
|
|
if (options.expectCubicBezier &&
|
|
|
|
|
BEZIER_KEYWORDS.indexOf(token.text) >= 0) {
|
|
|
|
|
this._appendCubicBezier(token.text, options);
|
2016-04-28 14:46:19 +00:00
|
|
|
|
} else if (colorOK() && colorUtils.isValidCSSColor(token.text)) {
|
2015-06-08 16:26:00 +00:00
|
|
|
|
this._appendColor(token.text, options);
|
2016-03-08 22:04:54 +00:00
|
|
|
|
} else if (angleOK(token.text)) {
|
|
|
|
|
this._appendAngle(token.text, options);
|
2015-06-08 16:26:00 +00:00
|
|
|
|
} else {
|
2015-05-19 15:56:16 +00:00
|
|
|
|
this._appendTextNode(text.substring(token.startOffset,
|
|
|
|
|
token.endOffset));
|
2015-06-08 16:26:00 +00:00
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case "id":
|
|
|
|
|
case "hash": {
|
|
|
|
|
let original = text.substring(token.startOffset, token.endOffset);
|
2016-04-28 14:46:19 +00:00
|
|
|
|
if (colorOK() && colorUtils.isValidCSSColor(original)) {
|
2015-06-08 16:26:00 +00:00
|
|
|
|
this._appendColor(original, options);
|
|
|
|
|
} else {
|
|
|
|
|
this._appendTextNode(original);
|
|
|
|
|
}
|
|
|
|
|
break;
|
2015-05-19 15:56:16 +00:00
|
|
|
|
}
|
2016-03-08 22:04:54 +00:00
|
|
|
|
case "dimension":
|
|
|
|
|
let value = text.substring(token.startOffset, token.endOffset);
|
|
|
|
|
if (angleOK(value)) {
|
|
|
|
|
this._appendAngle(value, options);
|
|
|
|
|
} else {
|
|
|
|
|
this._appendTextNode(value);
|
|
|
|
|
}
|
|
|
|
|
break;
|
2015-06-08 16:26:00 +00:00
|
|
|
|
case "url":
|
|
|
|
|
case "bad_url":
|
|
|
|
|
this._appendURL(text.substring(token.startOffset, token.endOffset),
|
|
|
|
|
token.text, options);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case "symbol":
|
|
|
|
|
if (token.text === "(") {
|
|
|
|
|
++parenDepth;
|
2016-03-08 22:04:54 +00:00
|
|
|
|
} else if (token.text === ")") {
|
2015-06-08 16:26:00 +00:00
|
|
|
|
--parenDepth;
|
2016-03-08 22:04:54 +00:00
|
|
|
|
if (parenDepth === 0) {
|
|
|
|
|
outerMostFunctionTakesColor = false;
|
|
|
|
|
}
|
2015-06-08 16:26:00 +00:00
|
|
|
|
}
|
|
|
|
|
// falls through
|
|
|
|
|
default:
|
2016-03-08 22:04:54 +00:00
|
|
|
|
this._appendTextNode(
|
|
|
|
|
text.substring(token.startOffset, token.endOffset));
|
2015-06-08 16:26:00 +00:00
|
|
|
|
break;
|
2013-11-05 17:00:13 +00:00
|
|
|
|
}
|
2013-10-18 14:01:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
2015-06-08 16:26:00 +00:00
|
|
|
|
let result = this._toDOM();
|
|
|
|
|
|
|
|
|
|
if (options.expectFilter && !options.filterSwatch) {
|
|
|
|
|
result = this._wrapFilter(text, options, result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
2013-10-18 14:01:20 +00:00
|
|
|
|
},
|
|
|
|
|
|
2014-07-09 21:50:18 +00:00
|
|
|
|
/**
|
|
|
|
|
* Append a cubic-bezier timing function value to the output
|
|
|
|
|
*
|
|
|
|
|
* @param {String} bezier
|
|
|
|
|
* The cubic-bezier timing function
|
|
|
|
|
* @param {Object} options
|
|
|
|
|
* Options object. For valid options and default values see
|
|
|
|
|
* _mergeOptions()
|
|
|
|
|
*/
|
2016-05-17 18:25:54 +00:00
|
|
|
|
_appendCubicBezier: function (bezier, options) {
|
2014-07-09 21:50:18 +00:00
|
|
|
|
let container = this._createNode("span", {
|
2016-03-08 22:04:54 +00:00
|
|
|
|
"data-bezier": bezier
|
2014-07-09 21:50:18 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (options.bezierSwatchClass) {
|
|
|
|
|
let swatch = this._createNode("span", {
|
|
|
|
|
class: options.bezierSwatchClass
|
|
|
|
|
});
|
|
|
|
|
container.appendChild(swatch);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let value = this._createNode("span", {
|
|
|
|
|
class: options.bezierClass
|
|
|
|
|
}, bezier);
|
|
|
|
|
|
|
|
|
|
container.appendChild(value);
|
|
|
|
|
this.parsed.push(container);
|
|
|
|
|
},
|
|
|
|
|
|
2016-03-08 22:04:54 +00:00
|
|
|
|
/**
|
|
|
|
|
* Append a angle value to the output
|
|
|
|
|
*
|
|
|
|
|
* @param {String} angle
|
|
|
|
|
* angle to append
|
|
|
|
|
* @param {Object} options
|
|
|
|
|
* Options object. For valid options and default values see
|
|
|
|
|
* _mergeOptions()
|
|
|
|
|
*/
|
2016-05-17 18:25:54 +00:00
|
|
|
|
_appendAngle: function (angle, options) {
|
2016-03-08 22:04:54 +00:00
|
|
|
|
let angleObj = new angleUtils.CssAngle(angle);
|
|
|
|
|
let container = this._createNode("span", {
|
|
|
|
|
"data-angle": angle
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (options.angleSwatchClass) {
|
|
|
|
|
let swatch = this._createNode("span", {
|
|
|
|
|
class: options.angleSwatchClass
|
|
|
|
|
});
|
|
|
|
|
this.angleSwatches.set(swatch, angleObj);
|
|
|
|
|
swatch.addEventListener("mousedown", this._onAngleSwatchMouseDown, false);
|
|
|
|
|
|
|
|
|
|
// Add click listener to stop event propagation when shift key is pressed
|
|
|
|
|
// in order to prevent the value input to be focused.
|
|
|
|
|
// Bug 711942 will add a tooltip to edit angle values and we should
|
|
|
|
|
// be able to move this listener to Tooltip.js when it'll be implemented.
|
2016-05-17 18:25:54 +00:00
|
|
|
|
swatch.addEventListener("click", function (event) {
|
2016-03-08 22:04:54 +00:00
|
|
|
|
if (event.shiftKey) {
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
}
|
|
|
|
|
}, false);
|
2016-03-28 19:01:32 +00:00
|
|
|
|
EventEmitter.decorate(swatch);
|
2016-03-08 22:04:54 +00:00
|
|
|
|
container.appendChild(swatch);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let value = this._createNode("span", {
|
|
|
|
|
class: options.angleClass
|
|
|
|
|
}, angle);
|
|
|
|
|
|
|
|
|
|
container.appendChild(value);
|
|
|
|
|
this.parsed.push(container);
|
|
|
|
|
},
|
|
|
|
|
|
2013-10-18 14:01:20 +00:00
|
|
|
|
/**
|
|
|
|
|
* Check if a CSS property supports a specific value.
|
|
|
|
|
*
|
2013-11-11 15:17:41 +00:00
|
|
|
|
* @param {String} name
|
2013-10-18 14:01:20 +00:00
|
|
|
|
* CSS Property name to check
|
2013-11-11 15:17:41 +00:00
|
|
|
|
* @param {String} value
|
2013-10-18 14:01:20 +00:00
|
|
|
|
* CSS Property value to check
|
|
|
|
|
*/
|
2016-05-17 18:25:54 +00:00
|
|
|
|
_cssPropertySupportsValue: function (name, value) {
|
2014-12-17 13:43:30 +00:00
|
|
|
|
return DOMUtils.cssPropertyIsValid(name, value);
|
2013-10-18 14:01:20 +00:00
|
|
|
|
},
|
|
|
|
|
|
2014-01-30 15:04:47 +00:00
|
|
|
|
/**
|
|
|
|
|
* Tests if a given colorObject output by CssColor is valid for parsing.
|
|
|
|
|
* Valid means it's really a color, not any of the CssColor SPECIAL_VALUES
|
|
|
|
|
* except transparent
|
|
|
|
|
*/
|
2016-05-17 18:25:54 +00:00
|
|
|
|
_isValidColor: function (colorObj) {
|
2014-01-30 15:04:47 +00:00
|
|
|
|
return colorObj.valid &&
|
|
|
|
|
(!colorObj.specialValue || colorObj.specialValue === "transparent");
|
|
|
|
|
},
|
|
|
|
|
|
2013-10-18 14:01:20 +00:00
|
|
|
|
/**
|
|
|
|
|
* Append a color to the output.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} color
|
|
|
|
|
* Color to append
|
|
|
|
|
* @param {Object} [options]
|
|
|
|
|
* Options object. For valid options and default values see
|
|
|
|
|
* _mergeOptions().
|
|
|
|
|
*/
|
2016-05-17 18:25:54 +00:00
|
|
|
|
_appendColor: function (color, options = {}) {
|
2013-11-11 15:17:41 +00:00
|
|
|
|
let colorObj = new colorUtils.CssColor(color);
|
|
|
|
|
|
2014-01-30 15:04:47 +00:00
|
|
|
|
if (this._isValidColor(colorObj)) {
|
2014-06-12 10:02:00 +00:00
|
|
|
|
let container = this._createNode("span", {
|
2016-05-17 18:25:54 +00:00
|
|
|
|
"data-color": color
|
2014-06-12 10:02:00 +00:00
|
|
|
|
});
|
|
|
|
|
|
2013-10-18 14:01:20 +00:00
|
|
|
|
if (options.colorSwatchClass) {
|
2014-06-12 10:02:00 +00:00
|
|
|
|
let swatch = this._createNode("span", {
|
2013-10-18 14:01:20 +00:00
|
|
|
|
class: options.colorSwatchClass,
|
|
|
|
|
style: "background-color:" + color
|
|
|
|
|
});
|
2015-04-17 10:53:06 +00:00
|
|
|
|
this.colorSwatches.set(swatch, colorObj);
|
2016-05-19 14:35:45 +00:00
|
|
|
|
swatch.addEventListener("mousedown", this._onColorSwatchMouseDown,
|
|
|
|
|
false);
|
2016-03-28 19:01:32 +00:00
|
|
|
|
EventEmitter.decorate(swatch);
|
2014-06-12 10:02:00 +00:00
|
|
|
|
container.appendChild(swatch);
|
2013-10-18 14:01:20 +00:00
|
|
|
|
}
|
2014-06-12 10:02:00 +00:00
|
|
|
|
|
2013-10-18 14:01:20 +00:00
|
|
|
|
if (options.defaultColorType) {
|
2013-11-11 15:17:41 +00:00
|
|
|
|
color = colorObj.toString();
|
2015-04-17 10:53:06 +00:00
|
|
|
|
container.dataset.color = color;
|
2013-10-18 14:01:20 +00:00
|
|
|
|
}
|
2014-06-12 10:02:00 +00:00
|
|
|
|
|
|
|
|
|
let value = this._createNode("span", {
|
2013-12-03 13:45:29 +00:00
|
|
|
|
class: options.colorClass
|
|
|
|
|
}, color);
|
2014-06-12 10:02:00 +00:00
|
|
|
|
|
|
|
|
|
container.appendChild(value);
|
|
|
|
|
this.parsed.push(container);
|
2015-09-03 09:46:00 +00:00
|
|
|
|
} else {
|
|
|
|
|
this._appendTextNode(color);
|
2013-10-18 14:01:20 +00:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2015-06-08 16:26:00 +00:00
|
|
|
|
/**
|
|
|
|
|
* Wrap some existing nodes in a filter editor.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} filters
|
|
|
|
|
* The full text of the "filter" property.
|
|
|
|
|
* @param {object} options
|
|
|
|
|
* The options object passed to parseCssProperty().
|
|
|
|
|
* @param {object} nodes
|
|
|
|
|
* Nodes created by _toDOM().
|
|
|
|
|
*
|
|
|
|
|
* @returns {object}
|
|
|
|
|
* A new node that supplies a filter swatch and that wraps |nodes|.
|
|
|
|
|
*/
|
2016-05-17 18:25:54 +00:00
|
|
|
|
_wrapFilter: function (filters, options, nodes) {
|
2015-04-09 09:00:42 +00:00
|
|
|
|
let container = this._createNode("span", {
|
|
|
|
|
"data-filters": filters
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (options.filterSwatchClass) {
|
|
|
|
|
let swatch = this._createNode("span", {
|
|
|
|
|
class: options.filterSwatchClass
|
|
|
|
|
});
|
|
|
|
|
container.appendChild(swatch);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let value = this._createNode("span", {
|
|
|
|
|
class: options.filterClass
|
2015-06-08 16:26:00 +00:00
|
|
|
|
});
|
|
|
|
|
value.appendChild(nodes);
|
2015-04-09 09:00:42 +00:00
|
|
|
|
container.appendChild(value);
|
2015-06-08 16:26:00 +00:00
|
|
|
|
|
|
|
|
|
return container;
|
2015-04-09 09:00:42 +00:00
|
|
|
|
},
|
|
|
|
|
|
2016-05-17 18:25:54 +00:00
|
|
|
|
_onColorSwatchMouseDown: function (event) {
|
2015-04-17 10:53:06 +00:00
|
|
|
|
// Prevent text selection in the case of shift-click or double-click.
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
|
|
if (!event.shiftKey) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let swatch = event.target;
|
|
|
|
|
let color = this.colorSwatches.get(swatch);
|
|
|
|
|
let val = color.nextColorUnit();
|
|
|
|
|
|
|
|
|
|
swatch.nextElementSibling.textContent = val;
|
2016-03-28 19:01:32 +00:00
|
|
|
|
swatch.emit("unit-change", val);
|
2015-04-17 10:53:06 +00:00
|
|
|
|
},
|
|
|
|
|
|
2016-05-17 18:25:54 +00:00
|
|
|
|
_onAngleSwatchMouseDown: function (event) {
|
2016-03-08 22:04:54 +00:00
|
|
|
|
// Prevent text selection in the case of shift-click or double-click.
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
|
|
if (!event.shiftKey) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let swatch = event.target;
|
|
|
|
|
let angle = this.angleSwatches.get(swatch);
|
|
|
|
|
let val = angle.nextAngleUnit();
|
|
|
|
|
|
|
|
|
|
swatch.nextElementSibling.textContent = val;
|
2016-03-28 19:01:32 +00:00
|
|
|
|
swatch.emit("unit-change", val);
|
2016-03-08 22:04:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
2015-09-28 05:10:00 +00:00
|
|
|
|
/**
|
|
|
|
|
* A helper function that sanitizes a possibly-unterminated URL.
|
|
|
|
|
*/
|
2016-05-17 18:25:54 +00:00
|
|
|
|
_sanitizeURL: function (url) {
|
2015-09-28 05:10:00 +00:00
|
|
|
|
// Re-lex the URL and add any needed termination characters.
|
2016-04-29 17:21:22 +00:00
|
|
|
|
let urlTokenizer = getCSSLexer(url);
|
2015-09-28 05:10:00 +00:00
|
|
|
|
// Just read until EOF; there will only be a single token.
|
|
|
|
|
while (urlTokenizer.nextToken()) {
|
|
|
|
|
// Nothing.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return urlTokenizer.performEOFFixup(url, true);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Append a URL to the output.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} match
|
|
|
|
|
* Complete match that may include "url(xxx)"
|
|
|
|
|
* @param {String} url
|
|
|
|
|
* Actual URL
|
|
|
|
|
* @param {Object} [options]
|
|
|
|
|
* Options object. For valid options and default values see
|
|
|
|
|
* _mergeOptions().
|
|
|
|
|
*/
|
2016-05-17 18:25:54 +00:00
|
|
|
|
_appendURL: function (match, url, options) {
|
2013-11-11 15:17:41 +00:00
|
|
|
|
if (options.urlClass) {
|
2015-09-28 05:10:00 +00:00
|
|
|
|
// Sanitize the URL. Note that if we modify the URL, we just
|
|
|
|
|
// leave the termination characters. This isn't strictly
|
|
|
|
|
// "as-authored", but it makes a bit more sense.
|
|
|
|
|
match = this._sanitizeURL(match);
|
|
|
|
|
// This regexp matches a URL token. It puts the "url(", any
|
|
|
|
|
// leading whitespace, and any opening quote into |leader|; the
|
|
|
|
|
// URL text itself into |body|, and any trailing quote, trailing
|
|
|
|
|
// whitespace, and the ")" into |trailer|. We considered adding
|
|
|
|
|
// functionality for this to CSSLexer, in some way, but this
|
|
|
|
|
// seemed simpler on the whole.
|
|
|
|
|
let [, leader, , body, trailer] =
|
|
|
|
|
/^(url\([ \t\r\n\f]*(["']?))(.*?)(\2[ \t\r\n\f]*\))$/i.exec(match);
|
|
|
|
|
|
|
|
|
|
this._appendTextNode(leader);
|
2013-11-11 15:17:41 +00:00
|
|
|
|
|
|
|
|
|
let href = url;
|
|
|
|
|
if (options.baseURI) {
|
2016-05-13 20:27:28 +00:00
|
|
|
|
href = new URL(url, options.baseURI).href;
|
2013-11-11 15:17:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
2015-06-08 16:26:00 +00:00
|
|
|
|
this._appendNode("a", {
|
2013-11-11 15:17:41 +00:00
|
|
|
|
target: "_blank",
|
|
|
|
|
class: options.urlClass,
|
|
|
|
|
href: href
|
2015-09-28 05:10:00 +00:00
|
|
|
|
}, body);
|
2013-11-11 15:17:41 +00:00
|
|
|
|
|
2015-09-28 05:10:00 +00:00
|
|
|
|
this._appendTextNode(trailer);
|
2013-11-11 15:17:41 +00:00
|
|
|
|
} else {
|
2015-09-28 05:10:00 +00:00
|
|
|
|
this._appendTextNode(match);
|
2013-11-11 15:17:41 +00:00
|
|
|
|
}
|
2013-10-18 14:01:20 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
2014-06-12 10:02:00 +00:00
|
|
|
|
* Create a node.
|
2013-10-18 14:01:20 +00:00
|
|
|
|
*
|
|
|
|
|
* @param {String} tagName
|
|
|
|
|
* Tag type e.g. "div"
|
|
|
|
|
* @param {Object} attributes
|
|
|
|
|
* e.g. {class: "someClass", style: "cursor:pointer"};
|
|
|
|
|
* @param {String} [value]
|
|
|
|
|
* If a value is included it will be appended as a text node inside
|
|
|
|
|
* the tag. This is useful e.g. for span tags.
|
2015-07-28 13:36:00 +00:00
|
|
|
|
* @return {Node} Newly created Node.
|
2013-10-18 14:01:20 +00:00
|
|
|
|
*/
|
2016-05-17 18:25:54 +00:00
|
|
|
|
_createNode: function (tagName, attributes, value = "") {
|
2015-07-28 13:36:00 +00:00
|
|
|
|
let node = this.doc.createElementNS(HTML_NS, tagName);
|
2013-10-18 14:01:20 +00:00
|
|
|
|
let attrs = Object.getOwnPropertyNames(attributes);
|
|
|
|
|
|
|
|
|
|
for (let attr of attrs) {
|
2013-12-03 13:45:29 +00:00
|
|
|
|
if (attributes[attr]) {
|
|
|
|
|
node.setAttribute(attr, attributes[attr]);
|
|
|
|
|
}
|
2013-10-18 14:01:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (value) {
|
2015-07-28 13:36:00 +00:00
|
|
|
|
let textNode = this.doc.createTextNode(value);
|
2013-10-18 14:01:20 +00:00
|
|
|
|
node.appendChild(textNode);
|
|
|
|
|
}
|
|
|
|
|
|
2014-06-12 10:02:00 +00:00
|
|
|
|
return node;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Append a node to the output.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} tagName
|
|
|
|
|
* Tag type e.g. "div"
|
|
|
|
|
* @param {Object} attributes
|
|
|
|
|
* e.g. {class: "someClass", style: "cursor:pointer"};
|
|
|
|
|
* @param {String} [value]
|
|
|
|
|
* If a value is included it will be appended as a text node inside
|
|
|
|
|
* the tag. This is useful e.g. for span tags.
|
|
|
|
|
*/
|
2016-05-17 18:25:54 +00:00
|
|
|
|
_appendNode: function (tagName, attributes, value = "") {
|
2014-06-12 10:02:00 +00:00
|
|
|
|
let node = this._createNode(tagName, attributes, value);
|
2013-10-18 14:01:20 +00:00
|
|
|
|
this.parsed.push(node);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Append a text node to the output. If the previously output item was a text
|
|
|
|
|
* node then we append the text to that node.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} text
|
|
|
|
|
* Text to append
|
|
|
|
|
*/
|
2016-05-17 18:25:54 +00:00
|
|
|
|
_appendTextNode: function (text) {
|
2013-10-18 14:01:20 +00:00
|
|
|
|
let lastItem = this.parsed[this.parsed.length - 1];
|
2013-11-05 17:00:13 +00:00
|
|
|
|
if (typeof lastItem === "string") {
|
2013-11-18 16:07:24 +00:00
|
|
|
|
this.parsed[this.parsed.length - 1] = lastItem + text;
|
2013-10-18 14:01:20 +00:00
|
|
|
|
} else {
|
2013-11-05 17:00:13 +00:00
|
|
|
|
this.parsed.push(text);
|
2013-10-18 14:01:20 +00:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Take all output and append it into a single DocumentFragment.
|
|
|
|
|
*
|
|
|
|
|
* @return {DocumentFragment}
|
|
|
|
|
* Document Fragment
|
|
|
|
|
*/
|
2016-05-17 18:25:54 +00:00
|
|
|
|
_toDOM: function () {
|
2015-07-28 13:36:00 +00:00
|
|
|
|
let frag = this.doc.createDocumentFragment();
|
2013-10-18 14:01:20 +00:00
|
|
|
|
|
|
|
|
|
for (let item of this.parsed) {
|
2013-11-05 17:00:13 +00:00
|
|
|
|
if (typeof item === "string") {
|
2015-07-28 13:36:00 +00:00
|
|
|
|
frag.appendChild(this.doc.createTextNode(item));
|
2013-11-05 17:00:13 +00:00
|
|
|
|
} else {
|
|
|
|
|
frag.appendChild(item);
|
|
|
|
|
}
|
2013-10-18 14:01:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.parsed.length = 0;
|
|
|
|
|
return frag;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Merges options objects. Default values are set here.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} overrides
|
|
|
|
|
* The option values to override e.g. _mergeOptions({colors: false})
|
|
|
|
|
*
|
|
|
|
|
* Valid options are:
|
|
|
|
|
* - defaultColorType: true // Convert colors to the default type
|
|
|
|
|
* // selected in the options panel.
|
|
|
|
|
* - colorSwatchClass: "" // The class to use for color swatches.
|
2013-12-03 13:45:29 +00:00
|
|
|
|
* - colorClass: "" // The class to use for the color value
|
|
|
|
|
* // that follows the swatch.
|
2014-07-09 21:50:18 +00:00
|
|
|
|
* - bezierSwatchClass: "" // The class to use for bezier swatches.
|
|
|
|
|
* - bezierClass: "" // The class to use for the bezier value
|
|
|
|
|
* // that follows the swatch.
|
2016-03-08 22:04:54 +00:00
|
|
|
|
* - angleSwatchClass: "" // The class to use for angle swatches.
|
|
|
|
|
* - angleClass: "" // The class to use for the angle value
|
|
|
|
|
* // that follows the swatch.
|
2015-05-11 10:33:00 +00:00
|
|
|
|
* - supportsColor: false // Does the CSS property support colors?
|
2013-11-11 15:17:41 +00:00
|
|
|
|
* - urlClass: "" // The class to be used for url() links.
|
2016-05-13 20:27:28 +00:00
|
|
|
|
* - baseURI: undefined // A string used to resolve
|
2013-11-11 15:17:41 +00:00
|
|
|
|
* // relative links.
|
2015-06-08 16:26:00 +00:00
|
|
|
|
* - filterSwatch: false // A special case for parsing a
|
|
|
|
|
* // "filter" property, causing the
|
|
|
|
|
* // parser to skip the call to
|
|
|
|
|
* // _wrapFilter. Used only for
|
|
|
|
|
* // previewing with the filter swatch.
|
2013-10-18 14:01:20 +00:00
|
|
|
|
* @return {Object}
|
|
|
|
|
* Overridden options object
|
|
|
|
|
*/
|
2016-05-17 18:25:54 +00:00
|
|
|
|
_mergeOptions: function (overrides) {
|
2013-10-18 14:01:20 +00:00
|
|
|
|
let defaults = {
|
|
|
|
|
defaultColorType: true,
|
|
|
|
|
colorSwatchClass: "",
|
2013-12-03 13:45:29 +00:00
|
|
|
|
colorClass: "",
|
2014-07-09 21:50:18 +00:00
|
|
|
|
bezierSwatchClass: "",
|
|
|
|
|
bezierClass: "",
|
2016-03-08 22:04:54 +00:00
|
|
|
|
angleSwatchClass: "",
|
|
|
|
|
angleClass: "",
|
2015-05-11 10:33:00 +00:00
|
|
|
|
supportsColor: false,
|
2013-11-11 15:17:41 +00:00
|
|
|
|
urlClass: "",
|
2016-05-13 20:27:28 +00:00
|
|
|
|
baseURI: undefined,
|
2015-06-08 16:26:00 +00:00
|
|
|
|
filterSwatch: false
|
2013-10-18 14:01:20 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (let item in overrides) {
|
|
|
|
|
defaults[item] = overrides[item];
|
|
|
|
|
}
|
|
|
|
|
return defaults;
|
2014-01-30 15:04:47 +00:00
|
|
|
|
}
|
2013-10-18 14:01:20 +00:00
|
|
|
|
};
|
2015-05-11 10:33:00 +00:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A wrapper for DOMUtils.cssPropertySupportsType that ignores invalid
|
|
|
|
|
* properties.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} name The property name.
|
|
|
|
|
* @param {number} type The type tested for support.
|
|
|
|
|
* @return {Boolean} Whether the property supports the type.
|
|
|
|
|
* If the property is unknown, false is returned.
|
|
|
|
|
*/
|
|
|
|
|
function safeCssPropertySupportsType(name, type) {
|
|
|
|
|
try {
|
|
|
|
|
return DOMUtils.cssPropertySupportsType(name, type);
|
2016-05-17 18:25:54 +00:00
|
|
|
|
} catch (e) {
|
2015-05-11 10:33:00 +00:00
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|