model.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. import TreeWalker from '/ckeditor5/engine/treemodel/treewalker.js';
  7. import Range from '/ckeditor5/engine/treemodel/range.js';
  8. import Position from '/ckeditor5/engine/treemodel/position.js';
  9. import Text from '/ckeditor5/engine/treemodel/text.js';
  10. import Element from '/ckeditor5/engine/treemodel/element.js';
  11. /**
  12. * Writes the contents of the document to an HTML-like string.
  13. *
  14. * @param {engine.treeModel.Document} document
  15. * @param {String} rootName
  16. * @param {Object} [options]
  17. * @param {Boolean} [options.selection] Whether to write the selection.
  18. * @returns {String} The stringified data.
  19. */
  20. export function getData( document, rootName, options ) {
  21. const root = document.getRoot( rootName );
  22. const walker = new TreeWalker( {
  23. boundaries: Range.createFromElement( root )
  24. } );
  25. let ret = '';
  26. let lastPosition = Position.createFromParentAndOffset( root, 0 );
  27. const selection = document.selection;
  28. options = options || {};
  29. for ( let value of walker ) {
  30. if ( options.selection ) {
  31. ret += writeSelection( value.previousPosition, selection );
  32. }
  33. ret += writeItem( value, selection, options );
  34. lastPosition = value.nextPosition;
  35. }
  36. if ( options.selection ) {
  37. ret += writeSelection( lastPosition, selection );
  38. }
  39. return ret;
  40. }
  41. /**
  42. * Sets the contents of the model and the selection in it.
  43. *
  44. * @param {engine.treeModel.Document} document
  45. * @param {String} rootName
  46. * @param {String} data
  47. */
  48. export function setData( document, rootName, data ) {
  49. let appendTo = document.getRoot( rootName );
  50. const path = [];
  51. let selectionStart, selectionEnd, selectionAttributes, textAttributes;
  52. const handlers = {
  53. text( token ) {
  54. appendTo.appendChildren( new Text( token.text, textAttributes ) );
  55. },
  56. textStart( token ) {
  57. textAttributes = token.attributes;
  58. path.push( '$text' );
  59. },
  60. textEnd() {
  61. if ( path.pop() != '$text' ) {
  62. throw new Error( 'Parse error - unexpected closing tag.' );
  63. }
  64. textAttributes = null;
  65. },
  66. openingTag( token ) {
  67. let el = new Element( token.name, token.attributes );
  68. appendTo.appendChildren( el );
  69. appendTo = el;
  70. path.push( token.name );
  71. },
  72. closingTag( token ) {
  73. if ( path.pop() != token.name ) {
  74. throw new Error( 'Parse error - unexpected closing tag.' );
  75. }
  76. appendTo = appendTo.parent;
  77. },
  78. collapsedSelection( token ) {
  79. document.selection.collapse( appendTo, 'END' );
  80. document.selection.setAttributesTo( token.attributes );
  81. },
  82. selectionStart( token ) {
  83. selectionStart = Position.createFromParentAndOffset( appendTo, appendTo.getChildCount() );
  84. selectionAttributes = token.attributes;
  85. },
  86. selectionEnd() {
  87. if ( !selectionStart ) {
  88. throw new Error( 'Parse error - missing selection start' );
  89. }
  90. selectionEnd = Position.createFromParentAndOffset( appendTo, appendTo.getChildCount() );
  91. document.selection.setRanges(
  92. [ new Range( selectionStart, selectionEnd ) ],
  93. selectionAttributes.backward
  94. );
  95. delete selectionAttributes.backward;
  96. document.selection.setAttributesTo( selectionAttributes );
  97. }
  98. };
  99. for ( let token of tokenize( data ) ) {
  100. handlers[ token.type ]( token );
  101. }
  102. if ( path.length ) {
  103. throw new Error( 'Parse error - missing closing tags: ' + path.join( ', ' ) + '.' );
  104. }
  105. if ( selectionStart && !selectionEnd ) {
  106. throw new Error( 'Parse error - missing selection end.' );
  107. }
  108. }
  109. // -- getData helpers ---------------------------------------------------------
  110. function writeItem( walkerValue, selection, options ) {
  111. const type = walkerValue.type;
  112. const item = walkerValue.item;
  113. if ( type == 'ELEMENT_START' ) {
  114. let attrs = writeAttributes( item.getAttributes() );
  115. if ( attrs ) {
  116. return `<${ item.name } ${ attrs }>`;
  117. }
  118. return `<${ item.name }>`;
  119. }
  120. if ( type == 'ELEMENT_END' ) {
  121. return `</${ item.name }>`;
  122. }
  123. return writeText( walkerValue, selection, options );
  124. }
  125. function writeText( walkerValue, selection, options ) {
  126. const item = walkerValue.item;
  127. const attrs = writeAttributes( item.getAttributes() );
  128. let text = Array.from( item.text );
  129. if ( options.selection ) {
  130. const startIndex = walkerValue.previousPosition.offset + 1;
  131. const endIndex = walkerValue.nextPosition.offset - 1;
  132. let index = startIndex;
  133. while ( index <= endIndex ) {
  134. // Add the selection marker without changing any indexes, so if second marker must be added
  135. // in the same loop it does not blow up.
  136. text[ index - startIndex ] +=
  137. writeSelection( Position.createFromParentAndOffset( item.commonParent, index ), selection );
  138. index++;
  139. }
  140. }
  141. text = text.join( '' );
  142. if ( attrs ) {
  143. return `<$text ${ attrs }>${ text }</$text>`;
  144. }
  145. return text;
  146. }
  147. function writeAttributes( attrs ) {
  148. attrs = Array.from( attrs );
  149. return attrs.map( attr => attr[ 0 ] + '=' + JSON.stringify( attr[ 1 ] ) ).sort().join( ' ' );
  150. }
  151. function writeSelection( currentPosition, selection ) {
  152. // TODO: This function obviously handles only the first range.
  153. const range = selection.getFirstRange();
  154. // Handle end of the selection.
  155. if ( !selection.isCollapsed && range.end.compareWith( currentPosition ) == 'SAME' ) {
  156. return '</selection>';
  157. }
  158. // Handle no match.
  159. if ( range.start.compareWith( currentPosition ) != 'SAME' ) {
  160. return '';
  161. }
  162. // Handle beginning of the selection.
  163. let ret = '<selection';
  164. const attrs = writeAttributes( selection.getAttributes() );
  165. // TODO: Once we'll support multiple ranges this will need to check which range it is.
  166. if ( selection.isBackward ) {
  167. ret += ' backward';
  168. }
  169. if ( attrs ) {
  170. ret += ' ' + attrs;
  171. }
  172. ret += ( selection.isCollapsed ? ' />' : '>' );
  173. return ret;
  174. }
  175. // -- setData helpers ---------------------------------------------------------
  176. const patterns = {
  177. selection: /^<(\/?selection)( [^>]*)?>/,
  178. tag: /^<([^>]+)>/,
  179. text: /^[^<]+/
  180. };
  181. const handlers = {
  182. selection( match ) {
  183. const tagName = match[ 1 ];
  184. const tagExtension = match[ 2 ] || '';
  185. if ( tagName[ 0 ] == '/' ) {
  186. return {
  187. type: 'selectionEnd'
  188. };
  189. }
  190. if ( tagExtension.endsWith( ' /' ) ) {
  191. return {
  192. type: 'collapsedSelection',
  193. attributes: parseAttributes( tagExtension.slice( 1, -2 ) )
  194. };
  195. }
  196. return {
  197. type: 'selectionStart',
  198. attributes: parseAttributes( tagExtension.slice( 1 ) )
  199. };
  200. },
  201. tag( match ) {
  202. const tagContents = match[ 1 ].split( /\s+/ );
  203. const tagName = tagContents.shift();
  204. const attrs = tagContents.join( ' ' );
  205. if ( tagName == '/$text' ) {
  206. return {
  207. type: 'textEnd'
  208. };
  209. }
  210. if ( tagName == '$text' ) {
  211. return {
  212. type: 'textStart',
  213. attributes: parseAttributes( attrs )
  214. };
  215. }
  216. if ( tagName[ 0 ] == '/' ) {
  217. return {
  218. type: 'closingTag',
  219. name: tagName.slice( 1 )
  220. };
  221. }
  222. return {
  223. type: 'openingTag',
  224. name: tagName,
  225. attributes: parseAttributes( attrs )
  226. };
  227. },
  228. text( match ) {
  229. return {
  230. type: 'text',
  231. text: match[ 0 ]
  232. };
  233. }
  234. };
  235. function *tokenize( data ) {
  236. while ( data ) {
  237. const consumed = consumeNextToken( data );
  238. data = consumed.data;
  239. yield consumed.token;
  240. }
  241. }
  242. function consumeNextToken( data ) {
  243. let match;
  244. for ( let patternName in patterns ) {
  245. match = data.match( patterns[ patternName ] );
  246. if ( match ) {
  247. data = data.slice( match[ 0 ].length );
  248. return {
  249. token: handlers[ patternName ]( match ),
  250. data
  251. };
  252. }
  253. }
  254. throw new Error( 'Parse error - unpexpected token: ' + data + '.' );
  255. }
  256. function parseAttributes( attrsString ) {
  257. attrsString = attrsString.trim();
  258. if ( !attrsString ) {
  259. return {};
  260. }
  261. const pattern = /(?:backward|(\w+)=("[^"]+"|[^\s]+))\s*/;
  262. const attrs = {};
  263. while ( attrsString ) {
  264. let match = attrsString.match( pattern );
  265. if ( !match ) {
  266. throw new Error( 'Parse error - unexpected token: ' + attrsString + '.' );
  267. }
  268. if ( match[ 0 ].trim() == 'backward' ) {
  269. attrs.backward = true;
  270. } else {
  271. attrs[ match[ 1 ] ] = JSON.parse( match[ 2 ] );
  272. }
  273. attrsString = attrsString.slice( match[ 0 ].length );
  274. }
  275. return attrs;
  276. }