case 'messages':
$out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) );
break;
+ case 'templates':
+ $out .= Xml::encodeJsCall(
+ 'mw.templates.set',
+ array( $name, (object)$module->getTemplates() ),
+ ResourceLoader::inDebugMode()
+ );
+ break;
default:
$out .= self::makeLoaderImplementScript(
$name,
$scripts,
$styles,
- new XmlJsCode( $messagesBlob )
+ new XmlJsCode( $messagesBlob ),
+ $module->getTemplates()
);
break;
}
* @param mixed $messages List of messages associated with this module. May either be an
* associative array mapping message key to value, or a JSON-encoded message blob containing
* the same data, wrapped in an XmlJsCode object.
+ * @param array $templates Keys are name of templates and values are the source of
+ * the template.
* @throws MWException
* @return string
*/
- public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
+ public static function makeLoaderImplementScript( $name, $scripts, $styles,
+ $messages, $templates
+ ) {
if ( is_string( $scripts ) ) {
$scripts = new XmlJsCode( "function ( $, jQuery ) {\n{$scripts}\n}" );
} elseif ( !is_array( $scripts ) ) {
throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
}
+
return Xml::encodeJsCall(
'mw.loader.implement',
array(
// PHP/json_encode() consider empty arrays to be numerical arrays and
// output javascript "[]" instead of "{}". This fixes that.
(object)$styles,
- (object)$messages
+ (object)$messages,
+ (object)$templates,
),
ResourceLoader::inDebugMode()
);
/** @var string Remote base path, see __construct() */
protected $remoteBasePath = '';
+ /** @var array Saves a list of the templates named by the modules. */
+ protected $templates = array();
+
/**
* @var array List of paths to JavaScript files to always include
* @par Usage:
* 'loaderScripts' => [file path string or array of file path strings],
* // Modules which must be loaded before this module
* 'dependencies' => [module name string or array of module name strings],
+ * 'templates' => array(
+ * [template alias with file.ext] => [file path to a template file],
+ * ),
* // Styles to always load
* 'styles' => [file path string or array of file path strings],
* // Styles to include in specific skin contexts
$localBasePath = null,
$remoteBasePath = null
) {
+ // Flag to decide whether to automagically add the mediawiki.template module
+ $hasTemplates = false;
// localBasePath and remoteBasePath both have unbelievably long fallback chains
// and need to be handled separately.
list( $this->localBasePath, $this->remoteBasePath ) =
case 'styles':
$this->{$member} = (array)$option;
break;
+ case 'templates':
+ $hasTemplates = true;
+ $this->{$member} = (array)$option;
+ break;
// Collated lists of file paths
case 'languageScripts':
case 'skinScripts':
break;
}
}
+ if ( $hasTemplates ) {
+ $this->dependencies[] = 'mediawiki.template';
+ }
}
/**
$files = array_merge(
$files,
$this->scripts,
+ $this->templates,
$context->getDebug() ? $this->debugScripts : array(),
$this->getLanguageScripts( $context->getLanguage() ),
self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ),
'dependencies',
'messages',
'targets',
+ 'templates',
'group',
'position',
'skipFunction',
protected function getLessCompiler( ResourceLoaderContext $context = null ) {
return ResourceLoader::getLessCompiler( $this->getConfig() );
}
+
+ /**
+ * Takes named templates by the module and returns an array mapping.
+ *
+ * @return array of templates mapping template alias to content
+ */
+ public function getTemplates() {
+ $templates = array();
+
+ foreach ( $this->templates as $alias => $templatePath ) {
+ // Alias is optional
+ if ( is_int( $alias ) ) {
+ $alias = $templatePath;
+ }
+ $localPath = $this->getLocalPath( $templatePath );
+ if ( file_exists( $localPath ) ) {
+ $content = file_get_contents( $localPath );
+ $templates[ $alias ] = $content;
+ } else {
+ $msg = __METHOD__ . ": template file not found: \"$localPath\"";
+ wfDebugLog( 'resourceloader', $msg );
+ throw new MWException( $msg );
+ }
+ }
+ return $templates;
+ }
}
return '';
}
+ /**
+ * Takes named templates by the module and returns an array mapping.
+ *
+ * @return array of templates mapping template alias to content
+ */
+ public function getTemplates() {
+ // Stub, override expected.
+ return array();
+ }
+
/**
* @return Config
* @since 1.24
"mw.html",
"mw.html.Cdata",
"mw.html.Raw",
- "mw.hook"
+ "mw.hook",
+ "mw.template"
]
},
{
'mediawiki.hlist',
),
),
+ 'mediawiki.template' => array(
+ 'scripts' => 'resources/src/mediawiki/mediawiki.template.js',
+ 'targets' => array( 'desktop', 'mobile' ),
+ ),
'mediawiki.apipretty' => array(
'styles' => 'resources/src/mediawiki/mediawiki.apipretty.css',
'targets' => array( 'desktop', 'mobile' ),
'position' => 'bottom',
),
'mediawiki.feedback' => array(
+ 'templates' => array(
+ 'dialog.html' => 'resources/src/mediawiki/templates/dialog.html',
+ ),
'scripts' => 'resources/src/mediawiki/mediawiki.feedback.js',
'styles' => 'resources/src/mediawiki/mediawiki.feedback.css',
'dependencies' => array(
),
),
'mediawiki.action.view.postEdit' => array(
+ 'templates' => array(
+ 'postEdit.html' => 'resources/src/mediawiki.action/templates/postEdit.html',
+ ),
'scripts' => 'resources/src/mediawiki.action/mediawiki.action.view.postEdit.js',
'styles' => 'resources/src/mediawiki.action/mediawiki.action.view.postEdit.css',
'dependencies' => array(
'scripts' => 'resources/src/mediawiki.special/mediawiki.special.undelete.js',
),
'mediawiki.special.upload' => array(
+ 'templates' => array(
+ 'thumbnail.html' => 'resources/src/mediawiki.special/templates/thumbnail.html',
+ ),
'scripts' => 'resources/src/mediawiki.special/mediawiki.special.upload.js',
'messages' => array(
'widthheight',
data.message = $.parseHTML( mw.message( 'postedit-confirmation-saved', data.user || mw.user ).escaped() );
}
- $div = $(
- '<div class="postedit-container">' +
- '<div class="postedit">' +
- '<div class="postedit-icon postedit-icon-checkmark postedit-content"></div>' +
- '<a href="#" class="postedit-close">×</a>' +
- '</div>' +
- '</div>'
- );
+ $div = mw.template.get( 'mediawiki.action.view.postEdit', 'postEdit.html' ).render();
if ( typeof data.message === 'string' ) {
$div.find( '.postedit-content' ).text( data.message );
--- /dev/null
+<div class="postedit-container">
+ <div class="postedit">
+ <div class="postedit-icon postedit-icon-checkmark postedit-content"></div>
+ <a href="#" class="postedit-close">×</a>
+ </div>
+</div>
ctx,
meta,
previewSize = 180,
- thumb = $( '<div id="mw-upload-thumbnail" class="thumb tright">' +
- '<div class="thumbinner">' +
- '<div class="mw-small-spinner" style="width: 180px; height: 180px"></div>' +
- '<div class="thumbcaption"><div class="filename"></div><div class="fileinfo"></div></div>' +
- '</div>' +
- '</div>' );
+ thumb = mw.template.get( 'mediawiki.special.upload', 'thumbnail.html' ).render();
thumb.find( '.filename' ).text( file.name ).end()
.find( '.fileinfo' ).text( prettySize( file.size ) ).end();
--- /dev/null
+<div id="mw-upload-thumbnail" class="thumb tright">
+ <div class="thumbinner">
+ <div class="mw-small-spinner" style="width: 180px; height: 180px"></div>
+ <div class="thumbcaption">
+ <div class="filename"></div>
+ <div class="fileinfo"></div>
+ </div>
+ </div>
+</div>
target: '_blank'
} );
- // TODO: Use a stylesheet instead of these inline styles
- this.$dialog =
- $( '<div style="position: relative;"></div>' ).append(
- $( '<div class="feedback-mode feedback-form"></div>' ).append(
- $( '<small>' ).append(
- $( '<p>' ).msg(
- 'feedback-bugornote',
- $bugNoteLink,
- fb.title.getNameText(),
- $feedbackPageLink.clone()
- )
- ),
- $( '<div style="margin-top: 1em;"></div>' )
- .msg( 'feedback-subject' )
- .append(
- $( '<br>' ),
- $( '<input type="text" class="feedback-subject" name="subject" maxlength="60" style="width: 100%; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box;"/>' )
- ),
- $( '<div style="margin-top: 0.4em;"></div>' )
- .msg( 'feedback-message' )
- .append(
- $( '<br>' ),
- $( '<textarea name="message" class="feedback-message" rows="5" cols="60"></textarea>' )
- )
- ),
- $( '<div class="feedback-mode feedback-bugs"></div>' ).append(
- $( '<p>' ).msg( 'feedback-bugcheck', $bugsListLink )
- ),
- $( '<div class="feedback-mode feedback-submitting" style="text-align: center; margin: 3em 0;"></div>' )
- .msg( 'feedback-adding' )
- .append(
- $( '<br>' ),
- $( '<span class="feedback-spinner"></span>' )
- ),
- $( '<div class="feedback-mode feedback-thanks" style="text-align: center; margin:1em"></div>' ).msg(
- 'feedback-thanks', fb.title.getNameText(), $feedbackPageLink.clone()
- ),
- $( '<div class="feedback-mode feedback-error" style="position: relative;"></div>' ).append(
- $( '<div class="feedback-error-msg style="color: #990000; margin-top: 0.4em;"></div>' )
- )
- );
+ // TODO: Use a stylesheet instead of these inline styles in the template
+ this.$dialog = mw.template.get( 'mediawiki.feedback', 'dialog.html' ).render();
+ this.$dialog.find( '.feedback-mode small p' ).msg(
+ 'feedback-bugornote',
+ $bugNoteLink,
+ fb.title.getNameText(),
+ $feedbackPageLink.clone()
+ );
+ this.$dialog.find( '.feedback-form .subject span' ).msg( 'feedback-subject' );
+ this.$dialog.find( '.feedback-form .message span' ).msg( 'feedback-message' );
+ this.$dialog.find( '.feedback-bugs p' ).msg( 'feedback-bugcheck', $bugsListLink );
+ this.$dialog.find( '.feedback-submitting span' ).msg( 'feedback-adding' );
+ this.$dialog.find( '.feedback-thanks' ).msg( 'feedback-thanks', fb.title.getNameText(),
+ $feedbackPageLink.clone() );
this.$dialog.dialog( {
width: 500,
*/
messages: new Map(),
+ /**
+ * Templates associated with a module
+ * @property {mw.Map}
+ */
+ templates: new Map(),
+
/* Public Methods */
/**
mw.messages.set( registry[module].messages );
}
+ // Initialise templates
+ if ( registry[module].templates ) {
+ mw.templates.set( module, registry[module].templates );
+ }
+
if ( $.isReady || registry[module].async ) {
// Make sure we don't run the scripts until all (potentially asynchronous)
// stylesheet insertions have completed.
* whether it's safe to extend the stylesheet (see #canExpandStylesheetWith).
*
* @param {Object} msgs List of key/value pairs to be added to mw#messages.
+ * @param {Object} [templates] List of key/value pairs to be added to mw#templates.
*/
- implement: function ( module, script, style, msgs ) {
+ implement: function ( module, script, style, msgs, templates ) {
// Validate input
if ( typeof module !== 'string' ) {
throw new Error( 'module must be a string, not a ' + typeof module );
if ( !$.isPlainObject( msgs ) ) {
throw new Error( 'msgs must be an object, not a ' + typeof msgs );
}
+ if ( templates !== undefined && !$.isPlainObject( templates ) ) {
+ throw new Error( 'templates must be an object, not a ' + typeof templates );
+ }
// Automatically register module
if ( registry[module] === undefined ) {
mw.loader.register( module );
registry[module].script = script;
registry[module].style = style;
registry[module].messages = msgs;
+ // Templates are optional (for back-compat)
+ registry[module].templates = templates || {};
// The module may already have been marked as erroneous
if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
registry[module].state = 'loaded';
// Unversioned, private, or site-/user-specific
( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) ||
// Partial descriptor
- $.inArray( undefined, [ descriptor.script, descriptor.style, descriptor.messages ] ) !== -1
+ $.inArray( undefined, [ descriptor.script, descriptor.style,
+ descriptor.messages, descriptor.templates ] ) !== -1
) {
// Decline to store
return false;
String( descriptor.script ) :
JSON.stringify( descriptor.script ),
JSON.stringify( descriptor.style ),
- JSON.stringify( descriptor.messages )
+ JSON.stringify( descriptor.messages ),
+ JSON.stringify( descriptor.templates )
];
// Attempted workaround for a possible Opera bug (bug 57567).
// This regex should never match under sane conditions.
--- /dev/null
+/**
+ * @class mw.template
+ * @singleton
+ */
+( function ( mw, $ ) {
+ var compiledTemplates = {},
+ compilers = {};
+
+ mw.template = {
+ /**
+ * Register a new compiler and template.
+ *
+ * @param {string} name of compiler. Should also match with any file extensions of templates that want to use it.
+ * @param {Function} compiler which must implement a compile function
+ */
+ registerCompiler: function ( name, compiler ) {
+ if ( !compiler.compile ) {
+ throw new Error( 'Compiler must implement compile method.' );
+ }
+ compilers[name] = compiler;
+ },
+
+ /**
+ * Get the name of the compiler associated with a template based on its name.
+ *
+ * @param {string} templateName Name of template (including file suffix)
+ * @return {String} Name of compiler
+ */
+ getCompilerName: function ( templateName ) {
+ var templateParts = templateName.split( '.' );
+
+ if ( templateParts.length < 2 ) {
+ throw new Error( 'Unable to identify compiler. Template name must have a suffix.' );
+ }
+ return templateParts[ templateParts.length - 1 ];
+ },
+
+ /**
+ * Get the compiler for a given compiler name.
+ *
+ * @param {string} compilerName Name of the compiler
+ * @return {Object} The compiler associated with that name
+ */
+ getCompiler: function ( compilerName ) {
+ var compiler = compilers[ compilerName ];
+ if ( !compiler ) {
+ throw new Error( 'Unknown compiler ' + compilerName );
+ }
+ return compiler;
+ },
+
+ /**
+ * Register a template associated with a module.
+ *
+ * Compiles the newly added template based on the suffix in its name.
+ *
+ * @param {string} moduleName Name of ResourceLoader module to get the template from
+ * @param {string} templateName Name of template to add including file extension
+ * @param {string} templateBody Contents of a template (e.g. html markup)
+ * @return {Function} Compiled template
+ */
+ add: function ( moduleName, templateName, templateBody ) {
+ var compiledTemplate,
+ compilerName = this.getCompilerName( templateName );
+
+ if (!compiledTemplates[moduleName]) {
+ compiledTemplates[moduleName] = {};
+ }
+
+ compiledTemplate = this.compile( templateBody, compilerName );
+ compiledTemplates[moduleName][ templateName ] = compiledTemplate;
+ return compiledTemplate;
+ },
+
+ /**
+ * Retrieve a template by module and template name.
+ *
+ * @param {string} moduleName Name of the module to retrieve the template from
+ * @param {string} templateName Name of template to be retrieved
+ * @return {Object} Compiled template
+ */
+ get: function ( moduleName, templateName ) {
+ var moduleTemplates, compiledTemplate;
+
+ // Check if the template has already been compiled, compile it if not
+ if ( !compiledTemplates[ moduleName ] || !compiledTemplates[ moduleName ][ templateName ] ) {
+ moduleTemplates = mw.templates.get( moduleName );
+ if ( !moduleTemplates || !moduleTemplates[ templateName ] ) {
+ throw new Error( 'Template ' + templateName + ' not found in module ' + moduleName );
+ }
+
+ // Add compiled version
+ compiledTemplate = this.add( moduleName, templateName, moduleTemplates[ templateName ] );
+ } else {
+ compiledTemplate = compiledTemplates[ moduleName ][ templateName ];
+ }
+ return compiledTemplate;
+ },
+
+ /**
+ * Wrap our template engine of choice.
+ *
+ * @param {string} templateBody Template body
+ * @param {string} compilerName The name of a registered compiler
+ * @return {Object} Template interface
+ */
+ compile: function ( templateBody, compilerName ) {
+ return this.getCompiler( compilerName ).compile( templateBody );
+ }
+ };
+
+ // Register basic html compiler
+ mw.template.registerCompiler( 'html', {
+ compile: function ( src ) {
+ return {
+ render: function () {
+ return $( $.parseHTML( $.trim( src ) ) );
+ }
+ };
+ }
+ } );
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+<div style="position: relative; display: block;" class="ui-dialog-content ui-widget-content">
+ <div class="feedback-mode feedback-form">
+ <small><p></p></small>
+ <div class="subject" style="margin-top: 1em;">
+ <span></span><br>
+ <input type="text" class="feedback-subject" name="subject" maxlength="60"
+ style="width: 100%; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box;">
+ </div>
+ <div class="message" style="margin-top: 0.4em;">
+ <span></span><br>
+ <textarea name="message" class="feedback-message" rows="5" cols="60"></textarea>
+ </div>
+ </div>
+ <div class="feedback-mode feedback-bugs">
+ <p></p>
+ </div>
+ <div class="feedback-mode feedback-submitting" style="text-align: center; margin: 3em 0;">
+ <span></span><br>
+ <span class="feedback-spinner"></span>
+ </div>
+ <div class="feedback-mode feedback-thanks" style="text-align: center; margin:1em"></div>
+ <div class="feedback-mode feedback-error" style="position: relative;">
+ <div class="feedback-error-msg" style=" color:#990000; margin-top:0.4em;"></div>
+ </div>
+</div>
array(
array( 'test.quux', ResourceLoaderModule::TYPE_COMBINED ),
'<script>if(window.mw){
-mw.loader.implement("test.quux",function($,jQuery){mw.test.baz({token:123});},{"css":[".mw-icon{transition:none}\n"]},{});
+mw.loader.implement("test.quux",function($,jQuery){mw.test.baz({token:123});},{"css":[".mw-icon{transition:none}\n"]},{},{});
}</script>
'
<?php
+/**
+ * @group ResourceLoader
+ */
class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
protected function setUp() {
);
}
+ public static function getModules() {
+ $base = array(
+ 'localBasePath' => realpath( dirname( __FILE__ ) ),
+ );
+
+ return array(
+ 'noTemplateModule' => array(),
+
+ 'htmlTemplateModule' => $base + array(
+ 'templates' => array(
+ 'templates/template.html',
+ 'templates/template2.html',
+ )
+ ),
+
+ 'aliasedHtmlTemplateModule' => $base + array(
+ 'templates' => array(
+ 'foo.html' => 'templates/template.html',
+ 'bar.html' => 'templates/template2.html',
+ )
+ ),
+
+ 'templateModuleHandlebars' => $base + array(
+ 'templates' => array(
+ 'templates/template_awesome.handlebars',
+ ),
+ ),
+ );
+ }
+
+ public static function providerGetTemplates() {
+ $modules = self::getModules();
+
+ return array(
+ array(
+ $modules['noTemplateModule'],
+ array(),
+ ),
+ array(
+ $modules['templateModuleHandlebars'],
+ array(
+ 'templates/template_awesome.handlebars' => "wow\n",
+ ),
+ ),
+ array(
+ $modules['htmlTemplateModule'],
+ array(
+ 'templates/template.html' => "<strong>hello</strong>\n",
+ 'templates/template2.html' => "<div>goodbye</div>\n",
+ ),
+ ),
+ array(
+ $modules['aliasedHtmlTemplateModule'],
+ array(
+ 'foo.html' => "<strong>hello</strong>\n",
+ 'bar.html' => "<div>goodbye</div>\n",
+ ),
+ ),
+ );
+ }
+
+ public static function providerGetModifiedTime() {
+ $modules = self::getModules();
+
+ return array(
+ // Check the default value when no templates present in module is 1
+ array( $modules['noTemplateModule'], 1 ),
+ );
+ }
+
/**
* @covers ResourceLoaderFileModule::getAllSkinStyleFiles
*/
array_map( 'basename', $module->getAllStyleFiles() )
);
}
+
+ /**
+ * @dataProvider providerGetTemplates
+ * @covers ResourceLoaderFileModule::getTemplates
+ */
+ public function testGetTemplates( $module, $expected ) {
+ $rl = new ResourceLoaderFileModule( $module );
+
+ $this->assertEquals( $rl->getTemplates(), $expected );
+ }
+
+ /**
+ * @dataProvider providerGetModifiedTime
+ * @covers ResourceLoaderFileModule::getModifiedTime
+ */
+ public function testGetModifiedTime( $module, $expected ) {
+ $rl = new ResourceLoaderFileModule( $module );
+ $ts = $rl->getModifiedTime( new ResourceLoaderContext(
+ new ResourceLoader, new FauxRequest() ) );
+ $this->assertEquals( $ts, $expected );
+ }
}
--- /dev/null
+<strong>hello</strong>
--- /dev/null
+<div>goodbye</div>
'tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js',
+ 'tests/qunit/suites/resources/mediawiki/mediawiki.template.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js',
'mediawiki.toc',
'mediawiki.Uri',
'mediawiki.user',
+ 'mediawiki.template',
'mediawiki.util',
'mediawiki.special.recentchanges',
'mediawiki.language',
--- /dev/null
+( function ( mw ) {
+
+ QUnit.module( 'mediawiki.template', {
+ setup: function () {
+ var abcCompiler = {
+ compile: function () {
+ return 'abc default compiler';
+ }
+ };
+
+ // Register some template compiler languages
+ mw.template.registerCompiler( 'abc', abcCompiler );
+ mw.template.registerCompiler( 'xyz', {
+ compile: function () {
+ return 'xyz compiler';
+ }
+ } );
+
+ // Stub register some templates
+ this.sandbox.stub( mw.templates, 'get' ).returns( {
+ 'test_templates_foo.xyz': 'goodbye',
+ 'test_templates_foo.abc': 'thankyou'
+ } );
+ }
+ } );
+
+ QUnit.test( 'add', 1, function ( assert ) {
+ assert.throws(
+ function () {
+ mw.template.add( 'module', 'test_templates_foo', 'hello' );
+ },
+ 'When no prefix throw exception'
+ );
+ } );
+
+ QUnit.test( 'compile', 1, function ( assert ) {
+ assert.throws(
+ function () {
+ mw.template.compile( '{{foo}}', 'rainbow' );
+ },
+ 'Unknown compiler names throw exceptions'
+ );
+ } );
+
+ QUnit.test( 'get', 4, function ( assert ) {
+ assert.strictEqual( mw.template.get( 'test.mediawiki.template', 'test_templates_foo.xyz' ), 'xyz compiler' );
+ assert.strictEqual( mw.template.get( 'test.mediawiki.template', 'test_templates_foo.abc' ), 'abc default compiler' );
+ assert.throws(
+ function () {
+ mw.template.get( 'this.should.not.exist', 'hello' );
+ },
+ 'When bad module name given throw error.'
+ );
+
+ assert.throws(
+ function () {
+ mw.template.get( 'mediawiki.template', 'hello' );
+ },
+ 'The template hello should not exist in the mediawiki.templates module and should throw an exception.'
+ );
+ } );
+
+}( mediaWiki ) );