瀏覽代碼

Merge pull request #25 from ckeditor/t/13

Feature: Simplified the URL configuration of the providers making both the protocol (`http://`, `https://`) and the `www` subdomain prefix optional in the `RegExps`. Closes #13.

Passed the entire output of the match against the provider `RegExp` to the `MediaEmbedProvider#html` function (previously only the last match), allowing more complex media previews. 

Improved the URL `RegExp` for the YouTube provider to consider URLs containing dashes. Closes #26.
Aleksander Nowodzinski 7 年之前
父節點
當前提交
7087667a74

+ 2 - 2
packages/ckeditor5-media-embed/docs/features/media-embed.md

@@ -148,7 +148,7 @@ ClassicEditor
 			providers: [
 				{
 					// An URL regexp or array of URL regexps:
-					url: /^(https:\/\/)?(www\.)?example\.com\/media\/(\w+)/,
+					url: /^example\.com\/media\/(\w+)/,
 
 					// To be defined only if the media is previewable:
 					html: mediaId => '...'
@@ -365,7 +365,7 @@ mediaEmbed: {
 	extraProviders: [
 		{
 			name: 'ckeditor',
-			url: /^(https:\/\/)?(www\.)?ckeditor\.com/
+			url: /^ckeditor\.com/
 		}
 	]
 }

+ 22 - 14
packages/ckeditor5-media-embed/src/mediaembed.js

@@ -49,14 +49,19 @@ export default class MediaEmbed extends Plugin {
  *		{
  *			name: 'example',
  *
- *			// The following RegExp matches https://www.example.com/media/{media id}
- *			// with optional "https://" and "www" prefixes.
- *			url: /^(https:\/\/)?(www\.)?example\.com\/media\/(\w+)/,
+ *			// The following RegExp matches https://www.example.com/media/{media id},
+ *			// (either with "http(s)://" and "www" or without), so the valid URLs are:
+ *			//
+ *			// * https://www.example.com/media/{media id},
+ *			// * http://www.example.com/media/{media id},
+ *			// * www.example.com/media/{media id},
+ *			// * example.com/media/{media id}
+ *			url: /^example\.com\/media\/(\w+)/,
  *
  *			// The rendering function of the provider.
  *			// Used to represent the media when editing the content (i.e. in the view)
  *			// and also in the data output of the editor if semantic data output is disabled.
- *			html: mediaId => `The HTML representing the media with ID=${ mediaId }.`
+ *			html: match => `The HTML representing the media with ID=${ match[ 1 ] }.`
  *		}
  *
  * You can allow any sort of media in the editor using the "allow–all" `RegExp`.
@@ -65,14 +70,14 @@ export default class MediaEmbed extends Plugin {
  *
  *		{
  *			name: 'allow-all',
- *			url: /^(https:\/\/)?(www\.)?.+/
+ *			url: /^.+/
  *		}
  *
  * To implement a responsive media, you can use the following HTML structure:
  *
  *		{
  *			...
- *			html: mediaId =>
+ *			html: match =>
  *				'<div style="position:relative; padding-bottom:100%; height:0">' +
  *					'<iframe src="..." frameborder="0" ' +
  *						'style="position:absolute; width:100%; height:100%; top:0; left:0">' +
@@ -84,10 +89,13 @@ export default class MediaEmbed extends Plugin {
  * @property {String} name The name of the provider. Used e.g. when
  * {@link module:media-embed/mediaembed~MediaEmbedConfig#removeProviders removing providers}.
  * @property {RegExp|Array.<RegExp>} url The `RegExp` object (or array of objects) defining the URL of the media.
- * If any URL matches the `RegExp`, it becomes the media in editor model, as defined by the provider. The content
- * of the last matching group is passed to the `html` rendering function of the media.
- * @property {Function} [html] (optional) Rendering function of the media. The function receives the content of
- * the last matching group from the corresponding `url` `RegExp` as an argument, allowing rendering a dedicated
+ * If any URL matches the `RegExp`, it becomes the media in editor model, as defined by the provider. The result
+ * of matching (output of `String.prototype.match()`) is passed to the `html` rendering function of the media.
+ *
+ * **Note:** You do not need to include the protocol (`http://`, `https://`) and `www` sub–domain in your `RegExps`,
+ * they are stripped from the URLs before matching anyway.
+ * @property {Function} [html] (optional) Rendering function of the media. The function receives the entire matching
+ * array from the corresponding `url` `RegExp` as an argument, allowing rendering a dedicated
  * preview of a media identified by a certain id or a hash. When not defined, the media embed feature
  * will use a generic media representation in the view and output data.
  * Note that when
@@ -161,8 +169,8 @@ export default class MediaEmbed extends Plugin {
  *					providers: [
  *						{
  *							 name: 'myProvider',
- *							 url: /^(https:\/\/)?(www\.)?example\.com\/media\/(\w+)/,
- *							 html: mediaId => '...'
+ *							 url: /^example\.com\/media\/(\w+)/,
+ *							 html: match => '...'
  *						},
  *						...
  * 					]
@@ -191,8 +199,8 @@ export default class MediaEmbed extends Plugin {
  *					extraProviders: [
  *						{
  *							 name: 'extraProvider',
- *							 url: /^(https:\/\/)?(www\.)?example\.com\/media\/(\w+)/,
- *							 html: mediaId => '...'
+ *							 url: /^example\.com\/media\/(\w+)/,
+ *							 html: match => '...'
  *						},
  *						...
  * 					]

+ 69 - 49
packages/ckeditor5-media-embed/src/mediaembedediting.js

@@ -11,8 +11,8 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
 import { modelToViewUrlAttributeConverter } from './converters';
 import MediaEmbedCommand from './mediaembedcommand';
+import MediaRegistry from './mediaregistry';
 import { toMediaWidget, createMediaFigureElement } from './utils';
-import { MediaRegistry } from './mediaregistry';
 import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
 import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
 
@@ -34,88 +34,108 @@ export default class MediaEmbedEditing extends Plugin {
 			providers: [
 				{
 					name: 'dailymotion',
-					url: /^(https:\/\/)?(www\.)?dailymotion\.com\/video\/(\w+)/,
-					html: id =>
-						'<div style="position: relative; padding-bottom: 100%; height: 0; ">' +
-							`<iframe src="https://www.dailymotion.com/embed/video/${ id }" ` +
-								'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' +
-								'frameborder="0" width="480" height="270" allowfullscreen allow="autoplay">' +
-							'</iframe>' +
-						'</div>'
+					url: /^dailymotion\.com\/video\/(\w+)/,
+					html: match => {
+						const id = match[ 1 ];
+
+						return (
+							'<div style="position: relative; padding-bottom: 100%; height: 0; ">' +
+								`<iframe src="https://www.dailymotion.com/embed/video/${ id }" ` +
+									'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' +
+									'frameborder="0" width="480" height="270" allowfullscreen allow="autoplay">' +
+								'</iframe>' +
+							'</div>'
+						);
+					}
 				},
 
 				{
 					name: 'spotify',
 					url: [
-						/^(https:\/\/)?(www\.)?open\.spotify\.com\/(artist\/\w+)/,
-						/^(https:\/\/)?(www\.)?open\.spotify\.com\/(album\/\w+)/,
-						/^(https:\/\/)?(www\.)?open\.spotify\.com\/(track\/\w+)/
+						/^open\.spotify\.com\/(artist\/\w+)/,
+						/^open\.spotify\.com\/(album\/\w+)/,
+						/^open\.spotify\.com\/(track\/\w+)/
 					],
-					html: id =>
-						'<div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 126%;">' +
-							`<iframe src="https://open.spotify.com/embed/${ id }" ` +
-								'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' +
-								'frameborder="0" allowtransparency="true" allow="encrypted-media">' +
-							'</iframe>' +
-						'</div>'
+					html: match => {
+						const id = match[ 1 ];
+
+						return (
+							'<div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 126%;">' +
+								`<iframe src="https://open.spotify.com/embed/${ id }" ` +
+									'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' +
+									'frameborder="0" allowtransparency="true" allow="encrypted-media">' +
+								'</iframe>' +
+							'</div>'
+						);
+					}
 				},
 
 				{
 					name: 'youtube',
 					url: [
-						/^(https:\/\/)?(www\.)?youtube\.com\/watch\?v=(\w+)/,
-						/^(https:\/\/)?(www\.)?youtube\.com\/v\/(\w+)/,
-						/^(https:\/\/)?(www\.)?youtube\.com\/embed\/(\w+)/,
-						/^(https:\/\/)?youtu\.be\/(\w+)/
+						/^youtube\.com\/watch\?v=([\w-]+)/,
+						/^youtube\.com\/v\/([\w-]+)/,
+						/^youtube\.com\/embed\/([\w-]+)/,
+						/^youtu\.be\/([\w-]+)/
 					],
-					html: id =>
-						'<div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 56.2493%;">' +
-							`<iframe src="https://www.youtube.com/embed/${ id }" ` +
-								'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' +
-								'frameborder="0" allow="autoplay; encrypted-media" allowfullscreen>' +
-							'</iframe>' +
-						'</div>'
+					html: match => {
+						const id = match[ 1 ];
+
+						return (
+							'<div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 56.2493%;">' +
+								`<iframe src="https://www.youtube.com/embed/${ id }" ` +
+									'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' +
+									'frameborder="0" allow="autoplay; encrypted-media" allowfullscreen>' +
+								'</iframe>' +
+							'</div>'
+						);
+					}
 				},
 
 				{
 					name: 'vimeo',
 					url: [
-						/^(https:\/\/)?(www\.)?vimeo\.com\/(\d+)/,
-						/^(https:\/\/)?(www\.)?vimeo\.com\/[^/]+\/[^/]+\/video\/(\d+)/,
-						/^(https:\/\/)?(www\.)?vimeo\.com\/album\/[^/]+\/video\/(\d+)/,
-						/^(https:\/\/)?(www\.)?vimeo\.com\/channels\/[^/]+\/(\d+)/,
-						/^(https:\/\/)?(www\.)?vimeo\.com\/groups\/[^/]+\/videos\/(\d+)/,
-						/^(https:\/\/)?(www\.)?vimeo\.com\/ondemand\/[^/]+\/(\d+)/,
-						/^(https:\/\/)?(www\.)?player\.vimeo\.com\/video\/(\d+)/
+						/^vimeo\.com\/(\d+)/,
+						/^vimeo\.com\/[^/]+\/[^/]+\/video\/(\d+)/,
+						/^vimeo\.com\/album\/[^/]+\/video\/(\d+)/,
+						/^vimeo\.com\/channels\/[^/]+\/(\d+)/,
+						/^vimeo\.com\/groups\/[^/]+\/videos\/(\d+)/,
+						/^vimeo\.com\/ondemand\/[^/]+\/(\d+)/,
+						/^player\.vimeo\.com\/video\/(\d+)/
 					],
-					html: id =>
-						'<div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 56.2493%;">' +
-							`<iframe src="https://player.vimeo.com/video/${ id }" ` +
-								'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' +
-								'frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen>' +
-							'</iframe>' +
-						'</div>'
+					html: match => {
+						const id = match[ 1 ];
+
+						return (
+							'<div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 56.2493%;">' +
+								`<iframe src="https://player.vimeo.com/video/${ id }" ` +
+									'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' +
+									'frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen>' +
+								'</iframe>' +
+							'</div>'
+						);
+					}
 				},
 
 				{
 					name: 'instagram',
-					url: /^(https:\/\/)?(www\.)?instagram\.com\/p\/(\w+)/
+					url: /^instagram\.com\/p\/(\w+)/
 				},
 				{
 					name: 'twitter',
-					url: /^(https:\/\/)?(www\.)?twitter\.com/
+					url: /^twitter\.com/
 				},
 				{
 					name: 'googleMaps',
-					url: /^(https:\/\/)?(www\.)?google\.com\/maps/
+					url: /^google\.com\/maps/
 				},
 				{
 					name: 'flickr',
-					url: /^(https:\/\/)?(www\.)?flickr\.com/
+					url: /^flickr\.com/
 				},
 				{
 					name: 'facebook',
-					url: /^(https:\/\/)?(www\.)?facebook\.com/
+					url: /^facebook\.com/
 				},
 			]
 		} );

+ 58 - 5
packages/ckeditor5-media-embed/src/mediaregistry.js

@@ -22,7 +22,7 @@ const mediaPlaceholderIconViewBox = '0 0 64 42';
  *
  * Mostly used by the {@link module:media-embed/mediaembedediting~MediaEmbedEditing} plugin.
  */
-export class MediaRegistry {
+export default class MediaRegistry {
 	/**
 	 * Creates an instance of the {@link module:media-embed/mediaregistry~MediaRegistry} class.
 	 *
@@ -102,7 +102,7 @@ export class MediaRegistry {
 	/**
 	 * Returns a `Media` instance for the given URL.
 	 *
-	 * @private
+	 * @protected
 	 * @param {String} url The url of the media.
 	 * @returns {module:media-embed/mediaregistry~Media|null} The `Media` instance or `null` when there's none.
 	 */
@@ -122,7 +122,7 @@ export class MediaRegistry {
 			}
 
 			for ( const subPattern of pattern ) {
-				const match = url.match( subPattern );
+				const match = this._getUrlMatches( url, subPattern );
 
 				if ( match ) {
 					return new Media( this.locale, url, match, contentRenderer );
@@ -132,6 +132,41 @@ export class MediaRegistry {
 
 		return null;
 	}
+
+	/**
+	 * Tries to match `url` to `pattern`.
+	 *
+	 * @private
+	 * @param {String} url The url of the media.
+	 * @param {RegExp} pattern The pattern that should accept the media url.
+	 * @returns {Array|null}
+	 */
+	_getUrlMatches( url, pattern ) {
+		// 1. Try to match without stripping the protocol and "www" sub-domain.
+		let match = url.match( pattern );
+
+		if ( match ) {
+			return match;
+		}
+
+		// 2. Try to match after stripping the protocol.
+		let rawUrl = url.replace( /^https?:\/\//, '' );
+		match = rawUrl.match( pattern );
+
+		if ( match ) {
+			return match;
+		}
+
+		// 3. Try to match after stripping the "www" sub-domain.
+		rawUrl = rawUrl.replace( /^www\./, '' );
+		match = rawUrl.match( pattern );
+
+		if ( match ) {
+			return match;
+		}
+
+		return null;
+	}
 }
 
 /**
@@ -148,7 +183,7 @@ class Media {
 		 *
 		 * @member {String}
 		 */
-		this.url = url;
+		this.url = this._getValidUrl( url );
 
 		/**
 		 * Shorthand for {@link module:utils/locale~Locale#t}.
@@ -223,7 +258,7 @@ class Media {
 	 */
 	_getContentHtml( options ) {
 		if ( this._contentRenderer ) {
-			return this._contentRenderer( this._match.pop() );
+			return this._contentRenderer( this._match );
 		} else {
 			// The placeholder only makes sense for editing view and media which have URLs.
 			// Placeholder is never displayed in data and URL-less media have no content.
@@ -284,4 +319,22 @@ class Media {
 
 		return placeholder.outerHTML;
 	}
+
+	/**
+	 * Returns full url to specified media.
+	 *
+	 * @param {String} url The url of the media.
+	 * @returns {String|null}
+	 */
+	_getValidUrl( url ) {
+		if ( !url ) {
+			return null;
+		}
+
+		if ( url.match( /^https?/ ) ) {
+			return url;
+		}
+
+		return 'https://' + url;
+	}
 }

+ 38 - 18
packages/ckeditor5-media-embed/tests/mediaembedediting.js

@@ -10,6 +10,7 @@ import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils
 import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import log from '@ckeditor/ckeditor5-utils/src/log';
+import env from '@ckeditor/ckeditor5-utils/src/env';
 
 describe( 'MediaEmbedEditing', () => {
 	let editor, model, doc, view;
@@ -20,28 +21,28 @@ describe( 'MediaEmbedEditing', () => {
 		A: {
 			name: 'A',
 			url: /^foo\.com\/(\w+)/,
-			html: id => `A, id=${ id }`
+			html: match => `A, id=${ match[ 1 ] }`
 		},
 		B: {
 			name: 'B',
 			url: /^bar\.com\/(\w+)/,
-			html: id => `B, id=${ id }`
+			html: match => `B, id=${ match[ 1 ] }`
 		},
 		C: {
 			name: 'C',
 			url: /^\w+\.com\/(\w+)/,
-			html: id => `C, id=${ id }`
+			html: match => `C, id=${ match[ 1 ] }`
 		},
 
 		extraA: {
 			name: 'extraA',
 			url: /^foo\.com\/(\w+)/,
-			html: id => `extraA, id=${ id }`
+			html: match => `extraA, id=${ match[ 1 ] }`
 		},
 		extraB: {
 			name: 'extraB',
 			url: /^\w+\.com\/(\w+)/,
-			html: id => `extraB, id=${ id }`
+			html: match => `extraB, id=${ match[ 1 ] }`
 		},
 
 		previewLess: {
@@ -51,10 +52,15 @@ describe( 'MediaEmbedEditing', () => {
 		allowEverything: {
 			name: 'allow-everything',
 			url: /(.*)/,
-			html: id => `allow-everything, id=${ id }`
+			html: match => `allow-everything, id=${ match[ 1 ] }`
 		}
 	};
 
+	beforeEach( () => {
+		// Most tests assume non-edge environment but we do not set `contenteditable=false` on Edge so stub `env.isEdge`.
+		testUtils.sinon.stub( env, 'isEdge' ).get( () => false );
+	} );
+
 	describe( 'constructor()', () => {
 		describe( 'configuration', () => {
 			describe( '#providers', () => {
@@ -95,7 +101,7 @@ describe( 'MediaEmbedEditing', () => {
 
 						expect( getViewData( editor.editing.view, { withoutSelection: true, renderUIElements: true } ) ).to.equal(
 							'<figure class="ck-widget media" contenteditable="false">' +
-								'<div class="ck-media__wrapper" data-oembed-url="foo.com/123">' +
+								'<div class="ck-media__wrapper" data-oembed-url="https://foo.com/123">' +
 									'A, id=123' +
 								'</div>' +
 							'</figure>'
@@ -105,7 +111,7 @@ describe( 'MediaEmbedEditing', () => {
 
 						expect( getViewData( editor.editing.view, { withoutSelection: true, renderUIElements: true } ) ).to.equal(
 							'<figure class="ck-widget media" contenteditable="false">' +
-								'<div class="ck-media__wrapper" data-oembed-url="bar.com/123">' +
+								'<div class="ck-media__wrapper" data-oembed-url="https://bar.com/123">' +
 									'B, id=123' +
 								'</div>' +
 							'</figure>'
@@ -115,7 +121,7 @@ describe( 'MediaEmbedEditing', () => {
 
 						expect( getViewData( editor.editing.view, { withoutSelection: true, renderUIElements: true } ) ).to.equal(
 							'<figure class="ck-widget media" contenteditable="false">' +
-								'<div class="ck-media__wrapper" data-oembed-url="anything.com/123">' +
+								'<div class="ck-media__wrapper" data-oembed-url="https://anything.com/123">' +
 									'C, id=123' +
 								'</div>' +
 							'</figure>'
@@ -218,6 +224,19 @@ describe( 'MediaEmbedEditing', () => {
 							'</div>' );
 						} );
 
+						// See: https://github.com/ckeditor/ckeditor5-media-embed/issues/26
+						it( 'upcasts the URL that contains a dash (youtube)', () => {
+							testMediaUpcast( [
+								'https://www.youtube.com/watch?v=euqbMkM-QQk'
+							],
+							'<div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 56.2493%;">' +
+								'<iframe src="https://www.youtube.com/embed/euqbMkM-QQk" ' +
+									'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' +
+									'frameborder="0" allow="autoplay; encrypted-media" allowfullscreen="">' +
+								'</iframe>' +
+							'</div>' );
+						} );
+
 						it( 'upcasts the URL (vimeo)', () => {
 							testMediaUpcast( [
 								'https://www.vimeo.com/1234',
@@ -331,7 +350,7 @@ describe( 'MediaEmbedEditing', () => {
 
 						expect( getViewData( editor.editing.view, { withoutSelection: true, renderUIElements: true } ) ).to.equal(
 							'<figure class="ck-widget media" contenteditable="false">' +
-								'<div class="ck-media__wrapper" data-oembed-url="foo.com/123">' +
+								'<div class="ck-media__wrapper" data-oembed-url="https://foo.com/123">' +
 									'A, id=123' +
 								'</div>' +
 							'</figure>'
@@ -341,7 +360,7 @@ describe( 'MediaEmbedEditing', () => {
 
 						expect( getViewData( editor.editing.view, { withoutSelection: true, renderUIElements: true } ) ).to.equal(
 							'<figure class="ck-widget media" contenteditable="false">' +
-								'<div class="ck-media__wrapper" data-oembed-url="anything.com/123">' +
+								'<div class="ck-media__wrapper" data-oembed-url="https://anything.com/123">' +
 									'extraB, id=123' +
 								'</div>' +
 							'</figure>'
@@ -365,7 +384,7 @@ describe( 'MediaEmbedEditing', () => {
 
 						expect( getViewData( editor.editing.view, { withoutSelection: true, renderUIElements: true } ) ).to.equal(
 							'<figure class="ck-widget media" contenteditable="false">' +
-								'<div class="ck-media__wrapper" data-oembed-url="bar.com/123">' +
+								'<div class="ck-media__wrapper" data-oembed-url="https://bar.com/123">' +
 									'B, id=123' +
 								'</div>' +
 							'</figure>'
@@ -388,7 +407,7 @@ describe( 'MediaEmbedEditing', () => {
 
 						expect( getViewData( editor.editing.view, { withoutSelection: true, renderUIElements: true } ) ).to.equal(
 							'<figure class="ck-widget media" contenteditable="false">' +
-								'<div class="ck-media__wrapper" data-oembed-url="bar.com/123">' +
+								'<div class="ck-media__wrapper" data-oembed-url="https://bar.com/123">' +
 									'B, id=123' +
 								'</div>' +
 							'</figure>'
@@ -415,7 +434,7 @@ describe( 'MediaEmbedEditing', () => {
 
 						expect( getViewData( editor.editing.view, { withoutSelection: true, renderUIElements: true } ) ).to.equal(
 							'<figure class="ck-widget media" contenteditable="false">' +
-								'<div class="ck-media__wrapper" data-oembed-url="bar.com/123">' +
+								'<div class="ck-media__wrapper" data-oembed-url="https://bar.com/123">' +
 									'B, id=123' +
 								'</div>' +
 							'</figure>'
@@ -883,12 +902,13 @@ describe( 'MediaEmbedEditing', () => {
 
 	function testMediaUpcast( urls, expected ) {
 		for ( const url of urls ) {
-			editor.setData(
-				`<figure class="media"><div data-oembed-url="${ url }"></div></figure>` );
+			editor.setData( `<figure class="media"><div data-oembed-url="${ url }"></div></figure>` );
 
 			const viewData = getViewData( view, { withoutSelection: true, renderUIElements: true } );
 			let expectedRegExp;
 
+			const expectedUrl = url.match( /^https?:\/\// ) ? url : 'https://' + url;
+
 			if ( expected ) {
 				expectedRegExp = new RegExp(
 					'<figure[^>]+>' +
@@ -902,8 +922,8 @@ describe( 'MediaEmbedEditing', () => {
 						'<div[^>]+>' +
 							'<div class="ck ck-media__placeholder ck-reset_all">' +
 								'<div class="ck-media__placeholder__icon">.*</div>' +
-								`<a class="ck-media__placeholder__url" href="${ url }" target="new">` +
-									`<span class="ck-media__placeholder__url__text">${ url }</span>` +
+								`<a class="ck-media__placeholder__url" href="${ expectedUrl }" target="new">` +
+									`<span class="ck-media__placeholder__url__text">${ expectedUrl }</span>` +
 									'<span class="ck ck-tooltip ck-tooltip_s">' +
 										'<span class="ck ck-tooltip__text">Open media in new tab</span>' +
 									'</span>' +

+ 126 - 0
packages/ckeditor5-media-embed/tests/mediaregistry.js

@@ -0,0 +1,126 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import MediaRegistry from '../src/mediaregistry';
+import log from '@ckeditor/ckeditor5-utils/src/log';
+
+describe( 'MediaRegistry', () => {
+	describe( 'constructor()', () => {
+		it( 'filters out providers that should be removed', () => {
+			const providers = [
+				{ name: 'dailymotion', url: [] },
+				{ name: 'spotify', url: [] },
+				{ name: 'youtube', url: [] },
+				{ name: 'vimeo', url: [] },
+			];
+			const removeProviders = [ 'spotify' ];
+
+			const mediaRegistry = new MediaRegistry( {}, { providers, removeProviders } );
+			const availableProviders = mediaRegistry.providerDefinitions.map( provider => provider.name );
+
+			expect( availableProviders ).to.deep.equal( [ 'dailymotion', 'youtube', 'vimeo' ] );
+		} );
+
+		it( 'allows extending providers using `extraProviders` option', () => {
+			const providers = [
+				{ name: 'dailymotion', url: [] },
+				{ name: 'youtube', url: [] },
+				{ name: 'vimeo', url: [] },
+			];
+			const extraProviders = [
+				{ name: 'spotify', url: [] },
+			];
+
+			const mediaRegistry = new MediaRegistry( {}, { providers, extraProviders } );
+			const availableProviders = mediaRegistry.providerDefinitions.map( provider => provider.name );
+
+			expect( availableProviders ).to.deep.equal( [ 'dailymotion', 'youtube', 'vimeo', 'spotify' ] );
+		} );
+
+		it( 'logs a warning when provider\'s name is not defined', () => {
+			const logStub = sinon.stub( log, 'warn' );
+
+			const providers = [
+				{ url: [ /dailymotion\.com/ ] },
+				{ name: 'spotify', url: [] },
+				{ name: 'youtube', url: [] },
+				{ name: 'vimeo', url: [] },
+			];
+
+			const mediaRegistry = new MediaRegistry( {}, { providers } );
+			const availableProviders = mediaRegistry.providerDefinitions.map( provider => provider.name );
+
+			expect( availableProviders ).to.deep.equal( [ 'spotify', 'youtube', 'vimeo' ] );
+			expect( logStub.calledOnce ).to.equal( true );
+			expect( logStub.firstCall.args[ 0 ] ).to.equal(
+				'media-embed-no-provider-name: The configured media provider has no name and cannot be used.'
+			);
+			expect( logStub.firstCall.args[ 1 ] ).to.deep.equal( { provider: { url: [ /dailymotion\.com/ ] } } );
+		} );
+	} );
+
+	describe( '_getMedia()', () => {
+		let mediaRegistry, htmlSpy;
+
+		beforeEach( () => {
+			htmlSpy = sinon.spy();
+
+			mediaRegistry = new MediaRegistry( {}, {
+				providers: [
+					{
+						name: 'youtube',
+						url: [
+							/^youtube\.com\/watch\?v=([\w-]+)/,
+							/^youtube\.com\/v\/([\w-]+)/,
+							/^youtube\.com\/embed\/([\w-]+)/,
+							/^youtu\.be\/([\w-]+)/
+						],
+						html: htmlSpy
+					}
+				]
+			} );
+		} );
+
+		it( 'works fine for url with sub-domain and the protocol', () => {
+			const media = mediaRegistry._getMedia( 'https://www.youtube.com/watch?v=euqbMkM-QQk' );
+
+			expect( media ).is.not.null;
+			expect( media.url ).to.equal( 'https://www.youtube.com/watch?v=euqbMkM-QQk' );
+		} );
+
+		it( 'works fine for url with defined protocol', () => {
+			const media = mediaRegistry._getMedia( 'https://youtube.com/watch?v=euqbMkM-QQk' );
+
+			expect( media ).is.not.null;
+			expect( media.url ).to.equal( 'https://youtube.com/watch?v=euqbMkM-QQk' );
+		} );
+
+		it( 'works fine for url with sub-domain without protocol', () => {
+			const media = mediaRegistry._getMedia( 'www.youtube.com/watch?v=euqbMkM-QQk' );
+
+			expect( media ).is.not.null;
+			expect( media.url ).to.equal( 'https://www.youtube.com/watch?v=euqbMkM-QQk' );
+		} );
+
+		it( 'works fine for url without protocol', () => {
+			const media = mediaRegistry._getMedia( 'youtube.com/watch?v=euqbMkM-QQk' );
+
+			expect( media ).is.not.null;
+			expect( media.url ).to.equal( 'https://youtube.com/watch?v=euqbMkM-QQk' );
+		} );
+
+		it( 'passes the entire match array to render function', () => {
+			const media = mediaRegistry._getMedia( 'https://www.youtube.com/watch?v=euqbMkM-QQk' );
+
+			media._getContentHtml();
+
+			expect( htmlSpy.calledOnce ).to.equal( true );
+			expect( htmlSpy.firstCall.args[ 0 ] ).to.deep.equal( [
+				'youtube.com/watch?v=euqbMkM-QQk',
+				'euqbMkM-QQk'
+			] );
+		} );
+	} );
+} );