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 <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits