From: Jakub Pawlowicz Date: Wed, 29 Oct 2014 20:26:21 +0000 (+0000) Subject: Adds line/column tracking in tokenization. X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=fb7362a78bf555eccd2ee44777237f1ac63db425;p=clean-css.git Adds line/column tracking in tokenization. --- diff --git a/lib/properties/optimizer.js b/lib/properties/optimizer.js index c44eb32f..df833477 100644 --- a/lib/properties/optimizer.js +++ b/lib/properties/optimizer.js @@ -123,11 +123,10 @@ module.exports = function Optimizer(compatibility, aggressiveMerging, context) { var keyValues = []; for (var i = 0, l = body.length; i < l; i++) { - var token = body[i].value; - - var firstColon = token.indexOf(':'); - var property = token.substring(0, firstColon); - var value = token.substring(firstColon + 1); + var token = body[i]; + var firstColon = token.value.indexOf(':'); + var property = token.value.substring(0, firstColon); + var value = token.value.substring(firstColon + 1); if (value === '') { context.warnings.push('Empty property \'' + property + '\' inside \'' + selector.map(valueMapper).join(',') + '\' selector. Ignoring.'); continue; @@ -136,8 +135,9 @@ module.exports = function Optimizer(compatibility, aggressiveMerging, context) { keyValues.push([ property, value, - token.indexOf('!important') > -1, - token.indexOf(IE_BACKSLASH_HACK, firstColon + 1) === token.length - IE_BACKSLASH_HACK.length + token.value.indexOf('!important') > -1, + token.value.indexOf(IE_BACKSLASH_HACK, firstColon + 1) === token.value.length - IE_BACKSLASH_HACK.length, + token.metadata ]); } @@ -246,7 +246,7 @@ module.exports = function Optimizer(compatibility, aggressiveMerging, context) { eligibleForCompacting = true; var property = tokens[i][0] + ':' + tokens[i][1]; - tokenized.push({ value: property }); + tokenized.push({ value: property, metadata: tokens[i][4] }); list.push(property); } diff --git a/lib/properties/token.js b/lib/properties/token.js index 333d43fc..02604767 100644 --- a/lib/properties/token.js +++ b/lib/properties/token.js @@ -29,6 +29,7 @@ module.exports = (function() { Token.prototype.isIrrelevant = false; Token.prototype.isReal = true; Token.prototype.isMarkedForDeletion = false; + Token.prototype.metadata = null; // Tells if this token is a component of the other one Token.prototype.isComponentOf = function (other) { @@ -93,6 +94,8 @@ module.exports = (function() { result.isDirty = true; } + result.metadata = fullProp.metadata; + return result; }; @@ -126,7 +129,7 @@ module.exports = (function() { } var property = t.prop + ':' + t.value + (t.isImportant ? important : ''); - tokenized.push({ value: property }); + tokenized.push({ value: property, metadata: t.metadata || {} }); list.push(property); } diff --git a/lib/selectors/optimizers/clean-up.js b/lib/selectors/optimizers/clean-up.js index 0e3781da..3474971e 100644 --- a/lib/selectors/optimizers/clean-up.js +++ b/lib/selectors/optimizers/clean-up.js @@ -2,37 +2,44 @@ function removeWhitespace(match, value) { return '[' + value.replace(/ /g, '') + ']'; } +function selectorSorter(s1, s2) { + return s1.value > s2.value ? 1 : -1; +} + var CleanUp = { selectors: function (selectors) { var plain = []; + var tokenized = []; for (var i = 0, l = selectors.length; i < l; i++) { - var selector = selectors[i].value; - var reduced = selector + var selector = selectors[i]; + var value = selector.value; + var reduced = value .replace(/\s/g, ' ') .replace(/\s{2,}/g, ' ') .replace(/ ?, ?/g, ',') .replace(/\s*([>\+\~])\s*/g, '$1') .trim(); - if (selector.indexOf('*') > -1) { + if (value.indexOf('*') > -1) { reduced = reduced .replace(/\*([:#\.\[])/g, '$1') .replace(/^(\:first\-child)?\+html/, '*$1+html'); } - if (selector.indexOf('[') > -1) + if (value.indexOf('[') > -1) reduced = reduced.replace(/\[([^\]]+)\]/g, removeWhitespace); - if (plain.indexOf(reduced) == -1) + if (plain.indexOf(reduced) == -1) { plain.push(reduced); + selector.value = reduced; + tokenized.push(selector); + } } - var sorted = plain.sort(); - return { - list: sorted, - tokenized: sorted.map(function (selector) { return { value: selector }; }) + list: plain.sort(), + tokenized: tokenized.sort(selectorSorter) }; }, diff --git a/lib/selectors/optimizers/simple.js b/lib/selectors/optimizers/simple.js index a29ca931..6dee63a8 100644 --- a/lib/selectors/optimizers/simple.js +++ b/lib/selectors/optimizers/simple.js @@ -183,10 +183,10 @@ function reduce(body, options) { var newProperty; for (var i = 0, l = body.length; i < l; i++) { - var token = body[i].value; - var firstColon = token.indexOf(':'); - var property = token.substring(0, firstColon); - var value = token.substring(firstColon + 1); + var token = body[i]; + var firstColon = token.value.indexOf(':'); + var property = token.value.substring(0, firstColon); + var value = token.value.substring(firstColon + 1); var important = false; if (!options.compatibility.properties.iePrefixHack && (property[0] == '_' || property[0] == '*')) @@ -210,7 +210,7 @@ function reduce(body, options) { value = colorMininifier(property, value, options.compatibility); newProperty = property + ':' + value + (important ? '!important' : ''); - reduced.push({ value: newProperty }); + reduced.push({ value: newProperty, metadata: token.metadata }); properties.push(newProperty); } diff --git a/lib/selectors/tokenizer.js b/lib/selectors/tokenizer.js index 879470b9..568fbb85 100644 --- a/lib/selectors/tokenizer.js +++ b/lib/selectors/tokenizer.js @@ -1,11 +1,13 @@ var Chunker = require('../utils/chunker'); var Extract = require('../utils/extractors'); +var SourceMaps = require('../utils/source-maps'); var flatBlock = /(^@(font\-face|page|\-ms\-viewport|\-o\-viewport|viewport|counter\-style)|\\@.+?)/; -function Tokenizer(minifyContext, addMetadata) { +function Tokenizer(minifyContext, addMetadata, addSourceMap) { this.minifyContext = minifyContext; this.addMetadata = addMetadata; + this.addSourceMap = addSourceMap; } Tokenizer.prototype.toTokens = function (data) { @@ -21,7 +23,10 @@ Tokenizer.prototype.toTokens = function (data) { chunker: chunker, chunk: chunker.next(), outer: this.minifyContext, - addMetadata: this.addMetadata + addMetadata: this.addMetadata, + addSourceMap: this.addSourceMap, + line: 1, + column: 1 }; return tokenize(context); @@ -78,6 +83,9 @@ function whatsNext(context) { function tokenize(context) { var chunk = context.chunk; var tokenized = []; + var newToken; + var value; + var addSourceMap = context.addSourceMap; while (true) { var next = whatsNext(context); @@ -108,44 +116,63 @@ function tokenize(context) { } else if (isSingle) { nextEnd = chunk.indexOf(';', nextSpecial + 1); - var single = chunk.substring(context.cursor, nextEnd + 1); - tokenized.push({ kind: 'at-rule', value: single }); + value = chunk.substring(context.cursor, nextEnd + 1); + newToken = { kind: 'at-rule', value: value }; + tokenized.push(newToken); + + if (addSourceMap) + newToken.metadata = SourceMaps.saveAndTrack(value, context, true); context.cursor = nextEnd + 1; } else { nextEnd = chunk.indexOf('{', nextSpecial + 1); - var block = chunk.substring(context.cursor, nextEnd).trim(); + value = chunk.substring(context.cursor, nextEnd).trim(); - var isFlat = flatBlock.test(block); + var isFlat = flatBlock.test(value); oldMode = context.mode; context.cursor = nextEnd + 1; context.mode = isFlat ? 'body' : 'block'; - var specialBody = tokenize(context); - if (typeof specialBody == 'string') - specialBody = Extract.properties(specialBody).tokenized; + newToken = { kind: 'block', value: value, isFlatBlock: isFlat }; + + if (addSourceMap) + newToken.metadata = SourceMaps.saveAndTrack(value, context, true); + + newToken.body = tokenize(context); + if (typeof newToken.body == 'string') + newToken.body = Extract.properties(newToken.body, context).tokenized; context.mode = oldMode; - tokenized.push({ kind: 'block', value: block, body: specialBody, isFlatBlock: isFlat }); + if (addSourceMap) + SourceMaps.suffix(context); + + tokenized.push(newToken); } } else if (what == 'escape') { nextEnd = chunk.indexOf('__', nextSpecial + 1); var escaped = chunk.substring(context.cursor, nextEnd + 2); tokenized.push({ kind: 'text', value: escaped }); + if (addSourceMap) + SourceMaps.track(escaped, context); + context.cursor = nextEnd + 2; } else if (what == 'bodyStart') { - var selectorData = Extract.selectors(chunk.substring(context.cursor, nextSpecial)); + var selectorData = Extract.selectors(chunk.substring(context.cursor, nextSpecial), context); oldMode = context.mode; context.cursor = nextSpecial + 1; context.mode = 'body'; - var bodyData = Extract.properties(tokenize(context)); + + var bodyData = Extract.properties(tokenize(context), context); + + if (addSourceMap) + SourceMaps.suffix(context); context.mode = oldMode; - var newToken = { + newToken = { kind: 'selector', value: selectorData.tokenized, body: bodyData.tokenized diff --git a/lib/utils/extractors.js b/lib/utils/extractors.js index 4067fbcb..40d1cc94 100644 --- a/lib/utils/extractors.js +++ b/lib/utils/extractors.js @@ -1,14 +1,32 @@ var Splitter = require('./splitter'); +var SourceMaps = require('../utils/source-maps'); -function valueMapper(property) { - return { value: property }; +function tokenMetadata(value, context, addExtra) { + var withoutContent; + var total; + var split = value.split('\n'); + var shift = 0; + for (withoutContent = 0, total = split.length; withoutContent < total; withoutContent++) { + var part = split[withoutContent]; + if (/\S/.test(part)) + break; + + shift += part.length + 1; + } + + context.line += withoutContent; + context.column = withoutContent > 0 ? 1 : context.column; + context.column += /^(\s)*/.exec(split[withoutContent])[0].length; + + return SourceMaps.saveAndTrack(value.substring(shift).trimLeft(), context, addExtra); } var Extractors = { - properties: function (string) { + properties: function (string, context) { var tokenized = []; var list = []; var buffer = []; + var all = []; var property; var isWhitespace; var wasWhitespace; @@ -16,19 +34,34 @@ var Extractors = { var wasSpecial; var current; var wasCloseParenthesis; + var isEscape; + var token; + var addSourceMap = context.addSourceMap; for (var i = 0, l = string.length; i < l; i++) { current = string[i]; - if (current === ';') { + isEscape = current == '_' && buffer.length < 2 && string.indexOf('__ESCAPED_', i) === 0; + if (isEscape) { + var endOfEscape = string.indexOf('__', i + 1) + 2; + buffer = all = [string.substring(i, endOfEscape)]; + i = endOfEscape - 1; + } + + if (current === ';' || isEscape) { if (wasWhitespace && buffer[buffer.length - 1] === ' ') buffer.pop(); if (buffer.length > 0) { property = buffer.join(''); - tokenized.push({ value: property }); + token = { value: property }; + tokenized.push(token); list.push(property); + + if (addSourceMap) + token.metadata = tokenMetadata(all.join(''), context, !isEscape); } buffer = []; + all = []; } else { isWhitespace = current === ' ' || current === '\t' || current === '\n'; isSpecial = current === ':' || current === '[' || current === ']' || current === ',' || current === '(' || current === ')'; @@ -44,6 +77,8 @@ var Extractors = { } else { buffer.push(isWhitespace ? ' ' : current); } + + all.push(current); } wasSpecial = isSpecial; @@ -55,8 +90,14 @@ var Extractors = { buffer.pop(); if (buffer.length > 0) { property = buffer.join(''); - tokenized.push({ value: property }); + token = { value: property }; + tokenized.push(token); list.push(property); + + if (addSourceMap) + token.metadata = tokenMetadata(all.join(''), context, false); + } else if (all.indexOf('\n') > -1) { + SourceMaps.track(all.join('\n'), context); } return { @@ -65,12 +106,27 @@ var Extractors = { }; }, - selectors: function (string) { + selectors: function (string, context) { + var tokenized = []; + var list = []; var selectors = new Splitter(',').split(string); + var addSourceMap = context.addSourceMap; + + for (var i = 0, l = selectors.length; i < l; i++) { + var selector = selectors[i]; + + list.push(selector); + + var token = { value: selector }; + tokenized.push(token); + + if (addSourceMap) + token.metadata = tokenMetadata(selector, context, true); + } return { - list: selectors, - tokenized: selectors.map(valueMapper) + list: list, + tokenized: tokenized }; } }; diff --git a/lib/utils/source-maps.js b/lib/utils/source-maps.js new file mode 100644 index 00000000..1413c7a6 --- /dev/null +++ b/lib/utils/source-maps.js @@ -0,0 +1,53 @@ +var SourceMaps = { + saveAndTrack: function (data, context, hasSuffix) { + var metadata = { + line: context.line, + column: context.column + }; + + this.track(data, context); + + if (hasSuffix) + context.column++; + + return metadata; + }, + + suffix: function (context) { + context.column++; + }, + + track: function (data, context) { + var parts = data.split('\n'); + + for (var i = 0, l = parts.length; i < l; i++) { + var part = parts[i]; + var cursor = 0; + + if (i > 0) { + context.line++; + context.column = 1; + } + + while (true) { + var next = part.indexOf('__ESCAPED_', cursor); + + if (next == -1) { + context.column += part.substring(cursor).length; + break; + } + + context.column += next - cursor; + cursor += next - cursor; + + var escaped = part.substring(next, part.indexOf('__', next + 1) + 2); + var encodedValues = escaped.substring(escaped.indexOf('(') + 1, escaped.indexOf(')')).split(','); + context.line += ~~encodedValues[0]; + context.column = (~~encodedValues[0] === 0 ? context.column : 1) + ~~encodedValues[1]; + cursor += escaped.length; + } + } + } +}; + +module.exports = SourceMaps; diff --git a/test/selectors/tokenizer-source-maps-test.js b/test/selectors/tokenizer-source-maps-test.js new file mode 100644 index 00000000..6a36d362 --- /dev/null +++ b/test/selectors/tokenizer-source-maps-test.js @@ -0,0 +1,402 @@ +var vows = require('vows'); +var assert = require('assert'); +var Tokenizer = require('../../lib/selectors/tokenizer'); + +function sourceMapContext(group, specs) { + var ctx = {}; + + function tokenizedContext(target, index) { + return function (tokenized) { + assert.deepEqual(tokenized[index], target); + }; + } + + for (var test in specs) { + for (var i = 0; i < specs[test][1].length; i++) { + var target = specs[test][1][i]; + + ctx[group + ' ' + test + ' - #' + (i + 1)] = { + topic: new Tokenizer({}, false, true).toTokens(specs[test][0]), + tokenized: tokenizedContext(target, i) + }; + } + } + + return ctx; +} + +vows.describe('source-maps/analyzer') + .addBatch( + sourceMapContext('selectors', { + 'single': [ + 'a{}', + [{ + kind: 'selector', + value: [{ value: 'a', metadata: { line: 1, column: 1 } }], + body: [] + }] + ], + 'double': [ + 'a,div{}', + [{ + kind: 'selector', + value: [ + { value: 'a', metadata: { line: 1, column: 1 } }, + { value: 'div', metadata: { line: 1, column: 3 } } + ], + body: [] + }] + ], + 'double with whitespace': [ + ' a,\n\ndiv{}', + [{ + kind: 'selector', + value: [ + { value: ' a', metadata: { line: 1, column: 2 } }, + { value: '\n\ndiv', metadata: { line: 3, column: 1 } } + ], + body: [] + }] + ], + 'triple': [ + 'a,div,p{}', + [{ + kind: 'selector', + value: [ + { value: 'a', metadata: { line: 1, column: 1 } }, + { value: 'div', metadata: { line: 1, column: 3 } }, + { value: 'p', metadata: { line: 1, column: 7 } } + ], + body: [] + }] + ], + 'triple with whitespace': [ + ' a,\n\ndiv\na,\n p{}', + [{ + kind: 'selector', + value: [ + { value: ' a', metadata: { line: 1, column: 2 } }, + { value: '\n\ndiv\na', metadata: { line: 3, column: 1 } }, + { value: '\n p', metadata: { line: 5, column: 2 } } + ], + body: [] + }] + ], + 'two': [ + 'a{}div{}', + [ + { + kind: 'selector', + value: [{ value: 'a', metadata: { line: 1, column: 1 } }], + body: [] + }, + { + kind: 'selector', + value: [{ value: 'div', metadata: { line: 1, column: 4 } }], + body: [] + } + ] + ], + 'three with whitespace and breaks': [ + 'a {}\n\ndiv{}\n \n p{}', + [ + { + kind: 'selector', + value: [{ value: 'a ', metadata: { line: 1, column: 1 } }], + body: [] + }, + { + kind: 'selector', + value: [{ value: '\n\ndiv', metadata: { line: 3, column: 1 } }], + body: [] + }, + { + kind: 'selector', + value: [{ value: '\n \n p', metadata: { line: 5, column: 3 } }], + body: [] + } + ] + ] + }) + ) + .addBatch( + sourceMapContext('properties', { + 'single': [ + 'a{color:red}', + [{ + kind: 'selector', + value: [{ value: 'a', metadata: { line: 1, column: 1 } }], + body: [{ value: 'color:red', metadata: { line: 1, column: 3 } }] + }] + ], + 'double': [ + 'a{color:red;border:none}', + [{ + kind: 'selector', + value: [{ value: 'a', metadata: { line: 1, column: 1 } }], + body: [ + { value: 'color:red', metadata: { line: 1, column: 3 } }, + { value: 'border:none', metadata: { line: 1, column: 13 } } + ] + }] + ], + 'triple with whitespace': [ + 'a{color:red;\nborder:\nnone;\n\n display:block}', + [{ + kind: 'selector', + value: [{ value: 'a', metadata: { line: 1, column: 1 } }], + body: [ + { value: 'color:red', metadata: { line: 1, column: 3 } }, + { value: 'border:none', metadata: { line: 2, column: 1 } }, + { value: 'display:block', metadata: { line: 5, column: 3 } } + ] + }] + ], + 'two declarations': [ + 'a{color:red}div{color:blue}', + [ + { + kind: 'selector', + value: [{ value: 'a', metadata: { line: 1, column: 1 } }], + body: [{ value: 'color:red', metadata: { line: 1, column: 3 } }] + }, + { + kind: 'selector', + value: [{ value: 'div', metadata: { line: 1, column: 13 } }], + body: [{ value: 'color:blue', metadata: { line: 1, column: 17 } }] + } + ] + ], + 'two declarations with whitespace': [ + 'a{color:red}\n div{color:blue}', + [ + { + kind: 'selector', + value: [{ value: 'a', metadata: { line: 1, column: 1 } }], + body: [{ value: 'color:red', metadata: { line: 1, column: 3 } }] + }, + { + kind: 'selector', + value: [{ value: '\n div', metadata: { line: 2, column: 2 } }], + body: [{ value: 'color:blue', metadata: { line: 2, column: 6 } }] + } + ] + ], + 'two declarations with whitespace and ending semicolon': [ + 'a{color:red;\n}\n div{color:blue}', + [ + { + kind: 'selector', + value: [{ value: 'a', metadata: { line: 1, column: 1 } }], + body: [{ value: 'color:red', metadata: { line: 1, column: 3 } }] + }, + { + kind: 'selector', + value: [{ value: '\n div', metadata: { line: 3, column: 2 } }], + body: [{ value: 'color:blue', metadata: { line: 3, column: 6 } }] + } + ] + ] + }) + ) + .addBatch( + sourceMapContext('at rules', { + '@import': [ + 'a{}@import \n"test.css";\n\na{color:red}', + [ + { + kind: 'selector', + value: [{ value: 'a', metadata: { line: 1, column: 1 } }], + body: [] + }, + { + kind: 'at-rule', + value: '@import \n"test.css";', + metadata: { line: 1, column: 4 } + }, + { + kind: 'selector', + value: [{ value: '\n\na', metadata: { line: 4, column: 1 } }], + body: [{ value: 'color:red', metadata: { line: 4, column: 3 } }] + } + ] + ], + '@charset': [ + '@charset "utf-8";a{color:red}', + [ + { + kind: 'at-rule', + value: '@charset "utf-8";', + metadata: { line: 1, column: 1 } + }, + { + kind: 'selector', + value: [{ value: 'a', metadata: { line: 1, column: 19 } }], + body: [{ value: 'color:red', metadata: { line: 1, column: 21 } }] + } + ] + ] + }) + ) + .addBatch( + sourceMapContext('blocks', { + '@media - simple': [ + '@media (min-width:980px){a{color:red}}', + [ + { + kind: 'block', + value: '@media (min-width:980px)', + metadata: { line: 1, column: 1 }, + isFlatBlock: false, + body: [{ + kind: 'selector', + value: [{ value: 'a', metadata: { line: 1, column: 26 } }], + body: [{ value: 'color:red', metadata: { line: 1, column: 28 } }] + }] + } + ] + ], + '@media - with whitespace': [ + '@media (\nmin-width:980px)\n{\na{\ncolor:\nred}p{}}', + [ + { + kind: 'block', + value: '@media (\nmin-width:980px)', + metadata: { line: 1, column: 1 }, + isFlatBlock: false, + body: [ + { + kind: 'selector', + value: [{ value: '\na', metadata: { line: 3, column: 1 } }], + body: [{ value: 'color:red', metadata: { line: 4, column: 1 } }] + }, + { + kind: 'selector', + value: [{ value: 'p', metadata: { line: 5, column: 5 } }], + body: [] + } + ] + } + ] + ], + '@font-face': [ + '@font-face{font-family: "Font";\nsrc: url("font.ttf");\nfont-weight: normal;font-style: normal}a{}', + [ + { + kind: 'block', + value: '@font-face', + metadata: { line: 1, column: 1 }, + isFlatBlock: true, + body: [ + { value: 'font-family:"Font"', metadata: { line: 1, column: 12 } }, + { value: 'src:url("font.ttf")', metadata: { line: 2, column: 1 } }, + { value: 'font-weight:normal', metadata: { line: 3, column: 1 } }, + { value: 'font-style:normal', metadata: { line: 3, column: 21 } } + ] + }, + { + kind: 'selector', + value: [{ value: 'a', metadata: { line: 3, column: 40 } }], + body: [] + } + ] + ] + }) + ) + .addBatch( + sourceMapContext('escaped content', { + 'top-level': [ + '__ESCAPED_COMMENT_CLEAN_CSS0(0, 5)__a{}', + [ + { + kind: 'text', + value: '__ESCAPED_COMMENT_CLEAN_CSS0(0, 5)__' + }, + { + kind: 'selector', + value: [{ value: 'a', metadata: { line: 1, column: 6 } }], + body: [] + } + ] + ], + 'top-level with line breaks': [ + '__ESCAPED_COMMENT_CLEAN_CSS0(2, 5)__a{}', + [ + { + kind: 'text', + value: '__ESCAPED_COMMENT_CLEAN_CSS0(2, 5)__' + }, + { + kind: 'selector', + value: [{ value: 'a', metadata: { line: 3, column: 6 } }], + body: [] + } + ] + ], + 'in selectors': [ + 'div[data-type=__ESCAPED_FREE_TEXT_CLEAN_CSS0(1,3)__],div[data-id=__ESCAPED_FREE_TEXT_CLEAN_CSS1(0,7)__]{color:red}', + [{ + kind: 'selector', + value: [ + { value: 'div[data-type=__ESCAPED_FREE_TEXT_CLEAN_CSS0(1,3)__]', metadata: { line: 1, column: 1 } }, + { value: 'div[data-id=__ESCAPED_FREE_TEXT_CLEAN_CSS1(0,7)__]', metadata: { line: 2, column: 6 } } + ], + body: [{ value: 'color:red', metadata: { line: 2, column: 27 } }] + }] + ], + 'in properties': [ + 'div{__ESCAPED_COMMENT_CLEAN_CSS0(2,5)__background:url(__ESCAPED_URL_CLEAN_CSS0(0,20)__);color:blue}a{font-family:__ESCAPED_FREE_TEXT_CLEAN_CSS0(1,3)__;color:red}', + [ + { + kind: 'selector', + value: [{ value: 'div', metadata: { line: 1, column: 1 } }], + body: [ + { value: '__ESCAPED_COMMENT_CLEAN_CSS0(2,5)__', metadata: { line: 1, column: 5 }}, + { value: 'background:url(__ESCAPED_URL_CLEAN_CSS0(0,20)__)', metadata: { line: 3, column: 6 } }, + { value: 'color:blue', metadata: { line: 3, column: 43 } } + ] + }, + { + kind: 'selector', + value: [{ value: 'a', metadata: { line: 3, column: 54 } }], + body: [ + { value: 'font-family:__ESCAPED_FREE_TEXT_CLEAN_CSS0(1,3)__', metadata: { line: 3, column: 56 } }, + { value: 'color:red', metadata: { line: 4, column: 5 } } + ] + } + ] + ], + 'in at-rules': [ + '@charset __ESCAPED_FREE_TEXT_CLEAN_CSS0(1, 5)__;div{}', + [ + { + kind: 'at-rule', + value: '@charset __ESCAPED_FREE_TEXT_CLEAN_CSS0(1, 5)__;', + metadata: { line: 1, column: 1 } + }, + { + kind: 'selector', + value: [{ value: 'div', metadata: { line: 2, column: 8 } }], + body: [] + } + ] + ], + 'in blocks': [ + '@media (__ESCAPED_COMMENT_CLEAN_CSS0(2, 1)__min-width:980px){a{color:red}}', + [ + { + kind: 'block', + value: '@media (__ESCAPED_COMMENT_CLEAN_CSS0(2, 1)__min-width:980px)', + metadata: { line: 1, column: 1 }, + isFlatBlock: false, + body: [{ + kind: 'selector', + value: [{ value: 'a', metadata: { line: 3, column: 19 } }], + body: [{ value: 'color:red', metadata: { line: 3, column: 21 } }] + }] + } + ] + ] + }) + ) + .export(module);