view.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  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. /**
  17. * Converts view elements to its string representation.
  18. * Root element can be provided as {@link engine.treeView.Element Element} or
  19. * {@link engine.treeView.DocumentFragment DocumentFragment}.
  20. *
  21. * const text = new Text( 'foobar' );
  22. * const b = new Element( 'b', { name: 'test' }, text );
  23. * const p = new Element( 'p', { style: 'color:red;' }, b );
  24. *
  25. * getData( p ); // <p style="color:red;"><b name="test">foobar</b></p>
  26. *
  27. * Additionally {@link engine.treeView.Selection Selection}
  28. * instance can be provided, then ranges from that selection will be converted too. If range position is placed inside
  29. * element node `[` and `]` will be used there.
  30. *
  31. * const text = new Text( 'foobar' );
  32. * const b = new Element( 'b', null, text );
  33. * const p = new Element( 'p', null, b );
  34. * const selection = new Selection();
  35. * selection.addRange( Range.createFromParentsAndOffsets( p, 0, p, 1 ) );
  36. *
  37. * getData( p, selection ); // <p>[<b>foobar</b>]</p>
  38. *
  39. * If range is placed inside text node `{` and `}` will be used there.
  40. *
  41. * const text = new Text( 'foobar' );
  42. * const b = new Element( 'b', null, text );
  43. * const p = new Element( 'p', null, b );
  44. * const selection = new Selection();
  45. * selection.addRange( Range.createFromParentsAndOffsets( text, 1, text, 5 ) );
  46. *
  47. * getData( p, selection ); // <p><b>f{ooba}r</b></p>
  48. *
  49. * Additional options object can be provided.
  50. * If `options.showType` property is set to `true` element types will be
  51. * presented for {@link engine.treeView.AttributeElement AttributeElements} and {@link engine.treeView.ContainerElement
  52. * ContainerElements}.
  53. *
  54. * const attribute = new AttributeElement( 'b' );
  55. * const container = new ContainerElement( 'p' );
  56. * getData( attribute, null, { showType: true } ); // <attribute:b></attribute:b>
  57. * getData( container, null, { showType: true } ); // <container:p></container:p>
  58. *
  59. * if `options.showPriority` property is set to `true`, priority will be displayed for all
  60. * {@link engine.treeView.AttributeElement AttributeElements}.
  61. *
  62. * const attribute = new AttributeElement( 'b' );
  63. * attribute.priority = 20;
  64. * getData( attribute, null, { showPriority: true } ); // <b priority=20></b>
  65. *
  66. * @param {engine.treeView.Element|engine.treeView.DocumentFragment} root
  67. * @param {engine.treeView.Selection} [selection]
  68. * @param {Object} [options]
  69. * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed ( `<container:p>`
  70. * instead of `<p>` and `<attribute:b>` instead of `<b>`.
  71. * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed.
  72. * @returns {String}
  73. */
  74. export function getData( root, selection, options ) {
  75. const viewStringify = new ViewStringify( root, selection, options );
  76. return viewStringify.stringify();
  77. }
  78. /**
  79. * Private helper class used for converting view tree to string.
  80. *
  81. * @private
  82. */
  83. class ViewStringify {
  84. /**
  85. * Creates ViewStringify instance.
  86. * @param root
  87. * @param {engine.treeView.Selection} [selection=null] Selection which ranges should be also converted to string.
  88. * @param {Object} [options] Options object.
  89. * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed ( `<container:p>`
  90. * instead of `<p>` and `<attribute:b>` instead of `<b>`.
  91. * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed.
  92. */
  93. constructor( root, selection = null, options = {} ) {
  94. this.root = root;
  95. this.selection = selection;
  96. this.ranges = [];
  97. if ( this.selection ) {
  98. this.ranges = [ ...selection.getRanges() ];
  99. }
  100. this.showType = !!options.showType;
  101. this.showPriority = !!options.showPriority;
  102. }
  103. /**
  104. * Converts view to string.
  105. *
  106. * @returns {string} String representation of the view elements.
  107. */
  108. stringify() {
  109. let result = '';
  110. this._walkView( this.root, ( chunk ) => {
  111. result += chunk;
  112. } );
  113. return result;
  114. }
  115. /**
  116. * Executes simple walker that iterates over all elements in the view tree starting from root element.
  117. * Calls `callback` with parsed chunks of string data.
  118. *
  119. * @private
  120. * @param {engine.treeView.DocumentFragment|engine.treeView.Element|engine.treeView.Text} root
  121. * @param {Function} callback
  122. */
  123. _walkView( root, callback ) {
  124. const isElement = root instanceof Element;
  125. if ( isElement || root instanceof DocumentFragment ) {
  126. if ( isElement ) {
  127. callback( this._stringifyElementOpen( root ) );
  128. }
  129. let offset = 0;
  130. callback( this._stringifyElementRanges( root, offset ) );
  131. for ( let child of root.getChildren() ) {
  132. this._walkView( child, callback );
  133. offset++;
  134. callback( this._stringifyElementRanges( root, offset ) );
  135. }
  136. if ( isElement ) {
  137. callback( this._stringifyElementClose( root ) );
  138. }
  139. }
  140. if ( root instanceof Text ) {
  141. callback( this._stringifyTextRanges( root ) );
  142. }
  143. }
  144. /**
  145. * Checks if given {@link engine.treeView.Element Element} has {@link engine.treeView.Range#start range start} or
  146. * {@link engine.treeView.Range#start range end} placed at given offset and returns its string representation.
  147. *
  148. * @private
  149. * @param {engine.treeView.Element} element
  150. * @param {Number} offset
  151. */
  152. _stringifyElementRanges( element, offset ) {
  153. let start = '';
  154. let end = '';
  155. let collapsed = '';
  156. for ( let range of this.ranges ) {
  157. if ( range.start.parent == element && range.start.offset === offset ) {
  158. if ( range.isCollapsed ) {
  159. collapsed += ELEMENT_RANGE_START_TOKEN + ELEMENT_RANGE_END_TOKEN;
  160. } else {
  161. start += ELEMENT_RANGE_START_TOKEN;
  162. }
  163. }
  164. if ( range.end.parent === element && range.end.offset === offset && !range.isCollapsed ) {
  165. end += ELEMENT_RANGE_END_TOKEN;
  166. }
  167. }
  168. return end + collapsed + start;
  169. }
  170. /**
  171. * Checks if given {@link engine.treeView.Element Text node} has {@link engine.treeView.Range#start range start} or
  172. * {@link engine.treeView.Range#start range end} placed somewhere inside. Returns string representation of text
  173. * with range delimiters placed inside.
  174. *
  175. * @private
  176. * @param {engine.treeView.Text} node
  177. */
  178. _stringifyTextRanges( node ) {
  179. const length = node.data.length;
  180. let result = node.data.split( '' );
  181. // Add one more element for ranges ending after last character in text.
  182. result[ length ] = '';
  183. // Represent each letter as object with information about opening/closing ranges at each offset.
  184. result = result.map( ( letter ) => {
  185. return {
  186. letter: letter,
  187. start: '',
  188. end: '',
  189. collapsed: ''
  190. };
  191. } );
  192. for ( let range of this.ranges ) {
  193. const start = range.start;
  194. const end = range.end;
  195. if ( start.parent == node && start.offset >= 0 && start.offset <= length ) {
  196. if ( range.isCollapsed ) {
  197. result[ end.offset ].collapsed += TEXT_RANGE_START_TOKEN + TEXT_RANGE_END_TOKEN;
  198. } else {
  199. result[ start.offset ].start += TEXT_RANGE_START_TOKEN;
  200. }
  201. }
  202. if ( end.parent == node && end.offset >= 0 && end.offset <= length && !range.isCollapsed ) {
  203. result[ end.offset ].end += TEXT_RANGE_END_TOKEN;
  204. }
  205. }
  206. return result.map( item => item.end + item.collapsed + item.start + item.letter ).join( '' );
  207. }
  208. /**
  209. * Converts passed {@link engine.treeView.Element Element} to opening tag.
  210. * Depending on current configuration opening tag can be simple (`<a>`), contain type prefix (`<container:p>` or
  211. * `<attribute:a>`), contain priority information ( `<attribute:a priority=20>` ). Element's attributes also
  212. * will be included (`<a href="http://ckeditor.com" name="foobar">`).
  213. *
  214. * @private
  215. * @param {engine.treeView.Element} element
  216. * @returns {string}
  217. */
  218. _stringifyElementOpen( element ) {
  219. const priority = this._stringifyElementPriority( element );
  220. const type = this._stringifyElementType( element );
  221. const name = [ type, element.name, priority ].filter( i=> i !== '' ).join( ':' );
  222. const attributes = this._stringifyElementAttributes( element );
  223. const parts = [ name, attributes ];
  224. return `<${ parts.filter( i => i !== '' ).join( ' ' ) }>`;
  225. }
  226. /**
  227. * Converts passed {@link engine.treeView.Element Element} to closing tag.
  228. * Depending on current configuration opening tag can be simple (`</a>`) or contain type prefix (`</container:p>` or
  229. * `</attribute:a>`).
  230. *
  231. * @private
  232. * @param {engine.treeView.Element} element
  233. * @returns {string}
  234. */
  235. _stringifyElementClose( element ) {
  236. const priority = this._stringifyElementPriority( element );
  237. const type = this._stringifyElementType( element );
  238. const name = [ type, element.name, priority ].filter( i=> i !== '' ).join( ':' );
  239. return `</${ name }>`;
  240. }
  241. /**
  242. * Converts passed {@link engine.treeView.Element Element's} type to its string representation
  243. * Returns 'attribute' for {@link engine.treeView.AttributeElement AttributeElements} and
  244. * 'container' for {@link engine.treeView.ContainerElement ContainerElements}. Returns empty string when current
  245. * configuration is preventing showing elements' types.
  246. *
  247. * @private
  248. * @param {engine.treeView.Element} element
  249. * @returns {string}
  250. */
  251. _stringifyElementType( element ) {
  252. if ( this.showType ) {
  253. if ( element instanceof AttributeElement ) {
  254. return 'attribute';
  255. }
  256. if ( element instanceof ContainerElement ) {
  257. return 'container';
  258. }
  259. }
  260. return '';
  261. }
  262. /**
  263. * Converts passed {@link engine.treeView.Element Element} to its priority representation.
  264. * Priority string representation will be returned when passed element is an instance of
  265. * {@link engine.treeView.AttributeElement AttributeElement} and current configuration allow to show priority.
  266. * Otherwise returns empty string.
  267. *
  268. * @private
  269. * @param {engine.treeView.Element} element
  270. * @returns {string}
  271. */
  272. _stringifyElementPriority( element ) {
  273. if ( this.showPriority && element instanceof AttributeElement ) {
  274. return element.priority;
  275. }
  276. return '';
  277. }
  278. /**
  279. * Converts passed {@link engine.treeView.Element Element} attributes to their string representation.
  280. * If element has no attributes - empty string is returned.
  281. *
  282. * @private
  283. * @param {engine.treeView.Element} element
  284. * @returns {string}
  285. */
  286. _stringifyElementAttributes( element ) {
  287. const attributes = [];
  288. // TODO: Maybe attributes should be put in alphabetical order, it might be easier to write expected string.
  289. for ( let attribute of element.getAttributeKeys() ) {
  290. attributes.push( `${ attribute }="${ element.getAttribute( attribute ) }"` );
  291. }
  292. return attributes.join( ' ' );
  293. }
  294. }