8
0

view.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. /* jshint latedef:false */
  7. import DocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
  8. import Element from '/ckeditor5/engine/treeview/element.js';
  9. import AttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
  10. import ContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
  11. import Text from '/ckeditor5/engine/treeview/text.js';
  12. const ELEMENT_RANGE_START_TOKEN = '[';
  13. const ELEMENT_RANGE_END_TOKEN = ']';
  14. const TEXT_RANGE_START_TOKEN = '{';
  15. const TEXT_RANGE_END_TOKEN = '}';
  16. export function getData( root, selection, options ) {
  17. const viewStringify = new ViewStringify( root, selection, options );
  18. return viewStringify.stringify();
  19. }
  20. /**
  21. * Private helper class used for converting view tree to string.
  22. *
  23. * @private
  24. */
  25. class ViewStringify {
  26. /**
  27. * Creates ViewStringify instance.
  28. * @param root
  29. * @param {engine.treeView.Selection} [selection=null] Selection which ranges should be also converted to string.
  30. * @param {Object} [options] Options object.
  31. * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed ( `<container:p>`
  32. * instead of `<p>` and `<attribute:b>` instead of `<b>`.
  33. * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed.
  34. */
  35. constructor( root, selection = null, options = {} ) {
  36. this.root = root;
  37. this.selection = selection;
  38. this.ranges = [];
  39. if ( this.selection ) {
  40. this.ranges = [ ...selection.getRanges() ];
  41. }
  42. this.showType = !!options.showType;
  43. this.showPriority = !!options.showPriority;
  44. }
  45. /**
  46. * Converts view to string.
  47. *
  48. * @returns {string} String representation of the view elements.
  49. */
  50. stringify() {
  51. let result = '';
  52. this._walkView( this.root, ( chunk ) => {
  53. result += chunk;
  54. } );
  55. return result;
  56. }
  57. /**
  58. * Executes simple walker that iterates over all elements in the view tree starting from root element.
  59. * Calls `callback` with parsed chunks of string data.
  60. *
  61. * @private
  62. * @param root
  63. * @param {Function} callback
  64. */
  65. _walkView( root, callback ) {
  66. const isElement = root instanceof Element;
  67. if ( isElement || root instanceof DocumentFragment ) {
  68. if ( isElement ) {
  69. callback( this._stringifyElementOpen( root ) );
  70. }
  71. let offset = 0;
  72. this._checkElementRanges( root, offset, callback );
  73. for ( let child of root.getChildren() ) {
  74. this._walkView( child, callback );
  75. offset++;
  76. this._checkElementRanges( root, offset, callback );
  77. }
  78. if ( isElement ) {
  79. callback( this._stringifyElementClose( root ) );
  80. }
  81. }
  82. if ( root instanceof Text ) {
  83. callback( this._checkTextRanges( root ) );
  84. }
  85. }
  86. /**
  87. * Checks if given {@link engine.treeView.Element Element} has {@link engine.treeView.Range#start range start} or
  88. * {@link engine.treeView.Range#start range end} placed at given offset. Calls `callback` function each time range
  89. * start or end is found.
  90. *
  91. * @private
  92. * @param {engine.treeView.Element} element
  93. * @param {Number} offset
  94. * @param {Function} callback
  95. */
  96. _checkElementRanges( element, offset, callback ) {
  97. for ( let range of this.ranges ) {
  98. if ( range.start.parent == element && range.start.offset === offset ) {
  99. callback( ELEMENT_RANGE_START_TOKEN );
  100. }
  101. if ( range.end.parent === element && range.end.offset === offset ) {
  102. callback( ELEMENT_RANGE_END_TOKEN );
  103. }
  104. }
  105. }
  106. /**
  107. * Checks if given {@link engine.treeView.Element Text node} has {@link engine.treeView.Range#start range start} or
  108. * {@link engine.treeView.Range#start range end} placed somewhere inside. Returns string representation of text
  109. * with range delimiters placed inside.
  110. *
  111. * @private
  112. * @param {engine.treeView.Text} textNode
  113. */
  114. _checkTextRanges( textNode ) {
  115. let result = textNode.data;
  116. let textOffset = 0;
  117. for ( let range of this.ranges ) {
  118. if ( range.start.parent == textNode ) {
  119. result = ViewStringify._insertToString( result, range.start.offset + textOffset, TEXT_RANGE_START_TOKEN );
  120. textOffset++;
  121. }
  122. if ( range.end.parent == textNode && !range.isCollapsed ) {
  123. result = ViewStringify._insertToString( result, range.end.offset + textOffset, TEXT_RANGE_END_TOKEN );
  124. textOffset++;
  125. }
  126. }
  127. return result;
  128. }
  129. /**
  130. * Converts passed {@link engine.treeView.Element Element} to opening tag.
  131. * Depending on current configuration opening tag can be simple (`<a>`), contain type prefix (`<container:p>` or
  132. * `<attribute:a>`), contain priority information ( `<attribute:a priority=20>` ). Element's attributes also
  133. * will be included (`<a href="http://ckeditor.com" name="foobar">`).
  134. *
  135. * @private
  136. * @param {engine.treeView.Element} element
  137. * @returns {string}
  138. */
  139. _stringifyElementOpen( element ) {
  140. const name = this._stringifyElementName( element );
  141. const priority = this._stringifyElementPriority( element );
  142. const attributes = this._stringifyElementAttributes( element );
  143. const parts = [ name, priority, attributes ];
  144. return `<${ parts.filter( i => i !== '' ).join( ' ' ) }>`;
  145. }
  146. /**
  147. * Converts passed {@link engine.treeView.Element Element} to closing tag.
  148. * Depending on current configuration opening tag can be simple (`</a>`) or contain type prefix (`</container:p>` or
  149. * `</attribute:a>`).
  150. *
  151. * @private
  152. * @param {engine.treeView.Element} element
  153. * @returns {string}
  154. */
  155. _stringifyElementClose( element ) {
  156. const name = this._stringifyElementName( element );
  157. return `</${ name }>`;
  158. }
  159. /**
  160. * Converts passed {@link engine.treeView.Element Element} its name representation.
  161. * Depending on current configuration name can be simple (`a`) or contain type prefix (`container:p` or
  162. * `attribute:a`).
  163. *
  164. * @private
  165. * @param {engine.treeView.Element} element
  166. * @returns {string}
  167. */
  168. _stringifyElementName( element ) {
  169. let name = element.name;
  170. if ( this.showType ) {
  171. if ( element instanceof AttributeElement ) {
  172. return 'attribute:' + name;
  173. }
  174. if ( element instanceof ContainerElement ) {
  175. return 'container:' + name;
  176. }
  177. }
  178. return name;
  179. }
  180. /**
  181. * Converts passed {@link engine.treeView.Element Element} to its priority representation.
  182. * Priority string representation will be returned when passed element is an instance of
  183. * {@link engine.treeView.AttributeElement AttributeElement} and current configuration allow to show priority.
  184. * Otherwise returns empty string.
  185. *
  186. * @private
  187. * @param {engine.treeView.Element} element
  188. * @returns {string}
  189. */
  190. _stringifyElementPriority( element ) {
  191. if ( this.showPriority && element instanceof AttributeElement ) {
  192. return `priority=${ element.priority }`;
  193. }
  194. return '';
  195. }
  196. /**
  197. * Converts passed {@link engine.treeView.Element Element} attributes to their string representation.
  198. * If element has no attributes - empty string is returned.
  199. *
  200. * @private
  201. * @param {engine.treeView.Element} element
  202. * @returns {string}
  203. */
  204. _stringifyElementAttributes( element ) {
  205. const attributes = [];
  206. // TODO: Maybe attributes should be put in alphabetical order, it might be easier to write expected string.
  207. for ( let attribute of element.getAttributeKeys() ) {
  208. attributes.push( `${ attribute }="${ element.getAttribute( attribute ) }"` );
  209. }
  210. return attributes.join( ' ' );
  211. }
  212. /**
  213. * Inserts given text at specified index in input text.
  214. * If index is outside input text boundaries - returns same, unmodified string.
  215. *
  216. * @private
  217. * @param {String} input Input string.
  218. * @param {Number} index Index where to insert inside input string.
  219. * @param {String} insert Text to insert.
  220. * @returns {string}
  221. */
  222. static _insertToString( input, index, insert ) {
  223. if ( index < 0 || index > input.length ) {
  224. return input;
  225. }
  226. return input.substr( 0, index ) + insert + input.substr( index );
  227. }
  228. }