docs/guides/Custom-CSS-parser.md
If you use GrapesJS only to build templates from scratch, starting from an empty canvas and relying strictly on the generated JSON for editing (with final HTML/CSS only for end users), you can probably skip this guide. On the other hand, if you import templates from existing HTML/CSS or let users embed custom code (eg. using the grapesjs-custom-code plugin), you should know that you might face some unexpected behaviors.
::: warning This guide requires GrapesJS v0.14.33 or higher :::
[[toc]]
Importing existing HTML/CSS is a great feature because it lets you start editing any kind of template immediately, and GrapesJS itself promotes this kind of workflow.
<div id="gjs">
<div class="txt-red">Hello world!</div>
<style>
.txt-red {
color: red;
}
</style>
</div>
<script type="text/javascript">
const editor = grapesjs.init({
container: '#gjs',
fromElement: true,
});
</script>
To work quickly and efficiently, GrapesJS needs to compile a simple string (HTML/CSS) into structured nodes (nested JS objects). Fortunately, most of the hard parsing work is already done by the browser itself, which translates that string into its own objects (DOM/CSSOM), so we can traverse them and build our own nodes from there. Browser objects alone are not enough, but they give us a strong starting point.
Being able to parse strings just by using the browser is great, because it lets us support import without requiring any third-party library. So where is the problem? While the generated DOM works quite well and lets us extract what we need, the same is not true for the CSSOM. In the next paragraph, we'll see why.
Unfortunately, we discovered that the CSSOM generated by browsers can be highly inconsistent compared to the CSS we ask them to parse. To demonstrate this, we'll create a simple example using the built-in parser and inspect its result.
For this example, we'll use a simple rule, parse it, and print the CSSOM result on screen.
<h1>To parse</h1>
<pre id="css-to-parse">
.simple-class {
background-image:url("https://image1.png"), url("https://image2.jpg");
background-attachment: fixed, scroll;
background-position:left top, center center;
background-repeat:repeat-y, no-repeat;
background-size: contain, cover;
box-shadow: 0 0 5px #9d7aa5, 0 0 10px #e6c3ee;
border: 2px solid #FF0000;
}
</pre>
<h1>Result</h1>
<pre id="result"></pre>
<script>
// We use ES5 just to make it more cross-browser, without the need of being compiled
function parse(str) {
var result = [];
// Create the element which will contain the style to parse
var el = document.createElement('style');
el.innerHTML = str;
// We have to append the style to get its CSSOM
document.head.appendChild(el);
var sheet = el.sheet;
// Now we can remove it
document.head.removeChild(el);
return sheet;
}
function CSSOMToString(root) {
// For the sake of brevity we just print what we need
var styleStr = '';
var rule = root.cssRules[0];
var style = rule.style;
// The only way we have to iterate over CSSStyleDeclaration
for (var i = 0, len = style.length; i < len; i++) {
var property = style[i];
var value = style.getPropertyValue(property);
styleStr += '\t' + property + ': ' + value + ';\n';
}
var result = document.getElementById('result');
result.innerHTML = rule.selectorText + ' {\n' + styleStr + '}';
}
var css = document.getElementById('css-to-parse').innerText;
CSSOMToString(parse(css));
</script>
Here are some results (using the latest browser versions plus IE11).
As you can see, this is what we get when asking for only 7 properties: some browsers add more properties, some convert colors to rgba(...), and others change the order of values (eg. box-shadow). WebKit-based browsers also attach properties they don't even understand themselves.
So it's clear that we can't rely on CSSOM objects. That's why we added the ability to set a custom CSS parser via the editor.setCustomParserCss method or the config.Parser.parserCss option during initialization. Let's see in detail how this is expected to work.
According to the current CSSWG specification, variables in shorthand properties can serialize to an empty string.
This means that while background-color: var(--my-var) serializes correctly, background: var(--my-var) does not.
The custom parser is just a function that receives 2 arguments: css, the CSS string to parse, and editor, the current editor instance. It should return an array of valid rule objects. The syntax of those objects is explained below. This is how you can set the custom parser.
const parserCss = (css, editor) => {
const result = [];
// ... parse the CSS string
result.push({
selectors: '.someclass, div .otherclass',
style: { color: 'red' },
});
// ...
return result; // The result should always be an array
};
// On initialization
// This is the recommended way, as you'll use the parser from the beginning
const editor = grapesjs.init({
//...
parser: {
parserCss,
},
});
// Or later, via editor API
editor.setCustomParserCss(parserCss);
The syntax of rule objects is pretty straightforward. Each object can contain the following keys.
| Key | Description | Example |
|---|---|---|
selectors | Selectors of the rule. | |
| REQUIRED return an empty string in case the rule has no selectors | .class1, div > #someid | |
style | Style declarations as an object | { color: 'red' } |
atRule | At-rule name | media |
params | Parameters of the at-rule | screen and (min-width: 480px) |
To make it clearer, let's look at a few examples.
// Input
`
@font-face {
font-family: "Font Name";
src: url("https://font-url.eot");
}
`
// Output
[
{
selectors: '',
atRule: 'font-face',
style: {
'font-family': '"Font Name"',
src: 'url("https://font-url.eot")',
},
}
]
// Input
`
@keyframes keyframe-name {
from { opacity: 0; }
to { opacity: 1; }
}
`
// Output
[
{
params: 'keyframe-name',
selectors: 'from',
atRule: 'keyframes',
style: {
opacity: '0',
},
}, {
params: 'keyframe-name',
selectors: 'to',
atRule: 'keyframes',
style: {
opacity: '1',
},
}
]
// Input
`
@media screen and (min-width: 480px) {
body {
background-color: lightgreen;
}
.class-test, .class-test2:hover {
color: blue !important;
}
}
`
// Output
[
{
params: 'screen and (min-width: 480px)',
selectors: 'body',
atRule: 'media',
style: {
'background-color': 'lightgreen',
},
}, {
params: 'screen and (min-width: 480px)',
selectors: '.class-test, .class-test2:hover',
atRule: 'media',
style: {
color: 'blue !important',
},
}
]
// Input
`
:root {
--some-color: red;
--some-width: 55px;
}
`
// Output
[
{
selectors: ':root',
style: {
'--some-color': 'red',
'--some-width': '55px',
},
},
]
Below is the list of currently available CSS parser plugins. If you need to create your own, we highly recommend exploring their source code.