8
0

model.js 7.9 KB

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