0

I am trying to match a function or mixin used in an SCSS string so I may remove it but I am having a bit of trouble.

For those unfamiliar with SCSS this is an example of the things I am trying to match (from bootstrap 4).

@mixin _assert-ascending($map, $map-name) {
  $prev-key: null;
  $prev-num: null;
    @each $key, $num in $map {
     @if $prev-num == null {
  // Do nothing
    } @else if not comparable($prev-num, $num) {
      @warn "Potentially invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} whose unit makes it incomparable to #{$prev-num}, the value of the previous key '#{$prev-key}' !";
    } @else if $prev-num >= $num {
  @warn "Invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} which isn't greater than #{$prev-num}, the value of the previous key '#{$prev-key}' !";
 }
  $prev-key: $key;
  $prev-num: $num;
 }
}

And a small function:

@function str-replace($string, $search, $replace: "") {
  $index: str-index($string, $search);
  @if $index {
    @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
  }
  @return $string;
}

So far I have the following regex:

@(function|mixin)\s?[[:print:]]+\n?([^\}]+)

However it only matches to the first } that it finds which makes it fail, this is because it needs to find the last occurance of the closing curly brace.

My thoughts are that a regex capable of matching a function definition could be adapted but I can't find a good one using my Google foo!

Thanks in advance!

James Kipling
  • 121
  • 10

3 Answers3

0

I would not recommend to use a regex for that, since a regex is not able to handle recursion, what you might need in that case.

For Instance:

@mixin test {
  body {
  }
}

Includes two »levels« of scope here ({{ }}), so your regex should be able to to count brackets as they open and close, to match the end of the mixin or function. But that is not possible with a regex.

This regex

/@mixin(.|\s)*\}/gm

will match the whole mixin, but if the input is like that:

@mixin foo { … }

body { … }

It will match everything up to the last } what includes the style definition for the body. That is because the regex cannot know which } closes the mixin.

Have a look at this answer, it explains more or less the same thing but based on matching html elements.

Instead you should use a parser, to parse the whole Stylesheet into syntax tree, than remove unneeded functions and than write it to string again.

philipp
  • 15,947
  • 15
  • 61
  • 106
0

In fact, like @philipp said, regex can't replace syntax analysis like compilers do.

But here is a sed command which is a little ugly but could make the trick :

sed -r -e ':a' -e 'N' -e '$!ba' -e 's/\n//g' -e 's/}\s*@(function|mixin)/}\n@\1/g' -e 's/^@(function|mixin)\s*str-replace(\s|\()+.*}$//gm' <your file>

  • -e ':a' -e 'N' -e '$!ba' -e 's/\n//g' : Read all file in a loop and remove the new line (See https://stackoverflow.com/a/1252191/7990687 for more information)
  • -e 's/}\s*@(function|mixin)/}\n@\1/g' : Make each @mixin or @function statement the start of a new line, and the preceding } the last character of the previous line
  • 's/^@(function|mixin)\s*str-replace(\s|\()+.*}$//gm' : Remove the line corresponding to the @function str-replace or @mixin str-replace declaration

But it will result in an output that will loose indentation, so you will have to reindent it after that.

I tried it on a file where I copy/paste multiple times the sample code you provided, so you will have to try it on your file because there could be cases where the regex will match more element than wanted. If it is the case, provide us a test file to try to resolve these issues.

Esteban
  • 1,752
  • 1
  • 8
  • 17
  • Thanks. Unfortunatley it's unknown what the function or mixin names are at runtime. The main issue is that there are variable that are being matched inside these functions/mixins which I don't want matched so my idea was to strip them out before the variable matching takes place. – James Kipling May 31 '17 at 08:06
0

After much headache here is the answer to my question!

The source needs to be split line by line and read, maintining a count of the open / closed braces to determine when the index is 0.

$pattern = '/(?<remove>@(function|mixin)\s?[\w-]+[$,:"\'()\s\w\d]+)/';
$subject = file_get_contents('vendor/twbs/bootstrap/scss/_variables.scss'); // just a regular SCSS file containing what I've already posted.
$lines = explode("\n",$subject);
$total_lines = count($lines);

foreach($lines as $line_no=>$line) {
  if(preg_match($pattern,$line,$matches)) {
    $match = $matches['remove'];
    $counter = 0;
    $open_braces = $closed_braces = 0;
      for($i=$line_no;$i<$total_lines;$i++) {
        $current = $lines[$i];
        $open_braces = substr_count($current,"{");
        $closed_braces = substr_count($current,"}");
        $counter += ($open_braces - $closed_braces);

        if($counter==0) {
          $start = $line_no;
          $end = $i;
           foreach(range($start,$end) as $a) {
             unset($lines[$a]);
           } // end foreach(range)
        break; // break out of this if!
       } // end for loop
      } // end preg_match
     } // endforeach

And we have a $lines array without any functions or mixins.

There is probably a more elegant way to do this but I don't have the time or the willing to write an AST parser for SCSS

This can be quite easily adapted into making a hacked one however!

James Kipling
  • 121
  • 10