Index: trunk/extensions/VisualEditor/modules/ve/dm/ve.dm.js |
— | — | @@ -3,549 +3,4 @@ |
4 | 4 | * |
5 | 5 | * All classes and functions will be attached to this object to keep the global namespace clean. |
6 | 6 | */ |
7 | | -ve.dm = { |
8 | | - |
9 | | - /* Static Members */ |
10 | | - |
11 | | - /** |
12 | | - * Mapping of symbolic names and node model constructors. |
13 | | - */ |
14 | | - 'nodeModels': {}, |
15 | | - /** |
16 | | - * Mapping of symbolic names and nesting rules. |
17 | | - * |
18 | | - * Each rule is an object with the follwing properties: |
19 | | - * parents and children properties may contain one of two possible values: |
20 | | - * {Array} List symbolic names of allowed element types (if empty, none will be allowed) |
21 | | - * {Null} Any element type is allowed (as long as the other element also allows it) |
22 | | - * |
23 | | - * @example Paragraph rules |
24 | | - * { |
25 | | - * 'parents': null, |
26 | | - * 'children': [] |
27 | | - * } |
28 | | - * @example List rules |
29 | | - * { |
30 | | - * 'parents': null, |
31 | | - * 'children': ['listItem'] |
32 | | - * } |
33 | | - * @example ListItem rules |
34 | | - * { |
35 | | - * 'parents': ['list'], |
36 | | - * 'children': null |
37 | | - * } |
38 | | - * @example TableCell rules |
39 | | - * { |
40 | | - * 'parents': ['tableRow'], |
41 | | - * 'children': null |
42 | | - * } |
43 | | - */ |
44 | | - 'nodeRules': { |
45 | | - 'document': { |
46 | | - 'parents': null, |
47 | | - 'children': null |
48 | | - } |
49 | | - }, |
50 | | - |
51 | | - /* Static Methods */ |
52 | | - |
53 | | - /* |
54 | | - * Create child nodes from an array of data. |
55 | | - * |
56 | | - * These child nodes are used for the model tree, which is a space partitioning data structure |
57 | | - * in which each node contains the length of itself (1 for opening, 1 for closing) and the |
58 | | - * lengths of it's child nodes. |
59 | | - */ |
60 | | - 'createNodesFromData': function( data ) { |
61 | | - var currentNode = new ve.dm.BranchNode(); |
62 | | - for ( var i = 0, length = data.length; i < length; i++ ) { |
63 | | - if ( data[i].type !== undefined ) { |
64 | | - // It's an element, figure out it's type |
65 | | - var element = data[i], |
66 | | - type = element.type, |
67 | | - open = type.charAt( 0 ) !== '/'; |
68 | | - // Trim the "/" off the beginning of closing tag types |
69 | | - if ( !open ) { |
70 | | - type = type.substr( 1 ); |
71 | | - } |
72 | | - if ( open ) { |
73 | | - // Validate the element type |
74 | | - if ( !( type in ve.dm.DocumentNode.nodeModels ) ) { |
75 | | - throw 'Unsuported element error. No class registered for element type: ' + |
76 | | - type; |
77 | | - } |
78 | | - // Create a model node for the element |
79 | | - var newNode = new ve.dm.DocumentNode.nodeModels[element.type]( element, 0 ); |
80 | | - // Add the new model node as a child |
81 | | - currentNode.push( newNode ); |
82 | | - // Descend into the new model node |
83 | | - currentNode = newNode; |
84 | | - } else { |
85 | | - // Return to the parent node |
86 | | - currentNode = currentNode.getParent(); |
87 | | - if ( currentNode === null ) { |
88 | | - throw 'createNodesFromData() received unbalanced data: found closing ' + |
89 | | - 'without matching opening at index ' + i; |
90 | | - } |
91 | | - } |
92 | | - } else { |
93 | | - // It's content, let's start tracking the length |
94 | | - var start = i; |
95 | | - // Move forward to the next object, tracking the length as we go |
96 | | - while ( data[i].type === undefined && i < length ) { |
97 | | - i++; |
98 | | - } |
99 | | - // Now we know how long the current node is |
100 | | - currentNode.setContentLength( i - start ); |
101 | | - // The while loop left us 1 element to far |
102 | | - i--; |
103 | | - } |
104 | | - } |
105 | | - return currentNode.getChildren().slice( 0 ); |
106 | | - }, |
107 | | - /** |
108 | | - * Creates a document model from a plain object. |
109 | | - * |
110 | | - * @static |
111 | | - * @method |
112 | | - * @param {Object} obj Object to create new document model from |
113 | | - * @returns {ve.dm.DocumentNode} Document model created from obj |
114 | | - */ |
115 | | - 'newFromPlainObject': function( obj ) { |
116 | | - if ( obj.type === 'document' ) { |
117 | | - var data = [], |
118 | | - attributes = ve.isPlainObject( obj.attributes ) ? |
119 | | - ve.copyObject( obj.attributes ) : {}; |
120 | | - for ( var i = 0; i < obj.children.length; i++ ) { |
121 | | - data = data.concat( |
122 | | - ve.dm.DocumentNode.flattenPlainObjectElementNode( obj.children[i] ) |
123 | | - ); |
124 | | - } |
125 | | - return new ve.dm.DocumentNode( data, attributes ); |
126 | | - } |
127 | | - throw 'Invalid object error. Object is not a valid document object.'; |
128 | | - }, |
129 | | - /** |
130 | | - * Generates a hash of an annotation object based on it's name and data. |
131 | | - * |
132 | | - * @static |
133 | | - * @method |
134 | | - * @param {Object} annotation Annotation object to generate hash for |
135 | | - * @returns {String} Hash of annotation |
136 | | - */ |
137 | | - 'getHash': ( window.JSON && typeof JSON.stringify === 'function' ) ? |
138 | | - JSON.stringify : ve.dm.JsonSerializer.stringify, |
139 | | - /** |
140 | | - * Gets the index of the first instance of a given annotation. |
141 | | - * |
142 | | - * This method differs from ve.inArray because it compares hashes instead of references. |
143 | | - * |
144 | | - * @static |
145 | | - * @method |
146 | | - * @param {Array} annotations Annotations to search through |
147 | | - * @param {Object} annotation Annotation to search for |
148 | | - * @param {Boolean} typeOnly Whether to only consider the type |
149 | | - * @returns {Integer} Index of annotation in annotations, or -1 if annotation was not found |
150 | | - */ |
151 | | - 'getIndexOfAnnotation': function( annotations, annotation, typeOnly ) { |
152 | | - if ( annotation === undefined || annotation.type === undefined ) { |
153 | | - throw 'Invalid annotation error. Can not find non-annotation data in character.'; |
154 | | - } |
155 | | - if ( ve.isArray( annotations ) ) { |
156 | | - // Find the index of a comparable annotation (checking for same value, not reference) |
157 | | - for ( var i = 0; i < annotations.length; i++ ) { |
158 | | - // Skip over character data - used when this is called on a content data item |
159 | | - if ( typeof annotations[i] === 'string' ) { |
160 | | - continue; |
161 | | - } |
162 | | - if ( |
163 | | - ( |
164 | | - typeOnly && |
165 | | - annotations[i].type === annotation.type |
166 | | - ) || |
167 | | - ( |
168 | | - !typeOnly && |
169 | | - annotations[i].hash === ( |
170 | | - annotation.hash || ve.dm.DocumentNode.getHash( annotation ) |
171 | | - ) |
172 | | - ) |
173 | | - ) { |
174 | | - return i; |
175 | | - } |
176 | | - } |
177 | | - } |
178 | | - return -1; |
179 | | - }, |
180 | | - /** |
181 | | - * Gets a list of indexes of annotations that match a regular expression. |
182 | | - * |
183 | | - * @static |
184 | | - * @method |
185 | | - * @param {Array} annotations Annotations to search through |
186 | | - * @param {RegExp} pattern Regular expression pattern to match with |
187 | | - * @returns {Integer[]} List of indexes in annotations that match |
188 | | - */ |
189 | | - 'getMatchingAnnotations': function( annotations, pattern ) { |
190 | | - if ( !( pattern instanceof RegExp ) ) { |
191 | | - throw 'Invalid annotation error. Can not find non-annotation data in character.'; |
192 | | - } |
193 | | - var matches = []; |
194 | | - if ( ve.isArray( annotations ) ) { |
195 | | - // Find the index of a comparable annotation (checking for same value, not reference) |
196 | | - for ( var i = 0; i < annotations.length; i++ ) { |
197 | | - // Skip over character data - used when this is called on a content data item |
198 | | - if ( typeof annotations[i] === 'string' ) { |
199 | | - continue; |
200 | | - } |
201 | | - if ( pattern.test( annotations[i].type ) ) { |
202 | | - matches.push( annotations[i] ); |
203 | | - } |
204 | | - } |
205 | | - } |
206 | | - return matches; |
207 | | - }, |
208 | | - /** |
209 | | - * Sorts annotations of a character. |
210 | | - * |
211 | | - * This method modifies data in place. The string portion of the annotation character will always |
212 | | - * remain at the beginning. |
213 | | - * |
214 | | - * @static |
215 | | - * @method |
216 | | - * @param {Array} character Annotated character to be sorted |
217 | | - */ |
218 | | - 'sortCharacterAnnotations': function( character ) { |
219 | | - if ( !ve.isArray( character ) ) { |
220 | | - return; |
221 | | - } |
222 | | - character.sort( function( a, b ) { |
223 | | - var aHash = a.hash || ve.dm.DocumentNode.getHash( a ), |
224 | | - bHash = b.hash || ve.dm.DocumentNode.getHash( b ); |
225 | | - return typeof a === 'string' ? -1 : |
226 | | - ( typeof b === 'string' ? 1 : ( aHash == bHash ? 0 : ( aHash < bHash ? -1 : 1 ) ) ); |
227 | | - } ); |
228 | | - }, |
229 | | - /** |
230 | | - * Adds annotation hashes to content data. |
231 | | - * |
232 | | - * This method modifies data in place. |
233 | | - * |
234 | | - * @method |
235 | | - * @param {Array} data Data to add annotation hashes to |
236 | | - */ |
237 | | - 'addAnnotationHashesToData': function( data ) { |
238 | | - for ( var i = 0; i < data.length; i++ ) { |
239 | | - if ( ve.isArray( data[i] ) ) { |
240 | | - for ( var j = 1; j < data.length; j++ ) { |
241 | | - if ( data[i][j].hash === undefined ) { |
242 | | - data[i][j].hash = ve.dm.DocumentNode.getHash( data[i][j] ); |
243 | | - } |
244 | | - } |
245 | | - } |
246 | | - } |
247 | | - }, |
248 | | - /** |
249 | | - * Applies annotations to content data. |
250 | | - * |
251 | | - * This method modifies data in place. |
252 | | - * |
253 | | - * @method |
254 | | - * @param {Array} data Data to remove annotations from |
255 | | - * @param {Array} annotations Annotations to apply |
256 | | - */ |
257 | | - 'addAnnotationsToData': function( data, annotations ) { |
258 | | - if ( annotations && annotations.length ) { |
259 | | - for ( var i = 0; i < data.length; i++ ) { |
260 | | - if ( ve.isArray( data[i] ) ) { |
261 | | - data[i] = [data[i]]; |
262 | | - } |
263 | | - data[i] = [data[i]].concat( annotations ); |
264 | | - } |
265 | | - } |
266 | | - }, |
267 | | - /** |
268 | | - * Removes annotations from content data. |
269 | | - * |
270 | | - * This method modifies data in place. |
271 | | - * |
272 | | - * @method |
273 | | - * @param {Array} data Data to remove annotations from |
274 | | - * @param {Array} [annotations] Annotations to remove (all will be removed if undefined) |
275 | | - */ |
276 | | - 'removeAnnotationsFromData': function( data, annotations ) { |
277 | | - for ( var i = 0; i < data.length; i++ ) { |
278 | | - if ( ve.isArray( data[i] ) ) { |
279 | | - data[i] = data[i][0]; |
280 | | - } |
281 | | - } |
282 | | - }, |
283 | | - /** |
284 | | - * Creates an ve.ContentModel object from a plain content object. |
285 | | - * |
286 | | - * A plain content object contains plain text and a series of annotations to be applied to ranges of |
287 | | - * the text. |
288 | | - * |
289 | | - * @example |
290 | | - * { |
291 | | - * 'text': '1234', |
292 | | - * 'annotations': [ |
293 | | - * // Makes "23" bold |
294 | | - * { |
295 | | - * 'type': 'bold', |
296 | | - * 'range': { |
297 | | - * 'start': 1, |
298 | | - * 'end': 3 |
299 | | - * } |
300 | | - * } |
301 | | - * ] |
302 | | - * } |
303 | | - * |
304 | | - * @static |
305 | | - * @method |
306 | | - * @param {Object} obj Plain content object, containing a "text" property and optionally |
307 | | - * an "annotations" property, the latter of which being an array of annotation objects including |
308 | | - * range information |
309 | | - * @returns {Array} |
310 | | - */ |
311 | | - 'flattenPlainObjectContentNode': function( obj ) { |
312 | | - if ( !ve.isPlainObject( obj ) ) { |
313 | | - // Use empty content |
314 | | - return []; |
315 | | - } else { |
316 | | - // Convert string to array of characters |
317 | | - var data = obj.text.split(''); |
318 | | - // Render annotations |
319 | | - if ( ve.isArray( obj.annotations ) ) { |
320 | | - for ( var i = 0, length = obj.annotations.length; i < length; i++ ) { |
321 | | - var src = obj.annotations[i]; |
322 | | - // Build simplified annotation object |
323 | | - var dst = { 'type': src.type }; |
324 | | - if ( 'data' in src ) { |
325 | | - dst.data = ve.copyObject( src.data ); |
326 | | - } |
327 | | - // Add a hash to the annotation for faster comparison |
328 | | - dst.hash = ve.dm.DocumentNode.getHash( dst ); |
329 | | - // Apply annotation to range |
330 | | - if ( src.range.start < 0 ) { |
331 | | - // TODO: The start can not be lower than 0! Throw error? |
332 | | - // Clamp start value |
333 | | - src.range.start = 0; |
334 | | - } |
335 | | - if ( src.range.end > data.length ) { |
336 | | - // TODO: The end can not be higher than the length! Throw error? |
337 | | - // Clamp end value |
338 | | - src.range.end = data.length; |
339 | | - } |
340 | | - for ( var j = src.range.start; j < src.range.end; j++ ) { |
341 | | - // Auto-convert to array |
342 | | - if ( typeof data[j] === 'string' ) { |
343 | | - data[j] = [data[j]]; |
344 | | - } |
345 | | - // Append |
346 | | - data[j].push( dst ); |
347 | | - } |
348 | | - } |
349 | | - } |
350 | | - return data; |
351 | | - } |
352 | | - }, |
353 | | - /** |
354 | | - * Flatten a plain node object into a data array, recursively. |
355 | | - * |
356 | | - * TODO: where do we document this whole structure - aka "WikiDom"? |
357 | | - * |
358 | | - * @static |
359 | | - * @method |
360 | | - * @param {Object} obj Plain node object to flatten |
361 | | - * @returns {Array} Flattened version of obj |
362 | | - */ |
363 | | - 'flattenPlainObjectElementNode': function( obj ) { |
364 | | - var i, |
365 | | - data = [], |
366 | | - element = { 'type': obj.type }; |
367 | | - if ( ve.isPlainObject( obj.attributes ) ) { |
368 | | - element.attributes = ve.copyObject( obj.attributes ); |
369 | | - } |
370 | | - // Open element |
371 | | - data.push( element ); |
372 | | - if ( ve.isPlainObject( obj.content ) ) { |
373 | | - // Add content |
374 | | - data = data.concat( ve.dm.DocumentNode.flattenPlainObjectContentNode( obj.content ) ); |
375 | | - } else if ( ve.isArray( obj.children ) ) { |
376 | | - // Add children - only do this if there is no content property |
377 | | - for ( i = 0; i < obj.children.length; i++ ) { |
378 | | - // TODO: Figure out if all this concatenating is inefficient. I think it is |
379 | | - data = data.concat( ve.dm.DocumentNode.flattenPlainObjectElementNode( obj.children[i] ) ); |
380 | | - } |
381 | | - } |
382 | | - // Close element - TODO: Do we need attributes here or not? |
383 | | - data.push( { 'type': '/' + obj.type } ); |
384 | | - return data; |
385 | | - }, |
386 | | - /** |
387 | | - * Get a plain object representation of content data. |
388 | | - * |
389 | | - * @method |
390 | | - * @returns {Object} Plain object representation |
391 | | - */ |
392 | | - 'getExpandedContentData': function( data ) { |
393 | | - var stack = []; |
394 | | - // Text and annotations |
395 | | - function start( offset, annotation ) { |
396 | | - // Make a new verion of the annotation object and push it to the stack |
397 | | - var obj = { |
398 | | - 'type': annotation.type, |
399 | | - 'range': { 'start': offset } |
400 | | - }; |
401 | | - if ( annotation.data ) { |
402 | | - obj.data = ve.copyObject( annotation.data ); |
403 | | - } |
404 | | - stack.push( obj ); |
405 | | - } |
406 | | - function end( offset, annotation ) { |
407 | | - for ( var i = stack.length - 1; i >= 0; i-- ) { |
408 | | - if ( !stack[i].range.end ) { |
409 | | - if ( annotation ) { |
410 | | - // We would just compare hashes, but the stack doesn't contain any |
411 | | - if ( stack[i].type === annotation.type && |
412 | | - ve.compareObjects( stack[i].data, annotation.data ) ) { |
413 | | - stack[i].range.end = offset; |
414 | | - break; |
415 | | - } |
416 | | - } else { |
417 | | - stack[i].range.end = offset; |
418 | | - } |
419 | | - } |
420 | | - } |
421 | | - } |
422 | | - var left = '', |
423 | | - right, |
424 | | - leftPlain, |
425 | | - rightPlain, |
426 | | - obj = { 'text': '' }, |
427 | | - offset = 0, |
428 | | - i, |
429 | | - j; |
430 | | - for ( i = 0; i < data.length; i++ ) { |
431 | | - right = data[i]; |
432 | | - leftPlain = typeof left === 'string'; |
433 | | - rightPlain = typeof right === 'string'; |
434 | | - // Open or close annotations |
435 | | - if ( !leftPlain && rightPlain ) { |
436 | | - // [formatted][plain] pair, close any annotations for left |
437 | | - end( i - offset ); |
438 | | - } else if ( leftPlain && !rightPlain ) { |
439 | | - // [plain][formatted] pair, open any annotations for right |
440 | | - for ( j = 1; j < right.length; j++ ) { |
441 | | - start( i - offset, right[j] ); |
442 | | - } |
443 | | - } else if ( !leftPlain && !rightPlain ) { |
444 | | - // [formatted][formatted] pair, open/close any differences |
445 | | - for ( j = 1; j < left.length; j++ ) { |
446 | | - if ( ve.dm.DocumentNode.getIndexOfAnnotation( data[i] , left[j], true ) === -1 ) { |
447 | | - end( i - offset, left[j] ); |
448 | | - } |
449 | | - } |
450 | | - for ( j = 1; j < right.length; j++ ) { |
451 | | - if ( ve.dm.DocumentNode.getIndexOfAnnotation( data[i - 1], right[j], true ) === -1 ) { |
452 | | - start( i - offset, right[j] ); |
453 | | - } |
454 | | - } |
455 | | - } |
456 | | - obj.text += rightPlain ? right : right[0]; |
457 | | - left = right; |
458 | | - } |
459 | | - if ( data.length ) { |
460 | | - end( i - offset ); |
461 | | - } |
462 | | - if ( stack.length ) { |
463 | | - obj.annotations = stack; |
464 | | - } |
465 | | - // Copy attributes if there are any set |
466 | | - if ( !ve.isEmptyObject( this.attributes ) ) { |
467 | | - obj.attributes = ve.extendObject( true, {}, this.attributes ); |
468 | | - } |
469 | | - return obj; |
470 | | - }, |
471 | | - /** |
472 | | - * Checks if a data at a given offset is content. |
473 | | - * |
474 | | - * @example Content data: |
475 | | - * <paragraph> a b c </paragraph> <list> <listItem> d e f </listItem> </list> |
476 | | - * ^ ^ ^ ^ ^ ^ |
477 | | - * |
478 | | - * @static |
479 | | - * @method |
480 | | - * @param {Array} data Data to evaluate offset within |
481 | | - * @param {Integer} offset Offset in data to check |
482 | | - * @returns {Boolean} If data at offset is content |
483 | | - */ |
484 | | - 'isContentData': function( data, offset ) { |
485 | | - // Shortcut: if there's already content there, we will trust it's supposed to be there |
486 | | - return typeof data[offset] === 'string' || ve.isArray( data[offset] ); |
487 | | - }, |
488 | | - /** |
489 | | - * Checks if a data at a given offset is an element. |
490 | | - * |
491 | | - * @example Element data: |
492 | | - * <paragraph> a b c </paragraph> <list> <listItem> d e f </listItem> </list> |
493 | | - * ^ ^ ^ ^ ^ ^ |
494 | | - * |
495 | | - * @static |
496 | | - * @method |
497 | | - * @param {Array} data Data to evaluate offset within |
498 | | - * @param {Integer} offset Offset in data to check |
499 | | - * @returns {Boolean} If data at offset is an element |
500 | | - */ |
501 | | - 'isElementData': function( data, offset ) { |
502 | | - // TODO: Is there a safer way to check if it's a plain object without sacrificing speed? |
503 | | - return offset >= 0 && offset < data.length && data[offset].type !== undefined; |
504 | | - }, |
505 | | - /** |
506 | | - * Checks if an offset within given data is structural. |
507 | | - * |
508 | | - * Structural offsets are those at the beginning, end or surrounded by elements. This differs |
509 | | - * from a location at which an element is present in that elements can be safely inserted at a |
510 | | - * structural location, but not nessecarily where an element is present. |
511 | | - * |
512 | | - * @example Structural offsets: |
513 | | - * <paragraph> a b c </paragraph> <list> <listItem> d e f </listItem> </list> |
514 | | - * ^ ^ ^ ^ ^ |
515 | | - * |
516 | | - * @static |
517 | | - * @method |
518 | | - * @param {Array} data Data to evaluate offset within |
519 | | - * @param {Integer} offset Offset to check |
520 | | - * @returns {Boolean} Whether offset is structural or not |
521 | | - */ |
522 | | - 'isStructuralOffset': function( data, offset ) { |
523 | | - // Edges are always structural |
524 | | - if ( offset === 0 || offset === data.length ) { |
525 | | - return true; |
526 | | - } |
527 | | - // Structual offsets will have elements on each side |
528 | | - if ( data[offset - 1].type !== undefined && data[offset].type !== undefined ) { |
529 | | - if ( '/' + data[offset - 1].type === data[offset].type ) { |
530 | | - return false; |
531 | | - } |
532 | | - return true; |
533 | | - } |
534 | | - return false; |
535 | | - }, |
536 | | - /** |
537 | | - * Checks if elements are present within data. |
538 | | - * |
539 | | - * @static |
540 | | - * @method |
541 | | - * @param {Array} data Data to look for elements within |
542 | | - * @returns {Boolean} If elements exist in data |
543 | | - */ |
544 | | - 'containsElementData': function( data ) { |
545 | | - for ( var i = 0, length = data.length; i < length; i++ ) { |
546 | | - if ( data[i].type !== undefined ) { |
547 | | - return true; |
548 | | - } |
549 | | - } |
550 | | - return false; |
551 | | - } |
552 | | -}; |
| 7 | +ve.dm = {}; |