Przeglądaj źródła

Introduced XmlDataProcessor.

Oskar Wrobel 9 lat temu
rodzic
commit
5af1a410a7

+ 2 - 12
packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js

@@ -81,19 +81,9 @@ export default class HtmlDataProcessor {
 	 * @returns {DocumentFragment}
 	 */
 	_toDom( data ) {
-		data = `<div>${ data }</div>`;
-
-		const document = this._domParser.parseFromString( data, 'text/xml' );
-
-		// Temporary parse validation.
-		const parserError = document.querySelector( 'parsererror' );
-
-		if ( parserError ) {
-			throw new Error( parserError.querySelector( 'div' ).textContent );
-		}
-
+		const document = this._domParser.parseFromString( data, 'text/html' );
 		const fragment = document.createDocumentFragment();
-		const nodes = document.documentElement.childNodes;
+		const nodes = document.body.childNodes;
 
 		while ( nodes.length > 0 ) {
 			fragment.appendChild( nodes[ 0 ] );

+ 104 - 0
packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js

@@ -0,0 +1,104 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import BasicHtmlWriter from './basichtmlwriter.js';
+import DomConverter from '../view/domconverter.js';
+import { NBSP_FILLER } from '../view/filler.js';
+
+/**
+ * XmlDataProcessor class.
+ * This data processor implementation uses XML as input/output data.
+ *
+ * @memberOf engine.dataProcessor
+ * @implements engine.dataProcessor.DataProcessor
+ */
+export default class XmlDataProcessor {
+	/**
+	 * Creates a new instance of the XmlDataProcessor class.
+	 */
+	constructor() {
+		/**
+		 * DOMParser instance used to parse XML string to XMLDocument.
+		 *
+		 * @private
+		 * @member {DOMParser} engine.dataProcessor.XmlDataProcessor#_domParser
+		 */
+		this._domParser = new DOMParser();
+
+		/**
+		 * DOM converter used to convert DOM elements to view elements.
+		 *
+		 * @private
+		 * @member {engine.view.DomConverter} engine.dataProcessor.XmlDataProcessor#_domConverter.
+		 */
+		this._domConverter = new DomConverter( { blockFiller: NBSP_FILLER } );
+
+		/**
+		 * BasicHtmlWriter instance used to convert DOM elements to HTML string.
+		 *
+		 * @private
+		 * @member {engine.dataProcessor.BasicHtmlWriter} engine.dataProcessor.HtmlDataProcessor#_htmlWriter
+		 */
+		this._htmlWriter = new BasicHtmlWriter();
+	}
+
+	/**
+	 * Converts provided {@link engine.view.DocumentFragment DocumentFragment} to data format - in this case XML string.
+	 *
+	 * @param {engine.view.DocumentFragment} viewFragment
+	 * @returns {String} XML string.
+	 */
+	toData( viewFragment ) {
+		// Convert view DocumentFragment to DOM DocumentFragment.
+		const domFragment = this._domConverter.viewToDom( viewFragment, document );
+
+		// Convert DOM DocumentFragment to HTML output.
+		return this._htmlWriter.getHtml( domFragment );
+	}
+
+	/**
+	 * Converts provided XML string to view tree.
+	 *
+	 * @param {String} data XML string.
+	 * @returns {engine.view.Node|engine.view.DocumentFragment|null} Converted view element.
+	 */
+	toView( data ) {
+		// Convert input XML data to DOM DocumentFragment.
+		const domFragment = this._toDom( data );
+
+		// Convert DOM DocumentFragment to view DocumentFragment.
+		return this._domConverter.domToView( domFragment );
+	}
+
+	/**
+	 * Converts XML String to its DOM representation. Returns DocumentFragment, containing nodes parsed from
+	 * provided data.
+	 *
+	 * @private
+	 * @param {String} data
+	 * @returns {DocumentFragment}
+	 */
+	_toDom( data ) {
+		data = `<xml xmlns:attribute="foo" xmlns:container="foo">${ data }</xml>`;
+
+		const document = this._domParser.parseFromString( data, 'text/xml' );
+
+		// Parse validation.
+		const parserError = document.querySelector( 'parsererror' );
+
+		if ( parserError ) {
+			throw new Error( 'Parse error - ' + parserError.querySelector( 'div' ).textContent );
+		}
+
+		const fragment = document.createDocumentFragment();
+		const nodes = document.documentElement.childNodes;
+
+		while ( nodes.length > 0 ) {
+			fragment.appendChild( nodes[ 0 ] );
+		}
+
+		return fragment;
+	}
+}

+ 83 - 0
packages/ckeditor5-engine/tests/dataprocessor/xmldataprocessor.js

@@ -0,0 +1,83 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: browser-only */
+
+import XmlDataProcessor from '/ckeditor5/engine/dataprocessor/xmldataprocessor.js';
+import xssTemplates from '/tests/engine/dataprocessor/_utils/xsstemplates.js';
+import ViewDocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
+import { stringify, parse } from '/tests/engine/_utils/view.js';
+
+describe( 'XmlDataProcessor', () => {
+	const dataProcessor = new XmlDataProcessor();
+
+	describe( 'toView', () => {
+		it( 'should return empty DocumentFragment when empty string is passed', () => {
+			const fragment = dataProcessor.toView( '' );
+			expect( fragment ).to.be.an.instanceOf( ViewDocumentFragment );
+			expect( fragment.childCount ).to.equal( 0 );
+		} );
+
+		it( 'should convert XML to DocumentFragment with single text node', () => {
+			const fragment = dataProcessor.toView( 'foo bar' );
+
+			expect( stringify( fragment ) ).to.equal( 'foo bar' );
+		} );
+
+		it( 'should convert HTML to DocumentFragment with multiple child nodes', () => {
+			const fragment = dataProcessor.toView( '<p>foo</p><p>bar</p>' );
+
+			expect( stringify( fragment ) ).to.equal( '<p>foo</p><p>bar</p>' );
+		} );
+
+		it( 'should not add any additional nodes', () => {
+			const fragment = dataProcessor.toView( 'foo <b>bar</b> text' );
+
+			expect( stringify( fragment ) ).to.equal( 'foo <b>bar</b> text' );
+		} );
+
+		it( 'should thrown an error when markup is invalid', () => {
+			expect( () => {
+				dataProcessor.toView( '<b>missing closing tag' );
+			} ).to.throw( Error, /Parse error/ );
+		} );
+
+		// Test against XSS attacks.
+		for ( let name in xssTemplates ) {
+			const input = xssTemplates[ name ].replace( /%xss%/g, 'testXss()' );
+
+			it( 'should prevent XSS attacks: ' + name, ( done ) => {
+				window.testXss = sinon.spy();
+				dataProcessor.toView( input );
+
+				setTimeout( () => {
+					sinon.assert.notCalled( window.testXss );
+					done();
+				}, 10 );
+			} );
+		}
+	} );
+
+	describe( 'toData', () => {
+		it( 'should return empty string when empty DocumentFragment is passed', () => {
+			const fragment = new ViewDocumentFragment();
+
+			expect( dataProcessor.toData( fragment ) ).to.equal( '' );
+		} );
+
+		it( 'should return text if document fragment with single text node is passed', () => {
+			const fragment = new ViewDocumentFragment();
+			fragment.appendChildren( parse( 'foo bar' ) );
+
+			expect( dataProcessor.toData( fragment ) ).to.equal( 'foo bar' );
+		} );
+
+		it( 'should convert HTML to DocumentFragment with multiple child nodes', () => {
+			const fragment = parse( '<p>foo</p><p>bar</p>' );
+
+			expect( dataProcessor.toData( fragment ) ).to.equal( '<p>foo</p><p>bar</p>' );
+		} );
+	} );
+} );