diff --git a/README.md b/README.md index 3bd6a0d..66f5375 100644 --- a/README.md +++ b/README.md @@ -183,10 +183,26 @@ whether the first character in the argument is a quotation mark. ] } ``` +### interpolation + +An interpolation of value, e.g. Sass interpolation `#{rgb(0,0,0)}`. + +Interpolation nodes have nodes all other nodes nested within them. + +Additional properties: + +- **value**: Interpolation prefix, e.g. `#` in `#{rgb(0,0,0)}`. +- **before**: Whitespace after the opening curly bracket and before the first value, + e.g. ` ` in `#{ rgb(0,0,0)}`. +- **after**: Whitespace before the closing curly bracket and after the last value, + e.g. ` ` in `#{rgb(0,0,0) }`. +- **nodes**: More nodes representing the arguments to the interpolation. +- **unclosed**: `true` if the curly bracket was not closed properly. e.g. `#{ unclosed-interpolation `. + ### unicode-range -The unicode-range CSS descriptor sets the specific range of characters to be -used from a font defined by @font-face and made available +The unicode-range CSS descriptor sets the specific range of characters to be +used from a font defined by @font-face and made available for use on the current page (`unicode-range: U+0025-00FF`). Node-specific properties: @@ -242,10 +258,16 @@ The `callback` is invoked with three arguments: `callback(node, index, nodes)`. Returns the `valueParser` instance. -### var parsed = valueParser(value) +### var parsed = valueParser(value, options) Returns the parsed node tree. +### options + +#### options.interpolationPrefix + +Prefix used for interpolation, e.g. `#` for Sass interpolation. + ### parsed.nodes The array of nodes. diff --git a/lib/index.d.ts b/lib/index.d.ts index be8b1f3..980a075 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -47,6 +47,18 @@ declare namespace postcssValueParser { nodes: Node[]; } + interface InterpolationNode + extends BaseNode, + ClosableNode, + AdjacentAwareNode { + type: "interpolation"; + + /** + * Nodes inside the interpolation + */ + nodes: Node[]; + } + interface SpaceNode extends BaseNode { type: "space"; } @@ -75,6 +87,7 @@ declare namespace postcssValueParser { | CommentNode | DivNode | FunctionNode + | InterpolationNode | SpaceNode | StringNode | UnicodeRangeNode @@ -124,6 +137,13 @@ declare namespace postcssValueParser { walk(callback: WalkCallback, bubble?: boolean): this; } + interface ValueParserOptions { + /** + * Prefix used for interpolation, e.g. `#` for Sass interpolation. + */ + interpolationPrefix?: string; + } + interface ValueParser { /** * Decompose a CSSĀ dimension into its numeric and unit part @@ -162,8 +182,9 @@ declare namespace postcssValueParser { * Parse a CSS value into a series of nodes to operate on * * @param value The value to parse + * @param options Value parser options */ - (value: string): ParsedValue; + (value: string, options?: ValueParserOptions): ParsedValue; } } diff --git a/lib/index.js b/lib/index.js index f9ac0e6..020033d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -2,12 +2,12 @@ var parse = require("./parse"); var walk = require("./walk"); var stringify = require("./stringify"); -function ValueParser(value) { +function ValueParser(value, options) { if (this instanceof ValueParser) { - this.nodes = parse(value); + this.nodes = parse(value, options); return this; } - return new ValueParser(value); + return new ValueParser(value, options); } ValueParser.prototype.toString = function() { diff --git a/lib/parse.js b/lib/parse.js index 502b5ba..19a53fc 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -10,9 +10,11 @@ var star = "*".charCodeAt(0); var uLower = "u".charCodeAt(0); var uUpper = "U".charCodeAt(0); var plus = "+".charCodeAt(0); +var openInterpolation = "{".charCodeAt(0); +var closeInterpolation = "}".charCodeAt(0); var isUnicodeRange = /^[a-f0-9?-]+$/i; -module.exports = function(input) { +module.exports = function(input, options) { var tokens = []; var value = input; @@ -35,6 +37,17 @@ module.exports = function(input) { var before = ""; var after = ""; + options = options || {}; + var interpolationPrefix = null; + var interpolationPrefixCode = null; + if ( + typeof options.interpolationPrefix !== "undefined" && + options.interpolationPrefix !== null + ) { + interpolationPrefix = options.interpolationPrefix; + interpolationPrefixCode = interpolationPrefix.charCodeAt(0); + } + while (pos < max) { // Whitespaces if (code <= 32) { @@ -46,7 +59,10 @@ module.exports = function(input) { token = value.slice(pos, next); prev = tokens[tokens.length - 1]; - if (code === closeParentheses && balanced) { + if ( + (code === closeParentheses || code === closeInterpolation) && + balanced + ) { after = token; } else if (prev && prev.type === "div") { prev.after = token; @@ -151,7 +167,8 @@ module.exports = function(input) { code = value.charCodeAt(pos); // Open parentheses - } else if (openParentheses === code) { + } else if (openParentheses === code || openInterpolation === code) { + var isFunction = openParentheses === code; // Whitespaces after open parentheses next = pos; do { @@ -160,9 +177,9 @@ module.exports = function(input) { } while (code <= 32); parenthesesOpenPos = pos; token = { - type: "function", + type: isFunction ? "function" : "interpolation", sourceIndex: pos - name.length, - value: name, + value: isFunction ? name : interpolationPrefix, before: value.slice(parenthesesOpenPos + 1, next) }; pos = next; @@ -230,7 +247,10 @@ module.exports = function(input) { name = ""; // Close parentheses - } else if (closeParentheses === code && balanced) { + } else if ( + (code === closeParentheses || code === closeInterpolation) && + balanced + ) { pos += 1; code = value.charCodeAt(pos); @@ -260,6 +280,8 @@ module.exports = function(input) { code === colon || code === slash || code === openParentheses || + (interpolationPrefix !== null && code === openInterpolation) || + (interpolationPrefix !== null && code === interpolationPrefixCode) || (code === star && parent && parent.type === "function" && @@ -267,12 +289,13 @@ module.exports = function(input) { (code === slash && parent.type === "function" && parent.value === "calc") || - (code === closeParentheses && balanced) + ((code === closeParentheses || code === closeInterpolation) && + balanced) ) ); token = value.slice(pos, next); - if (openParentheses === code) { + if (openParentheses === code || openInterpolation === code) { name = token; } else if ( (uLower === token.charCodeAt(0) || uUpper === token.charCodeAt(0)) && diff --git a/lib/stringify.js b/lib/stringify.js index 6079671..4b85811 100644 --- a/lib/stringify.js +++ b/lib/stringify.js @@ -17,16 +17,17 @@ function stringifyNode(node, custom) { return (node.before || "") + value + (node.after || ""); } else if (Array.isArray(node.nodes)) { buf = stringify(node.nodes, custom); - if (type !== "function") { + if (type !== "function" && type !== "interpolation") { return buf; } + var isFunction = type === "function"; return ( value + - "(" + + (isFunction ? "(" : "{") + (node.before || "") + buf + (node.after || "") + - (node.unclosed ? "" : ")") + (node.unclosed ? "" : isFunction ? ")" : "}") ); } return value; diff --git a/lib/walk.js b/lib/walk.js index 7666c5b..843acd7 100644 --- a/lib/walk.js +++ b/lib/walk.js @@ -9,7 +9,7 @@ module.exports = function walk(nodes, cb, bubble) { if ( result !== false && - node.type === "function" && + (node.type === "function" || node.type === "interpolation") && Array.isArray(node.nodes) ) { walk(node.nodes, cb, bubble); diff --git a/test/index.js b/test/index.js index 7478f9a..c6e1a87 100644 --- a/test/index.js +++ b/test/index.js @@ -21,7 +21,7 @@ test("ValueParser", function(tp) { }); tp.test("walk", function(t) { - t.plan(4); + t.plan(7); var result; result = []; @@ -74,6 +74,135 @@ test("ValueParser", function(tp) { result = []; + parser("fn( ) #{fn2( fn3())}", { interpolationPrefix: "#" }).walk(function( + node + ) { + if (node.type === "interpolation") { + result.push(node); + } + }); + + t.deepEqual( + result, + [ + { + type: "interpolation", + sourceIndex: 6, + value: "#", + before: "", + after: "", + nodes: [ + { + type: "function", + sourceIndex: 8, + value: "fn2", + before: " ", + after: "", + nodes: [ + { + type: "function", + sourceIndex: 13, + value: "fn3", + before: "", + after: "", + nodes: [] + } + ] + } + ] + } + ], + "should process all interpolations" + ); + + result = []; + + parser("fn( ) --#{fn2( fn3())}", { interpolationPrefix: "#" }).walk( + function(node) { + if (node.type === "interpolation") { + result.push(node); + } + } + ); + + t.deepEqual( + result, + [ + { + type: "interpolation", + sourceIndex: 8, + value: "#", + before: "", + after: "", + nodes: [ + { + type: "function", + sourceIndex: 10, + value: "fn2", + before: " ", + after: "", + nodes: [ + { + type: "function", + sourceIndex: 15, + value: "fn3", + before: "", + after: "", + nodes: [] + } + ] + } + ] + } + ], + "should process all interpolations with word" + ); + + result = []; + + parser("fn( ) /#{fn2( fn3())}", { interpolationPrefix: "#" }).walk(function( + node + ) { + if (node.type === "interpolation") { + result.push(node); + } + }); + + t.deepEqual( + result, + [ + { + type: "interpolation", + sourceIndex: 7, + value: "#", + before: "", + after: "", + nodes: [ + { + type: "function", + sourceIndex: 9, + value: "fn2", + before: " ", + after: "", + nodes: [ + { + type: "function", + sourceIndex: 14, + value: "fn3", + before: "", + after: "", + nodes: [] + } + ] + } + ] + } + ], + "should process all interpolations with divider (/)" + ); + + result = []; + parser("fn( ) fn2( fn3())").walk(function(node) { if (node.type === "function") { result.push(node); diff --git a/test/parse.js b/test/parse.js index 41fdc53..c5e0ccb 100644 --- a/test/parse.js +++ b/test/parse.js @@ -89,6 +89,96 @@ var tests = [ } ] }, + { + message: "should process interpolation", + fixture: "#{name()}", + options: { + interpolationPrefix: "#" + }, + expected: [ + { + type: "interpolation", + sourceIndex: 0, + value: "#", + before: "", + after: "", + nodes: [ + { + type: "function", + sourceIndex: 2, + value: "name", + before: "", + after: "", + nodes: [] + } + ] + } + ] + }, + { + message: "should process interpolation with word", + fixture: "--#{name()}", + options: { + interpolationPrefix: "#" + }, + expected: [ + { + type: "word", + sourceIndex: 0, + value: "--" + }, + { + type: "interpolation", + sourceIndex: 2, + value: "#", + before: "", + after: "", + nodes: [ + { + type: "function", + sourceIndex: 4, + value: "name", + before: "", + after: "", + nodes: [] + } + ] + } + ] + }, + { + message: "should process interpolation with divider (/)", + fixture: "/#{name()}", + options: { + interpolationPrefix: "#" + }, + expected: [ + { + type: "div", + sourceIndex: 0, + value: "/", + before: "", + after: "" + }, + { + type: "interpolation", + sourceIndex: 1, + value: "#", + before: "", + after: "", + nodes: [ + { + type: "function", + sourceIndex: 3, + value: "name", + before: "", + after: "", + nodes: [] + } + ] + } + ] + }, { message: "should process nested functions", fixture: "((()))", @@ -1319,6 +1409,6 @@ test("Parse", function(t) { t.plan(tests.length); tests.forEach(function(opts) { - t.deepEqual(parse(opts.fixture), opts.expected, opts.message); + t.deepEqual(parse(opts.fixture, opts.options), opts.expected, opts.message); }); }); diff --git a/test/stringify.js b/test/stringify.js index 36d13f2..3fd703b 100644 --- a/test/stringify.js +++ b/test/stringify.js @@ -54,6 +54,27 @@ var tests = [ message: "Should correctly process empty url with newline (LF)", fixture: "url(\n)" }, + { + message: "Should correctly process interpolation", + fixture: "#{url(\n)}", + options: { + interpolationPrefix: "#" + } + }, + { + message: "Should correctly process interpolation with word", + fixture: "-#{url(\n)}", + options: { + interpolationPrefix: "#" + } + }, + { + message: "Should correctly process interpolation with divider (/)", + fixture: "/#{url(\n)}", + options: { + interpolationPrefix: "#" + } + }, { message: "Should correctly process empty url with newline (LF)", fixture: "url(\n\n\n)" @@ -92,7 +113,11 @@ test("Stringify", function(t) { t.plan(tests.length + 4); tests.forEach(function(opts) { - t.equal(stringify(parse(opts.fixture)), opts.fixture, opts.message); + t.equal( + stringify(parse(opts.fixture, opts.options)), + opts.fixture, + opts.message + ); }); var tokens = parse(" rgba(12, 54, 65 ) ");