jenkins-bot has submitted this change and it was merged.

Change subject: TemplateStyles extension prototype
......................................................................


TemplateStyles extension prototype

This extension adds a <templatestyles> tag that, when placed
on a template, allows specifying CSS for pages where that
template is transcluded.

Unlike inline styles, the per-template CSS supports rules
with proper selectors, and @media blocks.

THIS VERSION DOES NOT CURRENTLY FILTER DECLARATIONS and is
therefore unsuitable for wikis where unprivileged users should
not be allowed to influcence the pagewide CSS in unrestricted
ways!

Bug: T483
Change-Id: Ibc1cae3079d164f7ac7bcc7c4ded3f02bb048614
---
A .jscsrc
A .jshintrc
A CSSParser.php
A CSSRenderer.php
A Gruntfile.js
A TemplateStyles.hooks.php
A composer.json
A extension.json
A i18n/en.json
A i18n/qqq.json
A package.json
A phpcs.xml
12 files changed, 580 insertions(+), 0 deletions(-)

Approvals:
  BryanDavis: Looks good to me, approved
  Paladox: Looks good to me, but someone else must approve
  coren: Looks good to me, but someone else must approve
  jenkins-bot: Verified



diff --git a/.jscsrc b/.jscsrc
new file mode 100644
index 0000000..9d22e3f
--- /dev/null
+++ b/.jscsrc
@@ -0,0 +1,3 @@
+{
+       "preset": "wikimedia"
+}
diff --git a/.jshintrc b/.jshintrc
new file mode 100644
index 0000000..66e3d48
--- /dev/null
+++ b/.jshintrc
@@ -0,0 +1,24 @@
+{
+       // Enforcing
+       "bitwise": true,
+       "eqeqeq": true,
+       "freeze": true,
+       "latedef": true,
+       "noarg": true,
+       "nonew": true,
+       "undef": true,
+       "unused": true,
+       "strict": false,
+
+       // Relaxing
+       "es5": false,
+
+       // Environment
+       "browser": true,
+       "jquery": true,
+
+       "globals": {
+               "mediaWiki": false,
+               "OO": false
+       }
+}
diff --git a/CSSParser.php b/CSSParser.php
new file mode 100644
index 0000000..a43d802
--- /dev/null
+++ b/CSSParser.php
@@ -0,0 +1,242 @@
+<?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\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 );
+
+               $space = false;
+               foreach ( $match[0] as $t ) {
+                       if ( preg_match( '/^(?:[ \n\t]|\/\*|<!--|-->)/', $t ) ) 
{
+                               if ( !$space ) {
+                                       $space = true;
+                                       $this->tokens[] = ' ';
+                                       continue;
+                               }
+                       } else {
+                               $space = false;
+                               $this->tokens[] = $t;
+                       }
+               }
+               $this->index = 0;
+       }
+
+       private function peek( $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 consumeTo( $delim ) {
+               $consume = 0;
+               while ( !in_array( $this->peek( $consume ), $delim ) )
+                       $consume++;
+               return $this->consume( $consume );
+       }
+
+       private function consumeWS() {
+               $consume = 0;
+               while ( $this->peek( $consume ) === ' ' )
+                       $consume++;
+               return $this->consume( $consume );
+       }
+
+       /**
+        * Parses:
+        *              decl            : WS* IDENT WS* ':' TOKEN* ';'
+        *                                      | WS* IDENT <error> ';'         
        -> skip
+        *                                      ;
+        *
+        * Returns:
+        *                      [ name => value ]
+        */
+       private function parseDecl() {
+               $this->consumeWS();
+               $name = $this->consume();
+               $this->consumeWS();
+               if ( $this->peek( 0 )!=':' ) {
+                       $this->consumeTo( [';', '}', null] );
+                       if ( $this->peek( 0 ) == ';' ) {
+                               $this->consume();
+                               $this->consumeWS();
+                       }
+                       return null;
+               }
+               $this->consume();
+               $this->consumeWS();
+               $value = $this->consumeTo( [';', '}', null] );
+               if ( $this->peek( 0 ) == ';' ) {
+                       $value .= $this->consume();
+                       $this->consumeWS();
+               }
+               return [ $name => $value ];
+       }
+
+       /**
+        * Parses:
+        *              decls           : '}'
+        *                                      | decl decls
+        *                                      ;
+        *
+        * Returns:
+        *                      [ decl* ]
+        */
+       private function parseDecls() {
+               $decls = [];
+               while ( $this->peek( 0 ) !== null and $this->peek( 0 ) != '}' ) 
{
+                       $decl = $this->parseDecl();
+                       if ( $decl )
+                               foreach ( $decl as $k => $d )
+                                       $decls[$k] = $d;
+               }
+               if ( $this->peek( 0 ) == '}' )
+                       $this->consume();
+               return $decls;
+       }
+
+       /**
+        * Parses:
+        *              rule            : WS* selectors ';'
+        *                                      | WS* selectors '{' decls
+        *                                      ;
+        *              selectors       : TOKEN*
+        *                                      | selectors ',' TOKEN*
+        *                                      ;
+        *
+        * Returns:
+        *                      [ selectors => [ selector* ], decls => [ decl* 
] ]
+        */
+       public function parseRule() {
+               $selectors = [];
+               $text = '';
+               $this->consumeWS();
+               while ( !in_array( $this->peek( 0 ), ['{', ';', null] ) ) {
+                       if ( $this->peek( 0 ) == ',' ) {
+                               $selectors[] = $text;
+                               $this->consume();
+                               $this->consumeWS();
+                               $text = '';
+                       } else
+                               $text .= $this->consume();
+               }
+               $selectors[] = $text;
+               if ( $this->peek( 0 ) == '{' ) {
+                       $this->consume();
+                       return [ "selectors"=>$selectors, 
"decls"=>$this->parseDecls() ];
+               }
+               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.
+        *
+        * Parses:
+        *              anyrule                 : ATIDENT='@media' WS* TOKEN* 
'{' rules '}'
+        *                                              | ATIDENT WS* TOKEN* ';'
+        *                                              | ATIDENT WS* TOKEN* 
'{' decls '}'
+        *                                              | rule
+        *                                              ;
+        *              rules                   : anyrule
+        *                                              | rules anyrule
+        *                                              ;
+        *
+        * Returns:
+        *                      [ [ name=>ATIDENT? , text=>body? , 
rules=>rules? ]* ]
+        */
+       public function rules( $end = [ null ] ) {
+               $atrules = [];
+               $rules = [];
+               $this->consumeWS();
+               while ( !in_array( $this->peek( 0 ), $end ) ) {
+                       if ( in_array( $this->peek( 0 ), [ '@media' ] ) ) {
+                               $at = $this->consume();
+                               $this->consumeWS();
+                               $text = '';
+                               while ( !in_array( $this->peek( 0 ), ['{', ';', 
null] ) )
+                                       $text .= $this->consume();
+                               if ( $this->peek( 0 ) == '{' ) {
+                                       $this->consume();
+                                       $r = $this->rules( [ '}', null ] );
+                                       if ( $r )
+                                               $atrules[] = [ "name"=>$at, 
"text"=>$text, "rules"=>$r ];
+                               } else {
+                                       $atrules[] = [ "name"=>$at, 
"text"=>$text ];
+                               }
+                       } elseif ( $this->peek( 0 )[0] == '@' ) {
+                               $at = $this->consume();
+                               $text = '';
+                               while ( !in_array( $this->peek( 0 ), ['{', ';', 
null] ) )
+                                       $text .= $this->consume();
+                               if ( $this->peek( 0 ) == '{' ) {
+                                       $this->consume();
+                                       $decl = $this->parseDecls();
+                                       if ( $decl )
+                                               $atrules[] = [ "name"=>$at, 
"text"=>$text, "rules"=>[ "selectors"=>'', "decls"=>$decl ] ];
+                               } else {
+                                       $atrules[] = [ "name"=>$at, 
"text"=>$text ];
+                               }
+                       } else
+                               $rules[] = $this->parseRule();
+                       $this->consumeWS();
+               }
+               if ( $rules )
+                       $atrules[] = [ "name"=>'', "rules"=>$rules ];
+               if ( $this->peek( 0 ) !== null )
+                       $this->consume();
+               return $atrules;
+       }
+
+}
+
diff --git a/CSSRenderer.php b/CSSRenderer.php
new file mode 100644
index 0000000..f0e95e4
--- /dev/null
+++ b/CSSRenderer.php
@@ -0,0 +1,75 @@
+<?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.
+        *
+        * @return string Rendered CSS
+        */
+       function render() {
+
+               $css = '';
+
+               foreach ( $this->bymedia as $at => $rules ) {
+                       if ( $at != '' )
+                               $css .= "$at {\n";
+                       foreach ( $rules as $rule ) {
+                               $css .= implode( ',', $rule['selectors'] ) . 
"{";
+                               foreach ( $rule['decls'] as $key => $value ) {
+                                       $css .= "$key:$value";
+                               }
+                               $css .= "} ";
+                       }
+                       if ( $at != '' )
+                               $css .= "} ";
+               }
+
+               return $css;
+       }
+
+}
+
+
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 0000000..a01772b
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,35 @@
+/*jshint node:true */
+module.exports = function ( grunt ) {
+       grunt.loadNpmTasks( 'grunt-contrib-jshint' );
+       grunt.loadNpmTasks( 'grunt-jsonlint' );
+       grunt.loadNpmTasks( 'grunt-banana-checker' );
+       grunt.loadNpmTasks( 'grunt-jscs' );
+
+       grunt.initConfig( {
+               jshint: {
+                       options: {
+                               jshintrc: true
+                       },
+                       all: [
+                               '**/*.js',
+                               '!node_modules/**'
+                       ]
+               },
+               jscs: {
+                       src: '<%= jshint.all %>'
+               },
+               banana: {
+                       all: 'i18n/'
+               },
+               jsonlint: {
+                       all: [
+                               '*.json',
+                               '**/*.json',
+                               '!node_modules/**'
+                       ]
+               }
+       } );
+
+       grunt.registerTask( 'test', [ 'jshint', 'jscs', 'jsonlint', 'banana' ] 
);
+       grunt.registerTask( 'default', 'test' );
+};
diff --git a/TemplateStyles.hooks.php b/TemplateStyles.hooks.php
new file mode 100644
index 0000000..7a4524d
--- /dev/null
+++ b/TemplateStyles.hooks.php
@@ -0,0 +1,98 @@
+<?php
+/**
+ * TemplateStyles extension hooks
+ *
+ * @file
+ * @ingroup Extensions
+ * @license LGPL-2.0+
+ */
+class TemplateStylesHooks {
+       /**
+        * Register parser hooks
+        */
+       public static function onParserFirstCallInit( &$parser ) {
+               $parser->setHook( 'templatestyles', array( 
'TemplateStylesHooks', 'render' ) );
+               return true;
+       }
+
+       public static function onOutputPageParserOutput( &$out, $parseroutput ) 
{
+               global $wgTemplateStylesNamespaces;
+               if ( $wgTemplateStylesNamespaces )
+                       $namespaces = $wgTemplateStylesNamespaces;
+               else
+                       $namespaces = [ NS_TEMPLATE ];
+
+               $renderer = new CSSRenderer();
+               $pages = [];
+
+               if ( $out->canUseWikiPage() )
+                       $pages[$out->getWikiPage()->getID()] = 'self';
+
+               foreach ( $namespaces as $ns )
+                       if ( array_key_exists( $ns, 
$parseroutput->getTemplates() ) )
+                               foreach ( $parseroutput->getTemplates()[$ns] as 
$title => $pageid )
+                                       $pages[$pageid] = $title;
+
+               if ( count( $pages ) ) {
+                       $db = wfGetDB( DB_SLAVE );
+                       $res = $db->select( 'page_props', [ 'pp_page', 
'pp_value' ], [
+                                       'pp_page' => array_keys( $pages ),
+                                       'pp_propname' => 'templatestyles'
+                               ],
+                               __METHOD__,
+                               [ 'ORDER BY', 'pp_page' ]
+                       );
+                       foreach ( $res as $row ) {
+                               $css = unserialize( gzdecode( $row->pp_value ) 
);
+                               $renderer->add( $css );
+                       }
+
+               }
+
+               $selfcss = $out->getProperty( 'templatestyles' );
+               if ( $selfcss ) {
+                       $selfcss = unserialize( gzdecode( $selfcss ) );
+                       $renderer->add( $selfcss );
+               }
+
+               $css = $renderer->render();
+               if ( $css )
+                       $out->addInlineStyle( $css );
+       }
+
+       /**
+        * Parser hook for <templatestyles>.
+        * If there is a CSS provided, render its source on the page and attach 
the
+        * parsed stylesheet to the page as a Property.
+        *
+        * @param string $input: The content of the tag.
+        * @param array $args: The attributes of the tag.
+        * @param Parser $parser: Parser instance available to render
+        *  wikitext into html, or parser methods.
+        * @param PPFrame $frame: Can be used to see what template parameters 
("{{{1}}}", etc.)
+        *  this hook was used with.
+        *
+        * @return string: HTML to insert in the page.
+        */
+       public static function render( $input, $args, $parser, $frame ) {
+               $css = new CSSParser( $input );
+
+               if ( $css )
+                       $parser->getOutput()->setProperty( 'templatestyles', 
gzencode( serialize( $css->rules() ) ) );
+
+               $html =
+                       Html::openElement( 'div', [ 'class' => 
'mw-templatestyles-doc' ] )
+                       . Html::rawElement(
+                               'p',
+                               [ 'class' => 'mw-templatestyles-caption' ],
+                               wfMessage( 'templatedata-doc-title' ) )
+                       . Html::element(
+                               'pre',
+                               [ 'class' => 'mw-templatestyles-stylesheet' ],
+                               $input )
+                       . Html::closeElement( 'div' );
+
+               return $html;
+       }
+
+}
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..d9fc29b
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,13 @@
+{
+    "license": "LGPL-2.1+",
+       "require-dev": {
+               "jakub-onderka/php-parallel-lint": "0.9",
+               "mediawiki/mediawiki-codesniffer": "0.4.0"
+       },
+       "scripts": {
+               "test": [
+                       "parallel-lint . --exclude vendor",
+                       "phpcs -p -s"
+               ]
+       }
+}
diff --git a/extension.json b/extension.json
new file mode 100644
index 0000000..2ac41b9
--- /dev/null
+++ b/extension.json
@@ -0,0 +1,42 @@
+{
+       "name": "TemplateStyles",
+       "version": "0.9",
+       "author": [
+               "Marc A. Pelletier"
+       ],
+       "url": "https://www.mediawiki.org/wiki/Extension:TemplateStyles";,
+       "namemsg": "templateStyles",
+       "descriptionmsg": "templateStyles-desc",
+       "license-name": "LGPL-2.0+",
+       "type": "other",
+       "manifest_version": 1,
+       "MessageDirs": {
+               "TemplateStyles": [
+                       "i18n"
+               ]
+       },
+       "AutoloadClasses": {
+               "TemplateStylesHooks": "TemplateStyles.hooks.php",
+               "CSSParser": "CSSParser.php",
+               "CSSRenderer": "CSSRenderer.php"
+       },
+       "ext.templateStyles": {
+               "scripts": [
+               ],
+               "styles": [
+               ],
+               "messages": [
+               ],
+               "dependencies": [
+               ]
+       },
+       "Hooks": {
+               "ParserFirstCallInit": [
+                       "TemplateStylesHooks::onParserFirstCallInit"
+               ],
+               "OutputPageParserOutput": [
+                       "TemplateStylesHooks::onOutputPageParserOutput"
+               ]
+       }
+}
+
diff --git a/i18n/en.json b/i18n/en.json
new file mode 100644
index 0000000..640383e
--- /dev/null
+++ b/i18n/en.json
@@ -0,0 +1,10 @@
+{
+    "@metadata": {
+        "authors": [
+            "Marc A. Pelletier"
+        ]
+    },
+    "templateStyles": "TemplateStyles",
+    "templateStyles-desc": "Implement per-template style sheets",
+       "templatestyles-doc-header": "Template-specific style sheet:"
+}
diff --git a/i18n/qqq.json b/i18n/qqq.json
new file mode 100644
index 0000000..707c1dd
--- /dev/null
+++ b/i18n/qqq.json
@@ -0,0 +1,10 @@
+{
+    "@metadata": {
+        "authors": [
+            "Marc A. Pelletier"
+        ]
+    },
+    "templateStyles": "The name of the extension",
+    "templateStyles-desc": 
"{{desc|name=TemplateStyles|url=https://www.mediawiki.org/wiki/Extension:TemplateStyles}}";,
+       "templatestyles-doc-header": "Used as caption for the display of the 
style sheet of the current template."
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..c92cb3c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,20 @@
+{
+  "private": true,
+  "name": "TemplateStyles",
+  "version": "0.9.0",
+  "repository": {
+    "type": "git",
+       "url": 
"https://gerrit.wikimedia.org/r/mediawiki/extensions/TemplateStyles";
+  },
+  "scripts": {
+    "test": "grunt test"
+  },
+  "devDependencies": {
+    "grunt": "0.4.5",
+    "grunt-cli": "0.1.13",
+    "grunt-contrib-jshint": "1.0.0",
+    "grunt-banana-checker": "0.5.0",
+    "grunt-jscs": "2.8.0",
+    "grunt-jsonlint": "1.0.7"
+  }
+}
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..d81a292
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<ruleset>
+       <rule ref="vendor/mediawiki/mediawiki-codesniffer/MediaWiki"/>
+       <file>.</file>
+       <arg name="extensions" value="php,php5,inc"/>
+       <arg name="encoding" value="utf8"/>
+       <exclude-pattern>vendor</exclude-pattern>
+</ruleset>

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

Gerrit-MessageType: merged
Gerrit-Change-Id: Ibc1cae3079d164f7ac7bcc7c4ded3f02bb048614
Gerrit-PatchSet: 14
Gerrit-Project: mediawiki/extensions/TemplateStyles
Gerrit-Branch: master
Gerrit-Owner: coren <m...@uberbox.org>
Gerrit-Reviewer: Brion VIBBER <br...@wikimedia.org>
Gerrit-Reviewer: BryanDavis <bda...@wikimedia.org>
Gerrit-Reviewer: Jdlrobson <jrob...@wikimedia.org>
Gerrit-Reviewer: Jforrester <jforres...@wikimedia.org>
Gerrit-Reviewer: Paladox <thomasmulhall...@yahoo.com>
Gerrit-Reviewer: Tim Starling <tstarl...@wikimedia.org>
Gerrit-Reviewer: coren <m...@uberbox.org>
Gerrit-Reviewer: jenkins-bot <>

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

Reply via email to