Parsing CSS in JavaScript/Jquery

Parsing CSS in JavaScript / jQuery

There is a CSS parser written in Javascript called JSCSSP

CSS parser/abstracter? How to convert stylesheet into object

On the client-side, a stylesheet is already an object; it gets parsed into a tree when the page loads.

Lets say you have an HTML page starting with

<html xmlns="http://www.w3.org/1999/xhtml"> 
<head>
<link href="/styles/global.css" rel="stylesheet" type="text/css" media="screen"/>
<link href="/styles/catalog.css" rel="stylesheet" type="text/css" media="screen"/>
<link href="/styles/banner.css" rel="stylesheet" type="text/css" media="screen"/>

and global.css contains the lines

#page { background-color: #fff; margin-top: 20px; margin-bottom: 20px; text-align: left; width: 100%; margin-top: 20px; padding: 0px; min-width: 900px; border: none; }
#header { background-color: transparent; }
#main { background-color: transparent; margin-left: 10px; margin-right: 10px; padding: 8px 0px 8px 0px;}
#sidebar { margin-right: 12px; }

Then, in order to find what's set for background-color of #page, you'd need to write document.styleSheets[0].cssRules[0].style.backgroundColor, which would return #fff (or rgb(255, 255, 255) on some browsers, I think).

Other useful stuff assuming the above sheets:

document.styleSheets[0].cssRules[3].cssText //"#sidebar { margin-right: 12px; }"
document.styleSheets[0].cssRules[2].selectorText //"#main"

If you had a more complex selector, like #main div.header a, #main div.header a:visited, then the selector text property returns the entire thing, not just the first bit.

Your specific question is "How can I find out what is set in the stylesheet for a given selector". Here's one way to approximate it:

function findProperty(selector) {
rules = document.styleSheets[0].cssRules
for(i in rules) {
if(rules[i].selectorText==selector) return rules[i].cssText;
}
return false;
}

findProperty('#sidebar'); //returns "#sidebar { margin-right: 12px; }"

The thing is that the CSS tree you have access to in this way has already been parsed by the browser (hence the 'approximate' above). If you're in Firefox, you won't see any -webkit styles because mozilla just drops them. Various browsers also tend to have their own way of displaying colors (as above; if your actual .css file has a given color set to #fff, and you check it in JavaScript after it's parsed, you might get back #fff, #ffffff or rgb(255, 255, 255)).

The above will tell you what your browser sees that CSS sheet as; if you want to know what specific ascii characters were contained in the initial .css file, the only reliable way is to look at the file itself, AFAIK.

A reference for the stylesheet object is available here.

Parse Text CSS into JSON with Javascript

This js parser has both methods you are looking for.

CSS JSON parser

// To JSON
var json = CSSJSON.toJSON(cssString);

// To CSS
var css = CSSJSON.toCSS(jsonObject);

Or jQuery plugin parser.

jQuery parser

Example css:

div div:first {
font-weight: bold;
-jquery: each(function() {alert(this.tagName);})
}

div > span {
color: red;
}

JSON output sent to the callback:

{
'div div:first' : {
'font-weight' : 'bold',
'-jquery': 'each(function() {alert(this.tagName);})'
},
'div > span' : {
'color': 'red'
}
}

You can apply css string to an element like this:

var cssJSON = '{ "display": "none" }';
var obj = JSON.parse(json);

$("#element").css(obj);

Parsing Returned Values of Class or Contains Selectors in jQuery

The disconnect you are having is between the jQuery API and the native JavaScript API.

With jQuery, when you use a selector, internally it will mock an array. The array is essentially inside of the jQuery object. The internal array is the set of actual matched elements on the page, and those elements will have the native JavaScript API functions available to them. The entire jQuery object as a whole will have the jQuery API exposed to it.

Here is what that looks like in code, and how it applies to your examples.

Your links selector is just fine, "a.a.x". You have an anchor tag with both class "a" and class "x" and that is how you would select the set of tags matching those criteria. So you have

var links = $("a.a.x");

One thing to remember is that $ is just shorthand for jQuery which is a function. So you are calling that function and it is returning (through some internal process) an instance of jQuery. So links is now referencing the instance of jQuery. Internally, that instance (jQuery object) contains a variable with an array of the elements matched from the selector (argument) used with the jQuery call. This array can be accessed (as a result of jQuery's internal process exposing the elements) by simply using index numbers.

You are accessing the array properly (or at least without error) here:

for (var i = 0; i < links.length; i++) {
console.log(links[i].attr('href'));
};

But there is an issue. Remember that the internal array of the jQuery object held the elements matched? links[i] here is the actual element, and is exposing the native JavaScript API. As a result, .attr is not available. The native version of the getter for attributes is "getAttribute", and you could change your above code to

for (var i = 0; i < links.length; i++) {
console.log(links[i].getAttribute('href'));
};

You could also construct a jQuery object with links[i] and then access the jQuery API.

for (var i = 0; i < links.length; i++) {
console.log($(links[i]).attr('href'));
};

Which begins to be redundant as you hopefully see. By similar construct, and because links is a jQuery object, you can access the jQuery API functions. That is why this code console.log(links.attr('href')); will result in the href attribute. However, as it is a getter, jQuery will internally only return the first result of the set. In order to use an iterative approach, jQuery exposes the function each. It is very useful and is used extensively behind the scenes. For example links.css('color','blue') will internally use each in order to iterate the entire set of links and change their text color to blue.

Your best best here would be to stick to using each.

$("a.a.x").each(function(){
//this will now be available as the native element similar to links[i]
console.log(this.href);//href is natively available as well
cosnole.log(this.getAttribute('href'));//also available here
console.log($(this).attr('href'));//and here when constructing a jQuery object as well
});

How to convert a JSON object (created by jquery-css-parser) back to valid CSS code?

Try this:

var cssString = "";

// cssJSON would be the JavaScript object representing your CSS.
for (var objKey in cssJSON) {

// objKey is the name of the key in the JavaScript object.
// In this case, it's also the CSS selector.
cssString += objKey + " {";

var cssProperties = cssJSON[objKey];
for (var cssPropertyName in cssProperties) {
cssString += cssPropertyName + ": " + cssProperties[cssPropertyName] + ";";
}

cssString += "}";
}

Be advised: I have not tested this code, so please use at your own risk. There may be some edge cases that this doesn't account for.

Get CSS value as written in stylesheet with jQuery or RegEx

You shouldn't really do or even need this in a real-life application but since you say this is just for fun; here is the fun part:

Yes you can do it. Loading the CSS file via AJAX calls is not the only way but it may be the only (but inefficient) way to make it really cross-browser. Even the declaration below won't make it truly cross-browser since calc() function is not supported by all browsers and it's a CSS3 feature.

EDIT: As of 2018, calc is supported by all modern browsers.

div {
width: 300px; /* a fallback value for old browsers */
width: -webkit-calc(100% - 50px);
width: -moz-calc(100% - 50px);
width: calc(100% - 50px);
}

You can get the raw CSS code from document.styleSheets and their rules. The cssText property of the rule will give you the full statement. But different browsers may parse the values with a calc() function differently.

I'll go with a more complex example to see how browsers treat the calc() function:

calc(100% - -50px*6 + 4em/2);

This is how Firefox (v.18) treats it:

calc() function in Firefox

And this is how Google Chrome (v.24) treats it:

calc() function in Chrome

As shown; FF gets the non-prefixed calc value as it is and Chrome gets the -webkit prefixed value and re-parses it with nested parenthesis (if needed). If you don't declare it -webkit in Chrome; it will completely ignore the value. So, we should take these into account when we manipulate the calc statement.

Now, using the complex example; we will first get the statement inside the calc() function:

"100% - -50px*6 + 4em/2"

Then dissolve the elements of statement into an array:

["100%", "-", "-50px", "*", "6", "+", "4em", "/", "2"]

Finally, process the values and units of the array items to make them programmatically usable (as you wanted):

[{ value:100, unit:"%" }, "-", { value:-50, unit:"px" }, "*", { value:6, unit:undefined }, "+", { value:4, unit:"em" }, "/", { value:2, unit:undefined }]

The final result above includes the value-objects and operators (in order).

Before reading further; note that the code below is not completely tested on all browsers and situations. It does not handle nested parenthesis parsing (like Chrome does) or, values with multiple or combined calc() functions. If you want to test this; I recommend Firefox since it does not parse nested parenthesis OR you can extend the code to enable support for them.

// Get the sheet you want to process. (assume we want to process the third sheet):
var sheet = document.styleSheets[2]; //you could also iterate all the sheets in a for loop
processRules(sheet);
/** Iterates through the rules of the specified style sheet;  
* then dissolves and logs values including a calc() function.
*/
function processRules(sheet) {
var rules = sheet.cssRules // Mozilla, Safari, Chrome, Opera
|| sheet.rules; // IE, Safari
for (var i = 0; i < rules.length; i++) {
var rule = rules[i];
// Check if we have a calc() function in this rule
if (hasCalc(rule.cssText)) {
// Get the calculation statement inside the calc() function.
var statement = getCalcStatement(rule.cssText);
// Dissolve the statement into its elements and log.
console.log(dissolveCalcElements(statement));
}
}
}
/** Checks whether the CSS value includes a calc() function, 
* (This is also for avoiding unnecessary regex.)
*/
function hasCalc(value) {
return value.toLowerCase().indexOf('calc(') >= 0;
}

/** Gets the full statement (string) inside a calc() function.
*/
function getCalcStatement(rule) {
if (!rule) return '';
var pattern = /calc\(([^\)]+)\).*/;
var match = pattern.exec(rule);
return match && match.length > 1 ? match[1] : '';
}

/** Splits the calc operation's elements (values and operators) and
* dissolves the values into objects with value and unit properties.
*/
function dissolveCalcElements(statement) {
// The CSS calc() function supports 4 basic math operations only:
// Addition (+), Subtraction (-), Multiplication (*), Division (/)
// White-spaces are very important in a calc statement.
// From Mozilla: "The + and - operators must always be surrounded by whitespace.
// The * and / operators do not require whitespace, but adding it for consistency is allowed, and recommended."
// We could use: statement.split(/(\s+[\+\-]\s+|\s*[\*\/]\s*)/);
// to include the operators inside the output array, but not all browsers
// support splicing the capturing parentheses into the array like that. So:
statement = statement.replace('*', ' * ').replace('/', ' / ');
var arr = statement.split(/\s+/);

console.log("arr", arr);
var calcElems = [];
for (var i = 0; i < arr.length; i++) {
var d = dissolveElement(arr[i]);
calcElems.push(d);
}
return calcElems;
}

/** Dissolves the value and unit of the element and
* returns either the operator or an object with "value" and "unit" properties.
*/
function dissolveElement(val) {
// Check if the value is an operator.
var ops = '+-*/';
if (ops.indexOf(val) >= 0) return val;

var o = {};
// CSS units in a calc statement can have all the regular units.
// According to W3C; they can also, can include a "vw" unit (stands for viewport).
var pattern = /([\+\-]?[0-9\.]+)(%|px|pt|em|in|cm|mm|ex|pc|vw)?/;
// Exec the value/unit pattern on the property value.
var match = pattern.exec(val);
// So we reset to the original value if there is no unit.
if (match) {
var v = match.length >= 2 ? match[1] : match[0];
o.value = toFloat(v); //parse value as float
o.unit = match.length >= 3 ? match[2] : '';
}
else {
o = { value:val, unit:''};
}
console.log("dissolve", match, val, o);
return o;
}

// Helper Functions
function toFloat(value) { return parseFloat(value) || 0.0; }

That's it. As I mentioned, I wouldn't do this but it's always good to know if anything is possible.

Note: Since you mentioned making a jQuery plugin (for fun); you don't really need jQuery for this. And calling functions like $('div').css('width') will only give you the computed value, not the raw calc statement.



Related Topics



Leave a reply



Submit