* Wrapper around jQuery append that converts all non-objects to TextNode so append will not
* convert what it detects as an htmlString to an element.
*
+ * If our own htmlEmitter jQuery object is given, its children will be unwrapped and appended to
+ * new parent.
+ *
* Object elements of children (jQuery, HTMLElement, TextNode, etc.) will be left as is.
*
* @private
if ( typeof children[ i ] !== 'object' ) {
children[ i ] = document.createTextNode( children[ i ] );
}
+ if ( children[ i ] instanceof jQuery && children[ i ].hasClass( 'mediaWiki_htmlEmitter' ) ) {
+ children[ i ] = children[ i ].contents();
+ }
}
return $parent.append( children );
.replace( /&/g, '&' );
}
+ /**
+ * Turn input into a string.
+ *
+ * @private
+ * @param {string|jQuery} input
+ * @return {string} Textual value of input
+ */
+ function textify( input ) {
+ if ( input instanceof jQuery ) {
+ input = input.text();
+ }
+ return String( input );
+ }
+
/**
* Given parser options, return a function that parses a key and replacements, returning jQuery object
*
return function () {
var $target = this.empty();
- // TODO: Simply appendWithoutParsing( $target, failableParserFn( arguments ).contents() )
- // or Simply appendWithoutParsing( $target, failableParserFn( arguments ) )
- $.each( failableParserFn( arguments ).contents(), function ( i, node ) {
- appendWithoutParsing( $target, node );
- } );
+ appendWithoutParsing( $target, failableParserFn( arguments ) );
return $target;
};
};
escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral,
whitespace, dollar, digits, htmlDoubleQuoteAttributeValue, htmlSingleQuoteAttributeValue,
htmlAttributeEquals, openHtmlStartTag, optionalForwardSlash, openHtmlEndTag, closeHtmlTag,
- openExtlink, closeExtlink, wikilinkPage, wikilinkContents, openWikilink, closeWikilink, templateName, pipe, colon,
+ openExtlink, closeExtlink, wikilinkContents, openWikilink, closeWikilink, templateName, pipe, colon,
templateContents, openTemplate, closeTemplate,
nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result,
settings = this.settings,
return result === null ? null : result.join( '' );
}
- // Used for wikilink page names. Like literalWithoutBar, but
- // without allowing escapes.
- function unescapedLiteralWithoutBar() {
- var result = nOrMore( 1, regularLiteralWithoutBar )();
- return result === null ? null : result.join( '' );
- }
-
function literal() {
var result = nOrMore( 1, escapedOrRegularLiteral )();
return result === null ? null : result.join( '' );
closeExtlink = makeStringParser( ']' );
// this extlink MUST have inner contents, e.g. [foo] not allowed; [foo bar] [foo <i>bar</i>], etc. are allowed
function extlink() {
- var result, parsedResult;
+ var result, parsedResult, target;
result = null;
parsedResult = sequence( [
openExtlink,
- nonWhitespaceExpression,
+ nOrMore( 1, nonWhitespaceExpression ),
whitespace,
nOrMore( 1, expression ),
closeExtlink
] );
if ( parsedResult !== null ) {
- result = [ 'EXTLINK', parsedResult[ 1 ] ];
- // TODO (mattflaschen, 2013-03-22): Clean this up if possible.
- // It's avoiding CONCAT for single nodes, so they at least doesn't get the htmlEmitter span.
- if ( parsedResult[ 3 ].length === 1 ) {
- result.push( parsedResult[ 3 ][ 0 ] );
- } else {
- result.push( [ 'CONCAT' ].concat( parsedResult[ 3 ] ) );
- }
+ // When the entire link target is a single parameter, we can't use CONCAT, as we allow
+ // passing fancy parameters (like a whole jQuery object or a function) to use for the
+ // link. Check only if it's a single match, since we can either do CONCAT or not for
+ // singles with the same effect.
+ target = parsedResult[ 1 ].length === 1 ?
+ parsedResult[ 1 ][ 0 ] :
+ [ 'CONCAT' ].concat( parsedResult[ 1 ] );
+ result = [
+ 'EXTLINK',
+ target,
+ [ 'CONCAT' ].concat( parsedResult[ 3 ] )
+ ];
}
return result;
}
- // this is the same as the above extlink, except that the url is being passed on as a parameter
- function extLinkParam() {
- var result = sequence( [
- openExtlink,
- dollar,
- digits,
- whitespace,
- expression,
- closeExtlink
- ] );
- if ( result === null ) {
- return null;
- }
- return [ 'EXTLINKPARAM', parseInt( result[ 2 ], 10 ) - 1, result[ 4 ] ];
- }
openWikilink = makeStringParser( '[[' );
closeWikilink = makeStringParser( ']]' );
pipe = makeStringParser( '|' );
return result === null ? null : result[ 1 ];
}
- wikilinkPage = choice( [
- unescapedLiteralWithoutBar,
- template
- ] );
-
function pipedWikilink() {
var result = sequence( [
- wikilinkPage,
+ nOrMore( 1, paramExpression ),
pipe,
nOrMore( 1, expression )
] );
- return result === null ? null : [ result[ 0 ], [ 'CONCAT' ].concat( result[ 2 ] ) ];
+ return result === null ? null : [
+ [ 'CONCAT' ].concat( result[ 0 ] ),
+ [ 'CONCAT' ].concat( result[ 2 ] )
+ ];
+ }
+
+ function unpipedWikilink() {
+ var result = sequence( [
+ nOrMore( 1, paramExpression )
+ ] );
+ return result === null ? null : [
+ [ 'CONCAT' ].concat( result[ 0 ] )
+ ];
}
wikilinkContents = choice( [
pipedWikilink,
- wikilinkPage // unpiped link
+ unpipedWikilink
] );
function wikilink() {
nonWhitespaceExpression = choice( [
template,
wikilink,
- extLinkParam,
extlink,
replacement,
literalWithoutSpace
paramExpression = choice( [
template,
wikilink,
- extLinkParam,
extlink,
replacement,
literalWithoutBar
expression = choice( [
template,
wikilink,
- extLinkParam,
extlink,
replacement,
html,
concat: function ( nodes ) {
var $span = $( '<span>' ).addClass( 'mediaWiki_htmlEmitter' );
$.each( nodes, function ( i, node ) {
- if ( node instanceof jQuery && node.hasClass( 'mediaWiki_htmlEmitter' ) ) {
- $.each( node.contents(), function ( j, childNode ) {
- appendWithoutParsing( $span, childNode );
- } );
- } else {
- // Let jQuery append nodes, arrays of nodes and jQuery objects
- // other things (strings, numbers, ..) are appended as text nodes (not as HTML strings)
- appendWithoutParsing( $span, node );
- }
+ // Let jQuery append nodes, arrays of nodes and jQuery objects
+ // other things (strings, numbers, ..) are appended as text nodes (not as HTML strings)
+ appendWithoutParsing( $span, node );
} );
return $span;
},
* @param {string[]} nodes
*/
wikilink: function ( nodes ) {
- var page, anchor, url;
+ var page, anchor, url, $el;
- page = nodes[ 0 ];
+ page = textify( nodes[ 0 ] );
url = mw.util.getUrl( page );
if ( nodes.length === 1 ) {
anchor = nodes[ 1 ];
}
- return $( '<a>' ).attr( {
+ $el = $( '<a>' ).attr( {
title: page,
href: url
- } )
- // FIXME This means that you can't have anything with formatting inside a wikilink.
- .text( anchor.jquery ? anchor.text() : anchor );
+ } );
+ return appendWithoutParsing( $el, anchor );
},
/**
},
/**
- * Transform parsed structure into external link
- * If the href is a jQuery object, treat it as "enclosing" the link text.
+ * Transform parsed structure into external link.
*
- * - ... function, treat it as the click handler.
- * - ... string, treat it as a URI.
+ * The "href" can be:
+ * - a jQuery object, treat it as "enclosing" the link text.
+ * - a function, treat it as the click handler.
+ * - a string, or our htmlEmitter jQuery object, treat it as a URI after stringifying.
*
* TODO: throw an error if nodes.length > 2 ?
*
var $el,
arg = nodes[ 0 ],
contents = nodes[ 1 ];
- if ( arg instanceof jQuery ) {
+ if ( arg instanceof jQuery && !arg.hasClass( 'mediaWiki_htmlEmitter' ) ) {
$el = arg;
} else {
$el = $( '<a>' );
} )
.click( arg );
} else {
- $el.attr( 'href', arg.toString() );
+ $el.attr( 'href', textify( arg ) );
}
}
return appendWithoutParsing( $el, contents );
},
- /**
- * This is basically use a combination of replace + external link (link with parameter
- * as url), but we don't want to run the regular replace here-on: inserting a
- * url as href-attribute of a link will automatically escape it already, so
- * we don't want replace to (manually) escape it as well.
- *
- * TODO: throw error if nodes.length > 1 ?
- *
- * @param {Array} nodes List of one element, integer, n >= 0
- * @param {Array} replacements List of at least n strings
- * @return {string} replacement
- */
- extlinkparam: function ( nodes, replacements ) {
- var replacement,
- index = parseInt( nodes[ 0 ], 10 );
- if ( index < replacements.length ) {
- replacement = replacements[ index ];
- } else {
- replacement = '$' + ( index + 1 );
- }
- return this.extlink( [ replacement, nodes[ 1 ] ] );
- },
-
/**
* Transform parsed structure into pluralization
* n.b. The first node may be a non-integer (for instance, a string representing an Arabic number).
for ( formIndex = 0; formIndex < forms.length; formIndex++ ) {
form = forms[ formIndex ];
- if ( form.jquery && form.hasClass( 'mediaWiki_htmlEmitter' ) ) {
+ if ( form instanceof jQuery && form.hasClass( 'mediaWiki_htmlEmitter' ) ) {
// This is a nested node, may be an explicit plural form like 5=[$2 linktext]
firstChild = form.contents().get( 0 );
if ( firstChild && firstChild.nodeType === Node.TEXT_NODE ) {
process( tasks, QUnit.start );
} );
- QUnit.test( 'Links', 7, function ( assert ) {
- var expectedDisambiguationsText,
+ QUnit.test( 'Links', 11, function ( assert ) {
+ var testCases,
+ expectedDisambiguationsText,
expectedMultipleBars,
expectedSpecialCharacters;
expectedListUsersSitename,
'Piped wikilink with parser function in the text'
);
+
+ testCases = [
+ [
+ 'extlink-html-full',
+ 'asd [http://example.org <strong>Example</strong>] asd',
+ 'asd <a href="http://example.org"><strong>Example</strong></a> asd'
+ ],
+ [
+ 'extlink-html-partial',
+ 'asd [http://example.org foo <strong>Example</strong> bar] asd',
+ 'asd <a href="http://example.org">foo <strong>Example</strong> bar</a> asd'
+ ],
+ [
+ 'wikilink-html-full',
+ 'asd [[Example|<strong>Example</strong>]] asd',
+ 'asd <a title="Example" href="/wiki/Example"><strong>Example</strong></a> asd'
+ ],
+ [
+ 'wikilink-html-partial',
+ 'asd [[Example|foo <strong>Example</strong> bar]] asd',
+ 'asd <a title="Example" href="/wiki/Example">foo <strong>Example</strong> bar</a> asd'
+ ]
+ ];
+
+ $.each( testCases, function () {
+ var
+ key = this[ 0 ],
+ input = this[ 1 ],
+ output = this[ 2 ];
+ mw.messages.set( key, input );
+ assert.htmlEqual(
+ formatParse( key ),
+ output,
+ 'HTML in links: ' + key
+ );
+ } );
+ } );
+
+ QUnit.test( 'Replacements in links', 14, function ( assert ) {
+ var testCases = [
+ [
+ 'extlink-param-href-full',
+ 'asd [$1 Example] asd',
+ 'asd <a href="http://example.com">Example</a> asd'
+ ],
+ [
+ 'extlink-param-href-partial',
+ 'asd [$1/example Example] asd',
+ 'asd <a href="http://example.com/example">Example</a> asd'
+ ],
+ [
+ 'extlink-param-text-full',
+ 'asd [http://example.org $2] asd',
+ 'asd <a href="http://example.org">Text</a> asd'
+ ],
+ [
+ 'extlink-param-text-partial',
+ 'asd [http://example.org Example $2] asd',
+ 'asd <a href="http://example.org">Example Text</a> asd'
+ ],
+ [
+ 'extlink-param-both-full',
+ 'asd [$1 $2] asd',
+ 'asd <a href="http://example.com">Text</a> asd'
+ ],
+ [
+ 'extlink-param-both-partial',
+ 'asd [$1/example Example $2] asd',
+ 'asd <a href="http://example.com/example">Example Text</a> asd'
+ ],
+ [
+ 'wikilink-param-href-full',
+ 'asd [[$1|Example]] asd',
+ 'asd <a title="Example" href="/wiki/Example">Example</a> asd'
+ ],
+ [
+ 'wikilink-param-href-partial',
+ 'asd [[$1/Test|Example]] asd',
+ 'asd <a title="Example/Test" href="/wiki/Example/Test">Example</a> asd'
+ ],
+ [
+ 'wikilink-param-text-full',
+ 'asd [[Example|$2]] asd',
+ 'asd <a title="Example" href="/wiki/Example">Text</a> asd'
+ ],
+ [
+ 'wikilink-param-text-partial',
+ 'asd [[Example|Example $2]] asd',
+ 'asd <a title="Example" href="/wiki/Example">Example Text</a> asd'
+ ],
+ [
+ 'wikilink-param-both-full',
+ 'asd [[$1|$2]] asd',
+ 'asd <a title="Example" href="/wiki/Example">Text</a> asd'
+ ],
+ [
+ 'wikilink-param-both-partial',
+ 'asd [[$1/Test|Example $2]] asd',
+ 'asd <a title="Example/Test" href="/wiki/Example/Test">Example Text</a> asd'
+ ],
+ [
+ 'wikilink-param-unpiped-full',
+ 'asd [[$1]] asd',
+ 'asd <a title="Example" href="/wiki/Example">Example</a> asd'
+ ],
+ [
+ 'wikilink-param-unpiped-partial',
+ 'asd [[$1/Test]] asd',
+ 'asd <a title="Example/Test" href="/wiki/Example/Test">Example/Test</a> asd'
+ ]
+ ];
+
+ $.each( testCases, function () {
+ var
+ key = this[ 0 ],
+ input = this[ 1 ],
+ output = this[ 2 ],
+ paramHref = key.slice( 0, 8 ) === 'wikilink' ? 'Example' : 'http://example.com',
+ paramText = 'Text';
+ mw.messages.set( key, input );
+ assert.htmlEqual(
+ formatParse( key, paramHref, paramText ),
+ output,
+ 'Replacements in links: ' + key
+ );
+ } );
} );
// Tests that {{-transformation vs. general parsing are done as requested
'Mismatched HTML start and end tag treated as text'
);
- // TODO (mattflaschen, 2013-03-18): It's not a security issue, but there's no real
- // reason the htmlEmitter span needs to be here. It's an artifact of how emitting works.
mw.messages.set( 'jquerymsg-script-and-external-link', '<script>alert( "jquerymsg-script-and-external-link test" );</script> [http://example.com <i>Foo</i> bar]' );
assert.htmlEqual(
formatParse( 'jquerymsg-script-and-external-link' ),
- '<script>alert( "jquerymsg-script-and-external-link test" );</script> <a href="http://example.com"><span class="mediaWiki_htmlEmitter"><i>Foo</i> bar</span></a>',
+ '<script>alert( "jquerymsg-script-and-external-link test" );</script> <a href="http://example.com"><i>Foo</i> bar</a>',
'HTML tags in external links not interfering with escaping of other tags'
);
mw.messages.set( 'jquerymsg-link-script', '[http://example.com <script>alert( "jquerymsg-link-script test" );</script>]' );
assert.htmlEqual(
formatParse( 'jquerymsg-link-script' ),
- '<a href="http://example.com"><span class="mediaWiki_htmlEmitter"><script>alert( "jquerymsg-link-script test" );</script></span></a>',
+ '<a href="http://example.com"><script>alert( "jquerymsg-link-script test" );</script></a>',
'Non-whitelisted HTML tag in external link anchor treated as text'
);
mw.messages.set( 'jquerymsg-wikitext-contents-script', '<i><script>Script inside</script></i>' );
assert.htmlEqual(
formatParse( 'jquerymsg-wikitext-contents-script' ),
- '<i><span class="mediaWiki_htmlEmitter"><script>Script inside</script></span></i>',
+ '<i><script>Script inside</script></i>',
'Contents of valid tag are treated as wikitext, so invalid HTML element is treated as text'
);