coren has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/282155

Change subject: [WIP] CSS parser and renderer classes
......................................................................

[WIP] CSS parser and renderer classes

This is the preliminary code for the CSSParser and CSSRenderer
classes that are intended to parse a strings containing style
sheets, and to render collections of parsed style sheets in a
sane manner.

The only at-rules that are supported are @media blocks, and
the current code does not yet filter declarations.

Change-Id: Ibc1cae3079d164f7ac7bcc7c4ded3f02bb048614
---
A CSSParser.php
A CSSRenderer.php
A tests/css_parse_test.php
3 files changed, 277 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/TemplateStyles 
refs/changes/55/282155/1

diff --git a/CSSParser.php b/CSSParser.php
new file mode 100644
index 0000000..98a22eb
--- /dev/null
+++ b/CSSParser.php
@@ -0,0 +1,183 @@
+<?php
+/**
+ * @file
+ * @ingroup Extensions
+ */
+
+/**
+ * Represents a style sheet as a structured tree, organized
+ * in rule blocks nested in at-rule blocks.
+ *
+ * @class
+ */
+class CSSParser {
+
+       private $tokens;
+       private $index;
+
+       /**
+        * Parse and (minimally) validate the passed string as a CSS, and
+        * constructs an array of tokens for parsing, as well as an index
+        * into that array.
+        *
+        * Internally, the class behaves as a lexer.
+        *
+        * @param string $css
+        */
+       function __construct( $css ) {
+               preg_match_all( "/(
+                         [ \\n\\t]+
+                       | \\/\\* (?: [^*\\n]+ | \\*[^\\/] )* \\*\\/ [ \\n\\t]*
+                       | \" (?: [^\"\\\\\\n]+ | \\\\. ) [\"\\n]
+                       | ' (?: [^'\\\\\\n]+ | \\\\. ) ['\\n]
+                       | [+-]? (?: [0-9]* \. )? [0-9]+ (?: [_a-z][_a-z0-9-]* | 
% )?
+                       | url [ \\n\\t]* \\(
+                       | @? -? (?: [_a-z] | \\\\[0-9a-f]{1,6} [ \\n\\t]? )
+                               (?: [_a-z0-9-]+ | \\\\[0-9a-f]{1,6} [ \\n\\t]? 
| [^\\0-\\177] )*
+                       | \\# (?: [_a-z0-9-]+ | \\\\[0-9a-f]{1,6} [ \\n\\t]? | 
[^\\0-\\177] )*
+                       | u\\+ [0-9a-f]{1,6} (?: - [0-9a-f]{1,6} )?
+                       | u\\+ [0-9a-f?]{1,6}
+                       | <!--
+                       | -->
+                       | .)/xis", $css, $match );
+               foreach ( $match[0] as $t ) {
+                       $space = false;
+                       if ( preg_match( "/^[ \\n\\t]|\\/\\*|<|-->/", $t ) ) {
+                               if ( !$space ) {
+                                       $space = true;
+                                       $this->tokens[] = ' ';
+                                       continue;
+                               }
+                       } else {
+                               $space = false;
+                               $this->tokens[] = $t;
+                       }
+               }
+               $this->index = 0;
+       }
+
+       private function i( $i ) {
+               if ( $this->index+$i >= count( $this->tokens ) )
+                       return null;
+               return $this->tokens[$this->index+$i];
+       }
+
+       private function consume( $num = 1 ) {
+               if ( $num > 0 ) {
+                       if ( $this->index+$num >= count( $this->tokens ) )
+                               $num = count( $this->tokens ) - $this->index;
+                       $text = implode( array_slice( $this->tokens, 
$this->index, $num ) );
+                       $this->index += $num;
+                       return $text;
+               }
+               return '';
+       }
+
+       private function skip_to( $delim ) {
+               for( $i=0; $this->i( $i )!==null and !in_array( $this->i( $i ), 
$delim ); $i++ )
+                       ;
+               return $this->consume( $i );
+       }
+
+       private function skip_ws() {
+               $spaces = 0;
+               while ( $this->i( $spaces ) == ' ' )
+                       $spaces++;
+               $this->consume( $spaces );
+       }
+
+       private function parse_decl() {
+               $this->skip_ws();
+               $name = $this->consume();
+               $this->skip_ws();
+               if ( $this->i( 0 )!=':' ) {
+                       $this->skip_to( [';', '}'] );
+                       if ( $this->i( 0 ) == ';' ) {
+                               $this->consume();
+                               $this->skip_ws();
+                       }
+                       return null;
+               }
+               $this->consume();
+               $this->skip_ws();
+               $value = $this->skip_to( [';', '}'] );
+               if ( $this->i( 0 ) == ';' ) {
+                       $value .= $this->consume();
+                       $this->skip_ws();
+               }
+               return [ $name => $value ];
+       }
+
+       private function parse_decls() {
+               $decls = [];
+               while ( $this->i( 0 ) !== null and $this->i( 0 ) != '}' ) {
+                       $decl = $this->parse_decl();
+                       if ( $decl )
+                               foreach ( $decl as $k => $d )
+                                       $decls[$k] = $d;
+               }
+               if ( $this->i( 0 ) == '}' )
+                       $this->consume();
+               return $decls;
+       }
+
+       public function parse_rule() {
+               $selectors = [];
+               $text = '';
+               $this->skip_ws();
+               while ( !in_array( $this->i( 0 ), ['{', ';', null] ) ) {
+                       if ( $this->i( 0 ) == ',' ) {
+                               $selectors[] = $text;
+                               $this->consume();
+                               $this->skip_ws();
+                               $text = '';
+                       } else
+                               $text .= $this->consume();
+               }
+               $selectors[] = $text;
+               if ( $this->i( 0 ) == '{' ) {
+                       $this->consume();
+                       return [ "selectors"=>$selectors, 
"decls"=>$this->parse_decls() ];
+               }
+               return null;
+       }
+
+       /**
+        * Parses the token array, and returns a tree representing the CSS 
suitable
+        * for feeding CSSRenderer objects.
+        *
+        * @param array $end An array of string representing tokens that can 
end the parse.  Defaults
+        *  to ending only at the end of the string.
+        * @return array A tree describing the CSS rule blocks.
+        */
+       public function rules( $end = [ null ] ) {
+               $atrules = [];
+               $rules = [];
+               $this->skip_ws();
+               while ( !in_array( $this->i( 0 ), $end ) ) {
+                       if ( $this->i( 0 )[0] == '@' ) {
+                               $at = $this->consume();
+                               $this->skip_ws();
+                               $text = '';
+                               while ( !in_array( $this->i( 0 ), ['{', ';', 
null] ) )
+                                       $text .= $this->consume();
+                               if ( $this->i( 0 ) == '{' ) {
+                                       $this->consume();
+                                       $r = $this->rules( [ '}', null ] );
+                                       if ( $r )
+                                               $atrules[] = [ "name"=>$at, 
"text"=>$text, "rules"=>$r ];
+                               } else
+                                       $atrules[] = [ "name"=>$at, 
"text"=>$text ];
+                       } else
+                               $rules[] = $this->parse_rule();
+                       $this->skip_ws();
+               }
+               if ( $rules )
+                       $atrules[] = [ "name"=>'', "rules"=>$rules ];
+               if ( $this->i( 0 ) !== null )
+                       $this->consume();
+               return $atrules;
+       }
+
+}
+
diff --git a/CSSRenderer.php b/CSSRenderer.php
new file mode 100644
index 0000000..dea0a2c
--- /dev/null
+++ b/CSSRenderer.php
@@ -0,0 +1,84 @@
+<?php
+
+
+/**
+ * @file
+ * @ingroup Extensions
+ */
+
+/**
+ * Collects parsed CSS trees, and merges them for rendering into text.
+ *
+ * @class
+ */
+class CSSRenderer {
+
+       private $bymedia;
+
+       function __construct() {
+               $this->bymedia = [];
+       }
+
+       /**
+        * Adds (and merge) a parsed CSS tree to the render list.
+        *
+        * @param array $rules The parsed tree as created by CSSParser::rules()
+        * @param string $media Forcibly specified @media block selector.  
Normally unspecified
+        *  and defaults to the empty string.
+        */
+       function add( $rules, $media = '' ) {
+               if ( !array_key_exists( $media, $this->bymedia ) )
+                       $this->bymedia[$media] = [];
+
+               foreach ( $rules as $at ) {
+                       switch( $at['name'] ) {
+                               case '@media':
+                                       if ( $media == '' )
+                                               $this->add( $at['rules'], 
"@media ".$at['text'] );
+                                       break;
+                               case '':
+                                       $this->bymedia[$media] = array_merge( 
$this->bymedia[$media], $at['rules'] );
+                                       break;
+                       }
+               }
+       }
+
+       /**
+        * Renders the collected CSS trees into a string suitable for inclusion
+        * in a <style> tag.
+        *
+        * @param string $selector A selector prefixed to every rules' 
selectors, to provide
+        *  scoping.  Defaults to the empty string.
+        * @return string Rendered CSS
+        */
+       function render($selector = '') {
+
+               $css = '';
+
+               if($selector != '')
+                       $selector .= ' ';
+
+               foreach ( $this->bymedia as $at => $rules ) {
+                       if ( $at != '' )
+                               $css .= "$at {\n";
+                       foreach ( $rules as $rule ) {
+                               $css .= implode( ',',
+                                       array_map(
+                                               function($s) use ($selector) { 
return "$selector$s"; },
+                                               $rule['selectors']
+                                       ) ) . "{";
+                               foreach ( $rule['decls'] as $key => $value ) {
+                                       $css .= "$key:$value";
+                               }
+                               $css .= "}\n";
+                       }
+                       if ( $at != '' )
+                               $css .= "}\n";
+               }
+
+               return $css;
+       }
+
+}
+
+
diff --git a/tests/css_parse_test.php b/tests/css_parse_test.php
new file mode 100644
index 0000000..b390516
--- /dev/null
+++ b/tests/css_parse_test.php
@@ -0,0 +1,10 @@
+<?php
+
+require('CSSParser.php');
+require('CSSRenderer.php');
+
+$tree = new CSSParser( file_get_contents( $argv[1] ) );
+$renderer = new CSSRenderer();
+$renderer->add( $tree->rules() );
+echo $renderer->render('.some-template');
+

-- 
To view, visit https://gerrit.wikimedia.org/r/282155
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ibc1cae3079d164f7ac7bcc7c4ded3f02bb048614
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/TemplateStyles
Gerrit-Branch: master
Gerrit-Owner: coren <m...@uberbox.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to