model.js 11 KB

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