digger-selector
Version:
The CSS selector parser used for digger
320 lines (270 loc) • 6.39 kB
JavaScript
module.exports = parse;
module.exports.mini = miniparse;
/*
Quarry.io Selector
-------------------
Represents a CSS selector that will be passed off to selectors or perform in-memory search
*/
/***********************************************************************************
***********************************************************************************
Here is the data structure:
"selector": " > * product.onsale[price<100] > img caption.red, friend",
"phases":
[
[
{
"splitter": ">",
"tag": "*"
},
{
"splitter": "",
"tag": "product",
"classnames": {
"onsale": true
},
"attr": [
{
"field": "price",
"operator": "<",
"value": "100"
}
]
},
{
"splitter": ">",
"tag": "img"
},
{
"splitter": "",
"tag": "caption",
"classnames": {
"red": true
}
}
],
[
{
"tag": "friend"
}
]
]
*/
/*
Regular Expressions for each chunk
*/
var chunkers = [
// the 'type' selector
{
name:'tag',
regexp:/^(\*|\w+)/,
mapper:function(val, map){
map.tag = val;
}
},
// the '.classname' selector
{
name:'class',
regexp:/^\.\w+/,
mapper:function(val, map){
map.class = map.class || {};
map.class[val.replace(/^\./, '')] = true;
}
},
// the '#id' selector
{
name:'id',
regexp:/^#\w+/,
mapper:function(val, map){
map.id = val.replace(/^#/, '');
}
},
// the '=diggerid' selector
{
name:'diggerid',
regexp:/^=[\w-]+/,
mapper:function(val, map){
map.diggerid = val.replace(/^=/, '');
}
},
// the '/path' selector
{
name:'diggerid',
regexp:/^\/[\w\/]+/,
mapper:function(val, map){
map.diggerid = val;
}
},
// the ':modifier' selector
{
name:'modifier',
regexp:/^:\w+(\(.*?\))?/,
mapper:function(val, map){
map.modifier = map.modifier || {};
var parts = val.split('(');
var key = parts[0];
val = parts[1];
if(val){
val = val.replace(/\)$/, '');
if(val.match(/^[\d\.-]+$/)){
val = JSON.parse(val);
}
}
else{
val = true;
}
map.modifier[key.replace(/^:/, '')] = val;
}
},
// the '[attr<100]' selector
{
name:'attr',
regexp:/^\[.*?["']?.*?["']?\]/,
mapper:function(val, map){
map.attr = map.attr || [];
var match = val.match(/\[(.*?)([=><\^\|\*\~\$\!]+)["']?(.*?)["']?\]/);
if(match){
map.attr.push({
field:match[1],
operator:match[2],
value:match[3]
});
}
else {
map.attr.push({
field:val.replace(/^\[/, '').replace(/\]$/, '')
});
}
}
},
// the ' ' or ' > ' or ' < ' or ' , ' or ' | '' splitter
{
name:'splitter',
regexp:/^[ ,<>|]+/,
mapper:function(val, map){
map.splitter = val.replace(/\s+/g, '');
}
}
];
/*
Parse selector string into flat array of chunks
Example in: product.onsale[price<100]
*/
function parseChunks(selector){
var lastMatch = null;
var workingString = selector ? selector : '';
var lastString = '';
// this is a flat array of type, string pairs
var chunks = [];
var matchNextChunk = function(){
lastMatch = null;
for(var i in chunkers){
var chunker = chunkers[i];
if(lastMatch = workingString.match(chunker.regexp)){
// merge the value into the chunker data
var data = {
value:lastMatch[0]
}
for(prop in chunker){
data[prop] = chunker[prop];
}
data.string = lastMatch[0];
chunks.push(data);
workingString = workingString.replace(lastMatch[0], '');
return true;
}
}
return false;
}
// the main chunking loop happens here
while(matchNextChunk()){
// this is the sanity check in case we match nothing
if(lastString==workingString){
break;
}
}
return chunks;
}
function new_selector(){
return {
string:'',
class:{},
attr:[],
modifier:{}
}
}
/*
turns a selector string into an array of arrays (phases) of selector objects
*/
function parse(selector_string, firstchunk){
if(typeof(selector_string)!='string'){
return selector_string;
}
var chunks = parseChunks(selector_string);
var phases = [];
var currentPhase = [];
var currentSelector = new_selector();
var addCurrentPhase = function(){
if(currentPhase.length>0){
phases.push(currentPhase);
}
currentPhase = [];
}
var addCurrentSelector = function(){
if(Object.keys(currentSelector).length>0){
currentPhase.push(currentSelector);
}
currentSelector = new_selector();
}
var addChunkToSelector = function(chunk, selector){
chunk.mapper.apply(null, [chunk.value, selector]);
selector.string += chunk.string;
}
chunks.forEach(function(chunk, index){
if(chunk.name=='splitter' && chunk.value.match(/,/)){
addCurrentSelector();
addCurrentPhase();
}
else{
if(chunk.name=='splitter' && index>0){
addCurrentSelector();
}
addChunkToSelector(chunk, currentSelector);
}
})
addCurrentSelector();
addCurrentPhase();
if(firstchunk){
return phases[0][0]
}
else{
return {
string:selector_string,
phases:phases
}
}
}
function miniparse(selector_string){
if(typeof(selector_string)!=='string'){
return selector_string;
}
selector_string = selector_string || '';
var selector = {
class:{},
modifier:{}
}
selector_string = selector_string.replace(/_(\w+)/, function(match, id){
selector.id = id;
return '';
})
selector_string = selector_string.replace(/\.(\w+)/g, function(match, classname){
selector.class[classname] = true;
return '';
})
if(selector_string.match(/\d/)){
selector.diggerid = selector_string;
}
else{
selector.tag = selector_string;
}
return selector;
}