8
0

model.js 9.1 KB

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