# Use Custom CSS Parser

If you just use GrapesJS for building templates from scratch, so you start from an empty canvas and for editing you strictly rely on the generated JSON (final HTML/CSS only for end-users) then, probably, you might skip this guide. On the other hand, if you import templates from already defined HTML/CSS or let the user embed custom codes (eg. using the grapesjs-custom-code (opens new window) plugin), then you have to know that you might face strange behaviors.

WARNING

This guide requires GrapesJS v0.14.33 or higher

# Import HTML/CSS

Importing already defined HTML/CSS is a really good feature as it lets you start editing immediately any kind of template and obviously, GrapesJS itself promotes this kind of approach

<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 fast and easier GrapesJS needs to compile a simple string (HTML/CSS) into structured nodes (nested JS objects). Fortunately, most of the hard work (parsing) is already done by the browser itself which translates that string into its own objects (DOM (opens new window)/CSSOM (opens new window)) and so we just rely on those, by traversing them and creating our nodes (unfortunately browser's objects are not enough). The fact we're able to parse our strings just by using the browser itself it's very cool, we can enable the import feature without requiring any third-party library, so... where is the problem? Well, while the generated DOM is performing quite well, as we're able to extract what we need, unfortunately, it's not the same for the CSSOM, so let's see in the next paragraph what is wrong with it.

# CSSOM results are inconsistent

Unfortunately, we have discovered that the CSSOM generated by browsers are highly inconsistent from what we ask to parse. To demonstrate it, we gonna create a simple example by using the built-in parser and we'll check its result. So, for our case we just take in account a simple rule, we'll 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>

# Results

Here some results (using latest versions + IE11)

As you see, this is what we get for asking only 7 properties, who adds more or less, someone converts colors to rgba functions and someone else changes the order of our values (eg. box-shadow). Webkit-based browsers attach also properties they self don't understand

So it's clear that we can't rely on CSSOM objects, that's why we added the possibility to set custom CSS parser via editor.setCustomParserCss method or config.Parser.parserCss option to use on initialization. Let's see in detail how it's expected to work

# CSSOM results can be nonintuitive

As per current csswg specification (opens new window) variables in shorthand properties can serialize to the empty string. This means that while background-color: var(--my-var) will serialize fine background: var(--my-var) will not.

# Set CSS parser

The custom parser you have to use it's just a function receiving 2 arguments: css, as the CSS string to parse, and editor, the instance of the current editor. As the result, you should return an array containing valid rule objects, the syntax of those objects are 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; // Result should be ALWAYS an array
};

// On initialization
// This is the recommended way, as you gonna use the parser from the beginning
const editor = grapesjs.init({
  //...
  parser: {
    parserCss,
  }
});

// Or later, via editor API
editor.setCustomParserCss(parserCss);

# Rule Objects

The syntax of rule objects is pretty straightforward, each object might contain 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 more clear let's see 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',
    },
  },
]

# Plugins

Below the list of current available CSS parsers as plugins, if you need to create your own we highly suggest to explore their sources