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