/** * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ 'use strict'; /* jshint latedef:false */ import DocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js'; import Element from '/ckeditor5/engine/treeview/element.js'; import AttributeElement from '/ckeditor5/engine/treeview/attributeelement.js'; import ContainerElement from '/ckeditor5/engine/treeview/containerelement.js'; import Text from '/ckeditor5/engine/treeview/text.js'; const ELEMENT_RANGE_START_TOKEN = '['; const ELEMENT_RANGE_END_TOKEN = ']'; const TEXT_RANGE_START_TOKEN = '{'; const TEXT_RANGE_END_TOKEN = '}'; /** * Converts view elements to its string representation. * Root element can be provided as {@link engine.treeView.Element Element} or * {@link engine.treeView.DocumentFragment DocumentFragment}. * * const text = new Text( 'foobar' ); * const b = new Element( 'b', { name: 'test' }, text ); * const p = new Element( 'p', { style: 'color:red;' }, b ); * * getData( p ); //

foobar

* * Additionally {@link engine.treeView.Selection Selection} * instance can be provided, then ranges from that selection will be converted too. If range position is placed inside * element node `[` and `]` will be used there. * * const text = new Text( 'foobar' ); * const b = new Element( 'b', null, text ); * const p = new Element( 'p', null, b ); * const selection = new Selection(); * selection.addRange( Range.createFromParentsAndOffsets( p, 0, p, 1 ) ); * * getData( p, selection ); //

[foobar]

* * If range is placed inside text node `{` and `}` will be used there. * * const text = new Text( 'foobar' ); * const b = new Element( 'b', null, text ); * const p = new Element( 'p', null, b ); * const selection = new Selection(); * selection.addRange( Range.createFromParentsAndOffsets( text, 1, text, 5 ) ); * * getData( p, selection ); //

f{ooba}r

* * Additional options object can be provided. * If `options.showType` property is set to `true` element types will be * presented for {@link engine.treeView.AttributeElement AttributeElements} and {@link engine.treeView.ContainerElement * ContainerElements}. * * const attribute = new AttributeElement( 'b' ); * const container = new ContainerElement( 'p' ); * getData( attribute, null, { showType: true } ); // * getData( container, null, { showType: true } ); // * * if `options.showPriority` property is set to `true`, priority will be displayed for all * {@link engine.treeView.AttributeElement AttributeElements}. * * const attribute = new AttributeElement( 'b' ); * attribute.priority = 20; * getData( attribute, null, { showPriority: true } ); // * * @param {engine.treeView.Element|engine.treeView.DocumentFragment} root * @param {engine.treeView.Selection} [selection] * @param {Object} [options] * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed ( `` * instead of `

` and `` instead of ``. * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed. * @returns {String} */ export function getData( root, selection, options ) { const viewStringify = new ViewStringify( root, selection, options ); return viewStringify.stringify(); } /** * Private helper class used for converting view tree to string. * * @private */ class ViewStringify { /** * Creates ViewStringify instance. * @param root * @param {engine.treeView.Selection} [selection=null] Selection which ranges should be also converted to string. * @param {Object} [options] Options object. * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed ( `` * instead of `

` and `` instead of ``. * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed. */ constructor( root, selection = null, options = {} ) { this.root = root; this.selection = selection; this.ranges = []; if ( this.selection ) { this.ranges = [ ...selection.getRanges() ]; } this.showType = !!options.showType; this.showPriority = !!options.showPriority; } /** * Converts view to string. * * @returns {string} String representation of the view elements. */ stringify() { let result = ''; this._walkView( this.root, ( chunk ) => { result += chunk; } ); return result; } /** * Executes simple walker that iterates over all elements in the view tree starting from root element. * Calls `callback` with parsed chunks of string data. * * @private * @param {engine.treeView.DocumentFragment|engine.treeView.Element|engine.treeView.Text} root * @param {Function} callback */ _walkView( root, callback ) { const isElement = root instanceof Element; if ( isElement || root instanceof DocumentFragment ) { if ( isElement ) { callback( this._stringifyElementOpen( root ) ); } let offset = 0; callback( this._stringifyElementRanges( root, offset ) ); for ( let child of root.getChildren() ) { this._walkView( child, callback ); offset++; callback( this._stringifyElementRanges( root, offset ) ); } if ( isElement ) { callback( this._stringifyElementClose( root ) ); } } if ( root instanceof Text ) { callback( this._stringifyTextRanges( root ) ); } } /** * Checks if given {@link engine.treeView.Element Element} has {@link engine.treeView.Range#start range start} or * {@link engine.treeView.Range#start range end} placed at given offset and returns its string representation. * * @private * @param {engine.treeView.Element} element * @param {Number} offset */ _stringifyElementRanges( element, offset ) { let start = ''; let end = ''; let collapsed = ''; for ( let range of this.ranges ) { if ( range.start.parent == element && range.start.offset === offset ) { if ( range.isCollapsed ) { collapsed += ELEMENT_RANGE_START_TOKEN + ELEMENT_RANGE_END_TOKEN; } else { start += ELEMENT_RANGE_START_TOKEN; } } if ( range.end.parent === element && range.end.offset === offset && !range.isCollapsed ) { end += ELEMENT_RANGE_END_TOKEN; } } return end + collapsed + start; } /** * Checks if given {@link engine.treeView.Element Text node} has {@link engine.treeView.Range#start range start} or * {@link engine.treeView.Range#start range end} placed somewhere inside. Returns string representation of text * with range delimiters placed inside. * * @private * @param {engine.treeView.Text} node */ _stringifyTextRanges( node ) { const length = node.data.length; let result = node.data.split( '' ); // Add one more element for ranges ending after last character in text. result[ length ] = ''; // Represent each letter as object with information about opening/closing ranges at each offset. result = result.map( ( letter ) => { return { letter: letter, start: '', end: '', collapsed: '' }; } ); for ( let range of this.ranges ) { const start = range.start; const end = range.end; if ( start.parent == node && start.offset >= 0 && start.offset <= length ) { if ( range.isCollapsed ) { result[ end.offset ].collapsed += TEXT_RANGE_START_TOKEN + TEXT_RANGE_END_TOKEN; } else { result[ start.offset ].start += TEXT_RANGE_START_TOKEN; } } if ( end.parent == node && end.offset >= 0 && end.offset <= length && !range.isCollapsed ) { result[ end.offset ].end += TEXT_RANGE_END_TOKEN; } } return result.map( item => item.end + item.collapsed + item.start + item.letter ).join( '' ); } /** * Converts passed {@link engine.treeView.Element Element} to opening tag. * Depending on current configuration opening tag can be simple (``), contain type prefix (`` or * ``), contain priority information ( `` ). Element's attributes also * will be included (``). * * @private * @param {engine.treeView.Element} element * @returns {string} */ _stringifyElementOpen( element ) { const priority = this._stringifyElementPriority( element ); const type = this._stringifyElementType( element ); const name = [ type, element.name, priority ].filter( i=> i !== '' ).join( ':' ); const attributes = this._stringifyElementAttributes( element ); const parts = [ name, attributes ]; return `<${ parts.filter( i => i !== '' ).join( ' ' ) }>`; } /** * Converts passed {@link engine.treeView.Element Element} to closing tag. * Depending on current configuration opening tag can be simple (``) or contain type prefix (`` or * ``). * * @private * @param {engine.treeView.Element} element * @returns {string} */ _stringifyElementClose( element ) { const priority = this._stringifyElementPriority( element ); const type = this._stringifyElementType( element ); const name = [ type, element.name, priority ].filter( i=> i !== '' ).join( ':' ); return ``; } /** * Converts passed {@link engine.treeView.Element Element's} type to its string representation * Returns 'attribute' for {@link engine.treeView.AttributeElement AttributeElements} and * 'container' for {@link engine.treeView.ContainerElement ContainerElements}. Returns empty string when current * configuration is preventing showing elements' types. * * @private * @param {engine.treeView.Element} element * @returns {string} */ _stringifyElementType( element ) { if ( this.showType ) { if ( element instanceof AttributeElement ) { return 'attribute'; } if ( element instanceof ContainerElement ) { return 'container'; } } return ''; } /** * Converts passed {@link engine.treeView.Element Element} to its priority representation. * Priority string representation will be returned when passed element is an instance of * {@link engine.treeView.AttributeElement AttributeElement} and current configuration allow to show priority. * Otherwise returns empty string. * * @private * @param {engine.treeView.Element} element * @returns {string} */ _stringifyElementPriority( element ) { if ( this.showPriority && element instanceof AttributeElement ) { return element.priority; } return ''; } /** * Converts passed {@link engine.treeView.Element Element} attributes to their string representation. * If element has no attributes - empty string is returned. * * @private * @param {engine.treeView.Element} element * @returns {string} */ _stringifyElementAttributes( element ) { const attributes = []; // TODO: Maybe attributes should be put in alphabetical order, it might be easier to write expected string. for ( let attribute of element.getAttributeKeys() ) { attributes.push( `${ attribute }="${ element.getAttribute( attribute ) }"` ); } return attributes.join( ' ' ); } }