r95276 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r95275‎ | r95276 | r95277 >
Date:23:17, 22 August 2011
Author:tparscal
Status:deferred
Tags:
Comment:
Moved es.Flow to es.ContentFlow
Modified paths:
  • /trunk/parsers/wikidom/demos/es/index.html (modified) (history)
  • /trunk/parsers/wikidom/lib/es/es.ContentFlow.js (added) (history)
  • /trunk/parsers/wikidom/lib/es/es.Flow.js (deleted) (history)
  • /trunk/parsers/wikidom/lib/es/es.ListBlock.js (modified) (history)
  • /trunk/parsers/wikidom/lib/es/es.ListBlockItem.js (modified) (history)
  • /trunk/parsers/wikidom/lib/es/es.ParagraphBlock.js (modified) (history)
  • /trunk/parsers/wikidom/tests/serializers/index.html (modified) (history)
  • /trunk/parsers/wikidom/tests/transactions/index.html (modified) (history)

Diff [purge]

Index: trunk/parsers/wikidom/tests/transactions/index.html
@@ -13,7 +13,7 @@
1414 <script src="../../lib/es/es.js"></script>
1515 <script src="../../lib/es/es.EventEmitter.js"></script>
1616 <script src="../../lib/es/es.Content.js"></script>
17 - <script src="../../lib/es/es.Flow.js"></script>
 17+ <script src="../../lib/es/es.ContentFlow.js"></script>
1818 <script src="../../lib/es/es.Block.js"></script>
1919 <script src="../../lib/es/es.ParagraphBlock.js"></script>
2020 <script src="../../lib/es/es.BlockTransaction.js"></script>
Index: trunk/parsers/wikidom/tests/serializers/index.html
@@ -19,6 +19,7 @@
2020 <script src="../../lib/es/es.Range.js"></script>
2121 <script src="../../lib/es/es.Selection.js"></script>
2222 <script src="../../lib/es/es.Content.js"></script>
 23+ <script src="../../lib/es/es.ContentFlow.js"></script>
2324 <script src="../../lib/es/es.Container.js"></script>
2425 <script src="../../lib/es/es.DomContainer.js"></script>
2526 <script src="../../lib/es/es.Block.js"></script>
@@ -30,7 +31,6 @@
3132 <script src="../../lib/es/es.Document.HtmlSerializer.js"></script>
3233 <script src="../../lib/es/es.Document.JsonSerializer.js"></script>
3334 <script src="../../lib/es/es.Surface.js"></script>
34 - <script src="../../lib/es/es.Flow.js"></script>
3535 <script src="../../lib/es/es.ParagraphBlock.js"></script>
3636 <script src="../../lib/es/es.ListBlockList.js"></script>
3737 <script src="../../lib/es/es.ListBlockItem.js"></script>
Index: trunk/parsers/wikidom/lib/es/es.Flow.js
@@ -1,571 +0,0 @@
2 -/**
3 - * Flowing text renderer.
4 - *
5 - * TODO: Cleanup code and comments
6 - *
7 - * @class
8 - * @constructor
9 - * @extends {es.EventEmitter}
10 - * @param $container {jQuery} Element to render into
11 - * @param content {es.Content} Initial content to render
12 - * @property $ {jQuery}
13 - * @property content {es.Content}
14 - * @property boundaries {Array}
15 - * @property lines {Array}
16 - * @property width {Integer}
17 - * @property bondaryTest {RegExp}
18 - * @property widthCache {Object}
19 - * @property renderState {Object}
20 - */
21 -es.Flow = function( $container, content ) {
22 - // Inheritance
23 - es.EventEmitter.call( this );
24 -
25 - // Members
26 - this.$ = $container;
27 - this.content = content || new es.Content();
28 - this.boundaries = [];
29 - this.lines = [];
30 - this.width = null;
31 - this.boundaryTest = /([ \-\t\r\n\f])/g;
32 - this.widthCache = {};
33 - this.renderState = {};
34 -
35 - // Events
36 - var flow = this;
37 - function render( args ) {
38 - flow.scanBoundaries();
39 - flow.render( args ? args.offset : 0 );
40 - }
41 - this.content.on( 'insert', render );
42 - this.content.on( 'remove', render );
43 - this.content.on( 'clear', render );
44 - this.content.on( 'annotate', render );
45 -
46 - // Initialization
47 - this.scanBoundaries();
48 -}
49 -
50 -es.Flow.prototype.getLineIndex = function( offset ) {
51 - for ( var i = 0; i < this.lines.length; i++ ) {
52 - if ( this.lines[i].range.containsOffset( offset ) ) {
53 - return i;
54 - }
55 - }
56 - return this.lines.length - 1;
57 -};
58 -
59 -/**
60 - * Gets offset within content closest to of a given position.
61 - *
62 - * Position is assumed to be local to the container the text is being flowed in.
63 - *
64 - * @param position {Object} Position to find offset for
65 - * @param position.left {Integer} Horizontal position in pixels
66 - * @param position.top {Integer} Vertical position in pixels
67 - * @return {Integer} Offset within content nearest the given coordinates
68 - */
69 -es.Flow.prototype.getOffset = function( position ) {
70 - // Empty content shortcut
71 - if ( this.content.getLength() === 0 ) {
72 - return 0;
73 - }
74 -
75 - /*
76 - * Line finding
77 - *
78 - * If the position is above the first line, the offset will always be 0, and if the position is
79 - * below the last line the offset will always be {this.content.length}. All other vertical
80 - * vertical positions will fall inside of one of the lines.
81 - */
82 - var lineCount = this.lines.length;
83 - // Positions above the first line always jump to the first offset
84 - if ( !lineCount || position.top < 0 ) {
85 - return 0;
86 - }
87 - // Find which line the position is inside of
88 - var i = 0,
89 - top = 0;
90 - while ( i < lineCount ) {
91 - top += this.lines[i].height;
92 - if ( position.top <= top ) {
93 - break;
94 - }
95 - i++;
96 - }
97 - // Positions below the last line always jump to the last offset
98 - if ( i == lineCount ) {
99 - return this.content.getLength();
100 - }
101 - // Alias current line object
102 - var line = this.lines[i];
103 -
104 - /*
105 - * Offset finding
106 - *
107 - * Now that we know which line we are on, we can just use the "fitCharacters" method to get the
108 - * last offset before "position.left".
109 - *
110 - * TODO: The offset needs to be chosen based on nearest offset to the cursor, not offset before
111 - * the cursor.
112 - */
113 - var $ruler = $( '<div class="editSurface-ruler"></div>' ).appendTo( this.$ ),
114 - ruler = $ruler[0],
115 - fit = this.fitCharacters( line.range, ruler, position.left );
116 - ruler.innerHTML = this.content.render( new es.Range( line.range.start, fit.end ) );
117 - var left = ruler.clientWidth;
118 - ruler.innerHTML = this.content.render( new es.Range( line.range.start, fit.end + 1 ) );
119 - var right = ruler.clientWidth;
120 - var center = Math.round( left + ( ( right - left ) / 2 ) );
121 - $ruler.remove();
122 - // Reset RegExp object's state
123 - this.boundaryTest.lastIndex = 0;
124 - return Math.min(
125 - // If the position is right of the center of the character it's on top of, increment offset
126 - fit.end + ( position.left >= center ? 1 : 0 ),
127 - // If the line ends in a non-boundary character, decrement offset
128 - line.range.end + ( this.boundaryTest.exec( line.text.substr( -1 ) ) ? -1 : 0 )
129 - );
130 -};
131 -
132 -/**
133 - * Gets position coordinates of a given offset.
134 - *
135 - * Offsets are boundaries between plain or annotated characters within content. Results are given in
136 - * left, top and bottom positions, which could be used to draw a cursor, highlighting, etc.
137 - *
138 - * @param offset {Integer} Offset within content
139 - * @return {Object} Object containing left, top and bottom properties, each positions in pixels as
140 - * well as a line index
141 - */
142 -es.Flow.prototype.getPosition = function( offset ) {
143 - /*
144 - * Range validation
145 - *
146 - * Rather than clamping the range, which can hide errors, exceptions will be thrown if offset is
147 - * less than 0 or greater than the length of the content.
148 - */
149 - if ( offset < 0 ) {
150 - throw 'Out of range error. Offset is expected to be greater than or equal to 0.';
151 - } else if ( offset > this.content.getLength() ) {
152 - throw 'Out of range error. Offset is expected to be less than or equal to text length.';
153 - }
154 -
155 - /*
156 - * Line finding
157 - *
158 - * It's possible that a more efficient method could be used here, but the number of lines to be
159 - * iterated through will rarely be over 100, so it's unlikely that any significant gains will be
160 - * had. Plus, as long as we are iterating over each line, we can also sum up the top and bottom
161 - * positions, which is a nice benefit of this method.
162 - */
163 - var line,
164 - lineCount = this.lines.length,
165 - lineIndex = 0,
166 - position = {
167 - 'left': 0,
168 - 'top': 0,
169 - 'bottom': 0
170 - };
171 - while ( lineIndex < lineCount ) {
172 - line = this.lines[lineIndex];
173 - if ( line.range.containsOffset( offset ) ) {
174 - position.bottom = position.top + line.height;
175 - break;
176 - }
177 - position.top += line.height;
178 - lineIndex++;
179 - }
180 -
181 - /*
182 - * Virtual n+1 position
183 - *
184 - * To allow access to position information of the right side of the last character on the last
185 - * line, a virtual n+1 position is supported. Offsets beyond this virtual position will cause
186 - * an exception to be thrown.
187 - */
188 - if ( lineIndex === lineCount ) {
189 - position.bottom = position.top;
190 - position.top -= line.height;
191 - }
192 -
193 - /*
194 - * Offset measuring
195 - *
196 - * Since the left position will be zero for the first character in the line, so we can skip
197 - * measuring for those cases.
198 - */
199 - if ( line.range.start < offset ) {
200 - var $ruler = $( '<div class="editSurface-ruler"></div>' ).appendTo( this.$ ),
201 - ruler = $ruler[0];
202 - ruler.innerHTML = this.content.render( new es.Range( line.range.start, offset ) );
203 - position.left = ruler.clientWidth;
204 - $ruler.remove();
205 - }
206 -
207 - return position;
208 -};
209 -
210 -/**
211 - * Updates the word boundary cache, which is used for word fitting.
212 - */
213 -es.Flow.prototype.scanBoundaries = function() {
214 - /*
215 - * Word boundary scan
216 - *
217 - * To perform binary-search on words, rather than characters, we need to collect word boundary
218 - * offsets into an array. The offset of the right side of the breaking character is stored, so
219 - * the gaps between stored offsets always include the breaking character at the end.
220 - *
221 - * To avoid encoding the same words as HTML over and over while fitting text to lines, we also
222 - * build a list of HTML escaped strings for each gap between the offsets stored in the
223 - * "boundaries" array. Slices of the "words" array can be joined, producing the escaped HTML of
224 - * the words.
225 - */
226 - var text = this.content.getText();
227 - // Purge "boundaries" and "words" arrays
228 - this.boundaries = [0];
229 - // Reset RegExp object's state
230 - this.boundaryTest.lastIndex = 0;
231 - // Iterate over each word+boundary sequence, capturing offsets and encoding text as we go
232 - var match,
233 - end;
234 - while ( match = this.boundaryTest.exec( text ) ) {
235 - // Include the boundary character in the range
236 - end = match.index + 1;
237 - // Store the boundary offset
238 - this.boundaries.push( end );
239 - }
240 - // If the last character is not a boundary character, we need to append the final range to the
241 - // "boundaries" and "words" arrays
242 - if ( end < text.length || this.boundaries.length === 1 ) {
243 - this.boundaries.push( text.length );
244 - }
245 -};
246 -
247 -/**
248 - * Renders a batch of lines and then yields execution before rendering another batch.
249 - *
250 - * In cases where a single word is too long to fit on a line, the word will be "virtually" wrapped,
251 - * causing them to be fragmented. Word fragments are rendered on their own lines, except for their
252 - * remainder, which is combined with whatever proceeding words can fit on the same line.
253 - */
254 -es.Flow.prototype.renderIteration = function( limit ) {
255 - var rs = this.renderState,
256 - iteration = 0,
257 - fractional = false,
258 - lineStart = this.boundaries[rs.wordOffset],
259 - lineEnd,
260 - wordFit = null,
261 - charOffset = 0,
262 - charFit = null,
263 - wordCount = this.boundaries.length;
264 - while ( ++iteration <= limit && rs.wordOffset < wordCount - 1 ) {
265 - wordFit = this.fitWords( new es.Range( rs.wordOffset, wordCount - 1 ), rs.ruler, rs.width );
266 - fractional = false;
267 - if ( wordFit.width > rs.width ) {
268 - // The first word didn't fit, we need to split it up
269 - charOffset = lineStart;
270 - var lineOffset = rs.wordOffset;
271 - rs.wordOffset++;
272 - lineEnd = this.boundaries[rs.wordOffset];
273 - do {
274 - charFit = this.fitCharacters(
275 - new es.Range( charOffset, lineEnd ), rs.ruler, rs.width
276 - );
277 - // If we were able to get the rest of the characters on the line OK
278 - if ( charFit.end === lineEnd) {
279 - // Try to fit more words on the line
280 - wordFit = this.fitWords(
281 - new es.Range( rs.wordOffset, wordCount - 1 ),
282 - rs.ruler,
283 - rs.width - charFit.width
284 - );
285 - if ( wordFit.end > rs.wordOffset ) {
286 - lineOffset = rs.wordOffset;
287 - rs.wordOffset = wordFit.end;
288 - charFit.end = lineEnd = this.boundaries[rs.wordOffset];
289 - }
290 - }
291 - this.appendLine( new es.Range( charOffset, charFit.end ), lineOffset, fractional );
292 - // Move on to another line
293 - charOffset = charFit.end;
294 - // Mark the next line as fractional
295 - fractional = true;
296 - } while ( charOffset < lineEnd );
297 - } else {
298 - lineEnd = this.boundaries[wordFit.end];
299 - this.appendLine( new es.Range( lineStart, lineEnd ), rs.wordOffset, fractional );
300 - rs.wordOffset = wordFit.end;
301 - }
302 - lineStart = lineEnd;
303 - }
304 - // Only perform on actual last iteration
305 - if ( rs.wordOffset >= wordCount - 1 ) {
306 - // Cleanup
307 - rs.$ruler.remove();
308 - this.lines = rs.lines;
309 - this.$.find( '.editSurface-line[line-index=' + ( this.lines.length - 1 ) + ']' )
310 - .nextAll()
311 - .remove();
312 - rs.timeout = undefined;
313 - this.emit( 'render' );
314 - } else {
315 - rs.ruler.innerHTML = '';
316 - var flow = this;
317 - rs.timeout = setTimeout( function() {
318 - flow.renderIteration( 3 );
319 - }, 0 );
320 - }
321 -};
322 -
323 -/**
324 - * Renders text into a series of HTML elements, each a single line of wrapped text.
325 - *
326 - * The offset parameter can be used to reduce the amount of work involved in re-rendering the same
327 - * text, but will be automatically ignored if the text or width of the container has changed.
328 - *
329 - * Rendering happens asynchronously, and yields execution between iterations. Iterative rendering
330 - * provides the JavaScript engine an ability to process events between rendering batches of lines,
331 - * allowing rendering to be interrupted and restarted if changes to content are happening before
332 - * rendering of all lines is complete.
333 - *
334 - * @param offset {Integer} Offset to re-render from, if possible (not yet implemented)
335 - */
336 -es.Flow.prototype.render = function( offset ) {
337 - var rs = this.renderState;
338 -
339 - // Check if rendering is currently underway
340 - if ( rs.timeout !== undefined ) {
341 - // Cancel the active rendering process
342 - clearTimeout( rs.timeout );
343 - // Cleanup
344 - rs.$ruler.remove();
345 - }
346 -
347 - // Clear caches that were specific to the previous render
348 - this.widthCache = {};
349 -
350 - // In case of empty content we still want to display empty with non-breaking space inside
351 - // This is very important for lists
352 - if(this.content.getLength() === 0) {
353 - var $line = $( '<div class="editSurface-line" line-index="0">&nbsp;</div>' );
354 - this.$.empty().append( $line );
355 - this.lines = [{
356 - 'text': ' ',
357 - 'range': new es.Range( 0,0 ),
358 - 'width': 0,
359 - 'height': $line.outerHeight(),
360 - 'wordOffset': 0,
361 - 'fractional': false
362 - }];
363 - this.emit( 'render' );
364 - return;
365 - }
366 -
367 - /*
368 - * Container measurement
369 - *
370 - * To get an accurate measurement of the inside of the container, without having to deal with
371 - * inconsistencies between browsers and box models, we can just create an element inside the
372 - * container and measure it.
373 - */
374 - rs.$ruler = $( '<div>&nbsp;</div>' ).appendTo( this.$ );
375 - rs.width = rs.$ruler.innerWidth();
376 - rs.ruler = rs.$ruler.addClass('editSurface-ruler')[0];
377 -
378 - // Ignore offset optimization if the width has changed or the text has never been flowed before
379 - if (this.width !== rs.width) {
380 - offset = undefined;
381 - }
382 - this.width = rs.width;
383 -
384 - // Reset the render state
385 - if ( offset ) {
386 - var gap,
387 - currentLine = this.lines.length - 1;
388 - for ( var i = this.lines.length - 1; i >= 0; i-- ) {
389 - var line = this.lines[i];
390 - if ( line.range.start < offset && line.range.end > offset ) {
391 - currentLine = i;
392 - }
393 - if ( ( line.range.end < offset && !line.fractional ) || i === 0 ) {
394 - rs.lines = this.lines.slice( 0, i );
395 - rs.wordOffset = line.wordOffset;
396 - gap = currentLine - i;
397 - break;
398 - }
399 - }
400 - this.renderIteration( 2 + gap );
401 - } else {
402 - rs.lines = [];
403 - rs.wordOffset = 0;
404 - this.renderIteration( 3 );
405 - }
406 -};
407 -
408 -/**
409 - * Adds a line containing a given range of text to the end of the DOM and the "lines" array.
410 - *
411 - * @param range {es.Range} Range of content to append
412 - * @param start {Integer} Beginning of text range for line
413 - * @param end {Integer} Ending of text range for line
414 - * @param wordOffset {Integer} Index within this.words which the line begins with
415 - * @param fractional {Boolean} If the line begins in the middle of a word
416 - */
417 -es.Flow.prototype.appendLine = function( range, wordOffset, fractional ) {
418 - var rs = this.renderState,
419 - lineCount = rs.lines.length;
420 - $line = this.$.children( '[line-index=' + lineCount + ']' );
421 - if ( !$line.length ) {
422 - $line = $( '<div class="editSurface-line" line-index="' + lineCount + '"></div>' );
423 - this.$.append( $line );
424 - }
425 - $line[0].innerHTML = this.content.render( range );
426 - // Collect line information
427 - rs.lines.push({
428 - 'text': this.content.getText( range ),
429 - 'range': range,
430 - 'width': $line.outerWidth(),
431 - 'height': $line.outerHeight(),
432 - 'wordOffset': wordOffset,
433 - 'fractional': fractional
434 - });
435 - // Disable links within content
436 - $line.find( '.editSurface-format-object a' )
437 - .mousedown( function( e ) {
438 - e.preventDefault();
439 - } )
440 - .click( function( e ) {
441 - e.preventDefault();
442 - } );
443 -};
444 -
445 -/**
446 - * Gets the index of the boundary of last word that fits inside the line
447 - *
448 - * The "words" and "boundaries" arrays provide linear access to the offsets around non-breakable
449 - * areas within the text. Using these, we can perform a binary-search for the best fit of words
450 - * within a line, just as we would with characters.
451 - *
452 - * Results are given as an object containing both an index and a width, the later of which can be
453 - * used to detect when the first word was too long to fit on a line. In such cases the result will
454 - * contain the index of the boundary of the first word and it's width.
455 - *
456 - * TODO: Because limit is most likely given as "words.length", it may be possible to improve the
457 - * efficiency of this code by making a best guess and working from there, rather than always
458 - * starting with [offset .. limit], which usually results in reducing the end position in all but
459 - * the last line, and in most cases more than 3 times, before changing directions.
460 - *
461 - * @param range {es.Range} Range of content to try to fit
462 - * @param ruler {HTMLElement} Element to take measurements with
463 - * @param width {Integer} Maximum width to allow the line to extend to
464 - * @return {Integer} Last index within "words" that contains a word that fits
465 - */
466 -es.Flow.prototype.fitWords = function( range, ruler, width ) {
467 - var offset = range.start,
468 - start = range.start,
469 - end = range.end,
470 - charOffset = this.boundaries[offset],
471 - middle,
472 - lineWidth,
473 - cacheKey;
474 - do {
475 - // Place "middle" directly in the center of "start" and "end"
476 - middle = Math.ceil( ( start + end ) / 2 );
477 - charMiddle = this.boundaries[middle];
478 -
479 - // Measure and cache width of substring
480 - cacheKey = charOffset + ':' + charMiddle;
481 - // Prepare the line for measurement using pre-escaped HTML
482 - ruler.innerHTML = this.content.render( new es.Range( charOffset, charMiddle ) );
483 - // Test for over/under using width of the rendered line
484 - this.widthCache[cacheKey] = lineWidth = ruler.clientWidth;
485 -
486 - // Test for over/under using width of the rendered line
487 - if ( lineWidth > width ) {
488 - // Detect impossible fit (the first word won't fit by itself)
489 - if (middle - offset === 1) {
490 - start = middle;
491 - break;
492 - }
493 - // Words after "middle" won't fit
494 - end = middle - 1;
495 - } else {
496 - // Words before "middle" will fit
497 - start = middle;
498 - }
499 - } while ( start < end );
500 - // Check if we ended by moving end to the left of middle
501 - if ( end === middle - 1 ) {
502 - // A final measurement is required
503 - var charStart = this.boundaries[start];
504 - ruler.innerHTML = this.content.render( new es.Range( charOffset, charStart ) );
505 - lineWidth = this.widthCache[charOffset + ':' + charStart] = ruler.clientWidth;
506 - }
507 - return { 'end': start, 'width': lineWidth };
508 -};
509 -
510 -/**
511 - * Gets the index of the boundary of the last character that fits inside the line
512 - *
513 - * Results are given as an object containing both an index and a width, the later of which can be
514 - * used to detect when the first character was too long to fit on a line. In such cases the result
515 - * will contain the index of the first character and it's width.
516 - *
517 - * @param range {es.Range} Range of content to try to fit
518 - * @param ruler {HTMLElement} Element to take measurements with
519 - * @param width {Integer} Maximum width to allow the line to extend to
520 - * @return {Integer} Last index within "text" that contains a character that fits
521 - */
522 -es.Flow.prototype.fitCharacters = function( range, ruler, width ) {
523 - var offset = range.start,
524 - start = range.start,
525 - end = range.end,
526 - middle,
527 - lineWidth,
528 - cacheKey;
529 - do {
530 - // Place "middle" directly in the center of "start" and "end"
531 - middle = Math.ceil( ( start + end ) / 2 );
532 -
533 - // Measure and cache width of substring
534 - cacheKey = offset + ':' + middle;
535 - if ( cacheKey in this.widthCache ) {
536 - lineWidth = this.widthCache[cacheKey];
537 - } else {
538 - // Fill the line with a portion of the text, escaped as HTML
539 - ruler.innerHTML = this.content.render( new es.Range( offset, middle ) );
540 - // Test for over/under using width of the rendered line
541 - this.widthCache[cacheKey] = lineWidth = ruler.clientWidth;
542 - }
543 -
544 - if ( lineWidth > width ) {
545 - // Detect impossible fit (the first character won't fit by itself)
546 - if (middle - offset === 1) {
547 - start = middle - 1;
548 - break;
549 - }
550 - // Words after "middle" won't fit
551 - end = middle - 1;
552 - } else {
553 - // Words before "middle" will fit
554 - start = middle;
555 - }
556 - } while ( start < end );
557 - // Check if we ended by moving end to the left of middle
558 - if ( end === middle - 1 ) {
559 - // Try for cache hit
560 - cacheKey = offset + ':' + start;
561 - if ( cacheKey in this.widthCache ) {
562 - lineWidth = this.widthCache[cacheKey];
563 - } else {
564 - // A final measurement is required
565 - ruler.innerHTML = this.content.render( new es.Range( offset, start ) );
566 - lineWidth = this.widthCache[cacheKey] = ruler.clientWidth;
567 - }
568 - }
569 - return { 'end': start, 'width': lineWidth };
570 -};
571 -
572 -es.extend( es.Flow, es.EventEmitter );
\ No newline at end of file
Index: trunk/parsers/wikidom/lib/es/es.ContentFlow.js
@@ -0,0 +1,571 @@
 2+/**
 3+ * Flowing text renderer.
 4+ *
 5+ * TODO: Cleanup code and comments
 6+ *
 7+ * @class
 8+ * @constructor
 9+ * @extends {es.EventEmitter}
 10+ * @param $container {jQuery} Element to render into
 11+ * @param content {es.Content} Initial content to render
 12+ * @property $ {jQuery}
 13+ * @property content {es.Content}
 14+ * @property boundaries {Array}
 15+ * @property lines {Array}
 16+ * @property width {Integer}
 17+ * @property bondaryTest {RegExp}
 18+ * @property widthCache {Object}
 19+ * @property renderState {Object}
 20+ */
 21+es.ContentFlow = function( $container, content ) {
 22+ // Inheritance
 23+ es.EventEmitter.call( this );
 24+
 25+ // Members
 26+ this.$ = $container;
 27+ this.content = content || new es.Content();
 28+ this.boundaries = [];
 29+ this.lines = [];
 30+ this.width = null;
 31+ this.boundaryTest = /([ \-\t\r\n\f])/g;
 32+ this.widthCache = {};
 33+ this.renderState = {};
 34+
 35+ // Events
 36+ var flow = this;
 37+ function render( args ) {
 38+ flow.scanBoundaries();
 39+ flow.render( args ? args.offset : 0 );
 40+ }
 41+ this.content.on( 'insert', render );
 42+ this.content.on( 'remove', render );
 43+ this.content.on( 'clear', render );
 44+ this.content.on( 'annotate', render );
 45+
 46+ // Initialization
 47+ this.scanBoundaries();
 48+}
 49+
 50+es.ContentFlow.prototype.getLineIndex = function( offset ) {
 51+ for ( var i = 0; i < this.lines.length; i++ ) {
 52+ if ( this.lines[i].range.containsOffset( offset ) ) {
 53+ return i;
 54+ }
 55+ }
 56+ return this.lines.length - 1;
 57+};
 58+
 59+/**
 60+ * Gets offset within content closest to of a given position.
 61+ *
 62+ * Position is assumed to be local to the container the text is being flowed in.
 63+ *
 64+ * @param position {Object} Position to find offset for
 65+ * @param position.left {Integer} Horizontal position in pixels
 66+ * @param position.top {Integer} Vertical position in pixels
 67+ * @return {Integer} Offset within content nearest the given coordinates
 68+ */
 69+es.ContentFlow.prototype.getOffset = function( position ) {
 70+ // Empty content shortcut
 71+ if ( this.content.getLength() === 0 ) {
 72+ return 0;
 73+ }
 74+
 75+ /*
 76+ * Line finding
 77+ *
 78+ * If the position is above the first line, the offset will always be 0, and if the position is
 79+ * below the last line the offset will always be {this.content.length}. All other vertical
 80+ * vertical positions will fall inside of one of the lines.
 81+ */
 82+ var lineCount = this.lines.length;
 83+ // Positions above the first line always jump to the first offset
 84+ if ( !lineCount || position.top < 0 ) {
 85+ return 0;
 86+ }
 87+ // Find which line the position is inside of
 88+ var i = 0,
 89+ top = 0;
 90+ while ( i < lineCount ) {
 91+ top += this.lines[i].height;
 92+ if ( position.top <= top ) {
 93+ break;
 94+ }
 95+ i++;
 96+ }
 97+ // Positions below the last line always jump to the last offset
 98+ if ( i == lineCount ) {
 99+ return this.content.getLength();
 100+ }
 101+ // Alias current line object
 102+ var line = this.lines[i];
 103+
 104+ /*
 105+ * Offset finding
 106+ *
 107+ * Now that we know which line we are on, we can just use the "fitCharacters" method to get the
 108+ * last offset before "position.left".
 109+ *
 110+ * TODO: The offset needs to be chosen based on nearest offset to the cursor, not offset before
 111+ * the cursor.
 112+ */
 113+ var $ruler = $( '<div class="editSurface-ruler"></div>' ).appendTo( this.$ ),
 114+ ruler = $ruler[0],
 115+ fit = this.fitCharacters( line.range, ruler, position.left );
 116+ ruler.innerHTML = this.content.render( new es.Range( line.range.start, fit.end ) );
 117+ var left = ruler.clientWidth;
 118+ ruler.innerHTML = this.content.render( new es.Range( line.range.start, fit.end + 1 ) );
 119+ var right = ruler.clientWidth;
 120+ var center = Math.round( left + ( ( right - left ) / 2 ) );
 121+ $ruler.remove();
 122+ // Reset RegExp object's state
 123+ this.boundaryTest.lastIndex = 0;
 124+ return Math.min(
 125+ // If the position is right of the center of the character it's on top of, increment offset
 126+ fit.end + ( position.left >= center ? 1 : 0 ),
 127+ // If the line ends in a non-boundary character, decrement offset
 128+ line.range.end + ( this.boundaryTest.exec( line.text.substr( -1 ) ) ? -1 : 0 )
 129+ );
 130+};
 131+
 132+/**
 133+ * Gets position coordinates of a given offset.
 134+ *
 135+ * Offsets are boundaries between plain or annotated characters within content. Results are given in
 136+ * left, top and bottom positions, which could be used to draw a cursor, highlighting, etc.
 137+ *
 138+ * @param offset {Integer} Offset within content
 139+ * @return {Object} Object containing left, top and bottom properties, each positions in pixels as
 140+ * well as a line index
 141+ */
 142+es.ContentFlow.prototype.getPosition = function( offset ) {
 143+ /*
 144+ * Range validation
 145+ *
 146+ * Rather than clamping the range, which can hide errors, exceptions will be thrown if offset is
 147+ * less than 0 or greater than the length of the content.
 148+ */
 149+ if ( offset < 0 ) {
 150+ throw 'Out of range error. Offset is expected to be greater than or equal to 0.';
 151+ } else if ( offset > this.content.getLength() ) {
 152+ throw 'Out of range error. Offset is expected to be less than or equal to text length.';
 153+ }
 154+
 155+ /*
 156+ * Line finding
 157+ *
 158+ * It's possible that a more efficient method could be used here, but the number of lines to be
 159+ * iterated through will rarely be over 100, so it's unlikely that any significant gains will be
 160+ * had. Plus, as long as we are iterating over each line, we can also sum up the top and bottom
 161+ * positions, which is a nice benefit of this method.
 162+ */
 163+ var line,
 164+ lineCount = this.lines.length,
 165+ lineIndex = 0,
 166+ position = {
 167+ 'left': 0,
 168+ 'top': 0,
 169+ 'bottom': 0
 170+ };
 171+ while ( lineIndex < lineCount ) {
 172+ line = this.lines[lineIndex];
 173+ if ( line.range.containsOffset( offset ) ) {
 174+ position.bottom = position.top + line.height;
 175+ break;
 176+ }
 177+ position.top += line.height;
 178+ lineIndex++;
 179+ }
 180+
 181+ /*
 182+ * Virtual n+1 position
 183+ *
 184+ * To allow access to position information of the right side of the last character on the last
 185+ * line, a virtual n+1 position is supported. Offsets beyond this virtual position will cause
 186+ * an exception to be thrown.
 187+ */
 188+ if ( lineIndex === lineCount ) {
 189+ position.bottom = position.top;
 190+ position.top -= line.height;
 191+ }
 192+
 193+ /*
 194+ * Offset measuring
 195+ *
 196+ * Since the left position will be zero for the first character in the line, so we can skip
 197+ * measuring for those cases.
 198+ */
 199+ if ( line.range.start < offset ) {
 200+ var $ruler = $( '<div class="editSurface-ruler"></div>' ).appendTo( this.$ ),
 201+ ruler = $ruler[0];
 202+ ruler.innerHTML = this.content.render( new es.Range( line.range.start, offset ) );
 203+ position.left = ruler.clientWidth;
 204+ $ruler.remove();
 205+ }
 206+
 207+ return position;
 208+};
 209+
 210+/**
 211+ * Updates the word boundary cache, which is used for word fitting.
 212+ */
 213+es.ContentFlow.prototype.scanBoundaries = function() {
 214+ /*
 215+ * Word boundary scan
 216+ *
 217+ * To perform binary-search on words, rather than characters, we need to collect word boundary
 218+ * offsets into an array. The offset of the right side of the breaking character is stored, so
 219+ * the gaps between stored offsets always include the breaking character at the end.
 220+ *
 221+ * To avoid encoding the same words as HTML over and over while fitting text to lines, we also
 222+ * build a list of HTML escaped strings for each gap between the offsets stored in the
 223+ * "boundaries" array. Slices of the "words" array can be joined, producing the escaped HTML of
 224+ * the words.
 225+ */
 226+ var text = this.content.getText();
 227+ // Purge "boundaries" and "words" arrays
 228+ this.boundaries = [0];
 229+ // Reset RegExp object's state
 230+ this.boundaryTest.lastIndex = 0;
 231+ // Iterate over each word+boundary sequence, capturing offsets and encoding text as we go
 232+ var match,
 233+ end;
 234+ while ( match = this.boundaryTest.exec( text ) ) {
 235+ // Include the boundary character in the range
 236+ end = match.index + 1;
 237+ // Store the boundary offset
 238+ this.boundaries.push( end );
 239+ }
 240+ // If the last character is not a boundary character, we need to append the final range to the
 241+ // "boundaries" and "words" arrays
 242+ if ( end < text.length || this.boundaries.length === 1 ) {
 243+ this.boundaries.push( text.length );
 244+ }
 245+};
 246+
 247+/**
 248+ * Renders a batch of lines and then yields execution before rendering another batch.
 249+ *
 250+ * In cases where a single word is too long to fit on a line, the word will be "virtually" wrapped,
 251+ * causing them to be fragmented. Word fragments are rendered on their own lines, except for their
 252+ * remainder, which is combined with whatever proceeding words can fit on the same line.
 253+ */
 254+es.ContentFlow.prototype.renderIteration = function( limit ) {
 255+ var rs = this.renderState,
 256+ iteration = 0,
 257+ fractional = false,
 258+ lineStart = this.boundaries[rs.wordOffset],
 259+ lineEnd,
 260+ wordFit = null,
 261+ charOffset = 0,
 262+ charFit = null,
 263+ wordCount = this.boundaries.length;
 264+ while ( ++iteration <= limit && rs.wordOffset < wordCount - 1 ) {
 265+ wordFit = this.fitWords( new es.Range( rs.wordOffset, wordCount - 1 ), rs.ruler, rs.width );
 266+ fractional = false;
 267+ if ( wordFit.width > rs.width ) {
 268+ // The first word didn't fit, we need to split it up
 269+ charOffset = lineStart;
 270+ var lineOffset = rs.wordOffset;
 271+ rs.wordOffset++;
 272+ lineEnd = this.boundaries[rs.wordOffset];
 273+ do {
 274+ charFit = this.fitCharacters(
 275+ new es.Range( charOffset, lineEnd ), rs.ruler, rs.width
 276+ );
 277+ // If we were able to get the rest of the characters on the line OK
 278+ if ( charFit.end === lineEnd) {
 279+ // Try to fit more words on the line
 280+ wordFit = this.fitWords(
 281+ new es.Range( rs.wordOffset, wordCount - 1 ),
 282+ rs.ruler,
 283+ rs.width - charFit.width
 284+ );
 285+ if ( wordFit.end > rs.wordOffset ) {
 286+ lineOffset = rs.wordOffset;
 287+ rs.wordOffset = wordFit.end;
 288+ charFit.end = lineEnd = this.boundaries[rs.wordOffset];
 289+ }
 290+ }
 291+ this.appendLine( new es.Range( charOffset, charFit.end ), lineOffset, fractional );
 292+ // Move on to another line
 293+ charOffset = charFit.end;
 294+ // Mark the next line as fractional
 295+ fractional = true;
 296+ } while ( charOffset < lineEnd );
 297+ } else {
 298+ lineEnd = this.boundaries[wordFit.end];
 299+ this.appendLine( new es.Range( lineStart, lineEnd ), rs.wordOffset, fractional );
 300+ rs.wordOffset = wordFit.end;
 301+ }
 302+ lineStart = lineEnd;
 303+ }
 304+ // Only perform on actual last iteration
 305+ if ( rs.wordOffset >= wordCount - 1 ) {
 306+ // Cleanup
 307+ rs.$ruler.remove();
 308+ this.lines = rs.lines;
 309+ this.$.find( '.editSurface-line[line-index=' + ( this.lines.length - 1 ) + ']' )
 310+ .nextAll()
 311+ .remove();
 312+ rs.timeout = undefined;
 313+ this.emit( 'render' );
 314+ } else {
 315+ rs.ruler.innerHTML = '';
 316+ var flow = this;
 317+ rs.timeout = setTimeout( function() {
 318+ flow.renderIteration( 3 );
 319+ }, 0 );
 320+ }
 321+};
 322+
 323+/**
 324+ * Renders text into a series of HTML elements, each a single line of wrapped text.
 325+ *
 326+ * The offset parameter can be used to reduce the amount of work involved in re-rendering the same
 327+ * text, but will be automatically ignored if the text or width of the container has changed.
 328+ *
 329+ * Rendering happens asynchronously, and yields execution between iterations. Iterative rendering
 330+ * provides the JavaScript engine an ability to process events between rendering batches of lines,
 331+ * allowing rendering to be interrupted and restarted if changes to content are happening before
 332+ * rendering of all lines is complete.
 333+ *
 334+ * @param offset {Integer} Offset to re-render from, if possible (not yet implemented)
 335+ */
 336+es.ContentFlow.prototype.render = function( offset ) {
 337+ var rs = this.renderState;
 338+
 339+ // Check if rendering is currently underway
 340+ if ( rs.timeout !== undefined ) {
 341+ // Cancel the active rendering process
 342+ clearTimeout( rs.timeout );
 343+ // Cleanup
 344+ rs.$ruler.remove();
 345+ }
 346+
 347+ // Clear caches that were specific to the previous render
 348+ this.widthCache = {};
 349+
 350+ // In case of empty content we still want to display empty with non-breaking space inside
 351+ // This is very important for lists
 352+ if(this.content.getLength() === 0) {
 353+ var $line = $( '<div class="editSurface-line" line-index="0">&nbsp;</div>' );
 354+ this.$.empty().append( $line );
 355+ this.lines = [{
 356+ 'text': ' ',
 357+ 'range': new es.Range( 0,0 ),
 358+ 'width': 0,
 359+ 'height': $line.outerHeight(),
 360+ 'wordOffset': 0,
 361+ 'fractional': false
 362+ }];
 363+ this.emit( 'render' );
 364+ return;
 365+ }
 366+
 367+ /*
 368+ * Container measurement
 369+ *
 370+ * To get an accurate measurement of the inside of the container, without having to deal with
 371+ * inconsistencies between browsers and box models, we can just create an element inside the
 372+ * container and measure it.
 373+ */
 374+ rs.$ruler = $( '<div>&nbsp;</div>' ).appendTo( this.$ );
 375+ rs.width = rs.$ruler.innerWidth();
 376+ rs.ruler = rs.$ruler.addClass('editSurface-ruler')[0];
 377+
 378+ // Ignore offset optimization if the width has changed or the text has never been flowed before
 379+ if (this.width !== rs.width) {
 380+ offset = undefined;
 381+ }
 382+ this.width = rs.width;
 383+
 384+ // Reset the render state
 385+ if ( offset ) {
 386+ var gap,
 387+ currentLine = this.lines.length - 1;
 388+ for ( var i = this.lines.length - 1; i >= 0; i-- ) {
 389+ var line = this.lines[i];
 390+ if ( line.range.start < offset && line.range.end > offset ) {
 391+ currentLine = i;
 392+ }
 393+ if ( ( line.range.end < offset && !line.fractional ) || i === 0 ) {
 394+ rs.lines = this.lines.slice( 0, i );
 395+ rs.wordOffset = line.wordOffset;
 396+ gap = currentLine - i;
 397+ break;
 398+ }
 399+ }
 400+ this.renderIteration( 2 + gap );
 401+ } else {
 402+ rs.lines = [];
 403+ rs.wordOffset = 0;
 404+ this.renderIteration( 3 );
 405+ }
 406+};
 407+
 408+/**
 409+ * Adds a line containing a given range of text to the end of the DOM and the "lines" array.
 410+ *
 411+ * @param range {es.Range} Range of content to append
 412+ * @param start {Integer} Beginning of text range for line
 413+ * @param end {Integer} Ending of text range for line
 414+ * @param wordOffset {Integer} Index within this.words which the line begins with
 415+ * @param fractional {Boolean} If the line begins in the middle of a word
 416+ */
 417+es.ContentFlow.prototype.appendLine = function( range, wordOffset, fractional ) {
 418+ var rs = this.renderState,
 419+ lineCount = rs.lines.length;
 420+ $line = this.$.children( '[line-index=' + lineCount + ']' );
 421+ if ( !$line.length ) {
 422+ $line = $( '<div class="editSurface-line" line-index="' + lineCount + '"></div>' );
 423+ this.$.append( $line );
 424+ }
 425+ $line[0].innerHTML = this.content.render( range );
 426+ // Collect line information
 427+ rs.lines.push({
 428+ 'text': this.content.getText( range ),
 429+ 'range': range,
 430+ 'width': $line.outerWidth(),
 431+ 'height': $line.outerHeight(),
 432+ 'wordOffset': wordOffset,
 433+ 'fractional': fractional
 434+ });
 435+ // Disable links within content
 436+ $line.find( '.editSurface-format-object a' )
 437+ .mousedown( function( e ) {
 438+ e.preventDefault();
 439+ } )
 440+ .click( function( e ) {
 441+ e.preventDefault();
 442+ } );
 443+};
 444+
 445+/**
 446+ * Gets the index of the boundary of last word that fits inside the line
 447+ *
 448+ * The "words" and "boundaries" arrays provide linear access to the offsets around non-breakable
 449+ * areas within the text. Using these, we can perform a binary-search for the best fit of words
 450+ * within a line, just as we would with characters.
 451+ *
 452+ * Results are given as an object containing both an index and a width, the later of which can be
 453+ * used to detect when the first word was too long to fit on a line. In such cases the result will
 454+ * contain the index of the boundary of the first word and it's width.
 455+ *
 456+ * TODO: Because limit is most likely given as "words.length", it may be possible to improve the
 457+ * efficiency of this code by making a best guess and working from there, rather than always
 458+ * starting with [offset .. limit], which usually results in reducing the end position in all but
 459+ * the last line, and in most cases more than 3 times, before changing directions.
 460+ *
 461+ * @param range {es.Range} Range of content to try to fit
 462+ * @param ruler {HTMLElement} Element to take measurements with
 463+ * @param width {Integer} Maximum width to allow the line to extend to
 464+ * @return {Integer} Last index within "words" that contains a word that fits
 465+ */
 466+es.ContentFlow.prototype.fitWords = function( range, ruler, width ) {
 467+ var offset = range.start,
 468+ start = range.start,
 469+ end = range.end,
 470+ charOffset = this.boundaries[offset],
 471+ middle,
 472+ lineWidth,
 473+ cacheKey;
 474+ do {
 475+ // Place "middle" directly in the center of "start" and "end"
 476+ middle = Math.ceil( ( start + end ) / 2 );
 477+ charMiddle = this.boundaries[middle];
 478+
 479+ // Measure and cache width of substring
 480+ cacheKey = charOffset + ':' + charMiddle;
 481+ // Prepare the line for measurement using pre-escaped HTML
 482+ ruler.innerHTML = this.content.render( new es.Range( charOffset, charMiddle ) );
 483+ // Test for over/under using width of the rendered line
 484+ this.widthCache[cacheKey] = lineWidth = ruler.clientWidth;
 485+
 486+ // Test for over/under using width of the rendered line
 487+ if ( lineWidth > width ) {
 488+ // Detect impossible fit (the first word won't fit by itself)
 489+ if (middle - offset === 1) {
 490+ start = middle;
 491+ break;
 492+ }
 493+ // Words after "middle" won't fit
 494+ end = middle - 1;
 495+ } else {
 496+ // Words before "middle" will fit
 497+ start = middle;
 498+ }
 499+ } while ( start < end );
 500+ // Check if we ended by moving end to the left of middle
 501+ if ( end === middle - 1 ) {
 502+ // A final measurement is required
 503+ var charStart = this.boundaries[start];
 504+ ruler.innerHTML = this.content.render( new es.Range( charOffset, charStart ) );
 505+ lineWidth = this.widthCache[charOffset + ':' + charStart] = ruler.clientWidth;
 506+ }
 507+ return { 'end': start, 'width': lineWidth };
 508+};
 509+
 510+/**
 511+ * Gets the index of the boundary of the last character that fits inside the line
 512+ *
 513+ * Results are given as an object containing both an index and a width, the later of which can be
 514+ * used to detect when the first character was too long to fit on a line. In such cases the result
 515+ * will contain the index of the first character and it's width.
 516+ *
 517+ * @param range {es.Range} Range of content to try to fit
 518+ * @param ruler {HTMLElement} Element to take measurements with
 519+ * @param width {Integer} Maximum width to allow the line to extend to
 520+ * @return {Integer} Last index within "text" that contains a character that fits
 521+ */
 522+es.ContentFlow.prototype.fitCharacters = function( range, ruler, width ) {
 523+ var offset = range.start,
 524+ start = range.start,
 525+ end = range.end,
 526+ middle,
 527+ lineWidth,
 528+ cacheKey;
 529+ do {
 530+ // Place "middle" directly in the center of "start" and "end"
 531+ middle = Math.ceil( ( start + end ) / 2 );
 532+
 533+ // Measure and cache width of substring
 534+ cacheKey = offset + ':' + middle;
 535+ if ( cacheKey in this.widthCache ) {
 536+ lineWidth = this.widthCache[cacheKey];
 537+ } else {
 538+ // Fill the line with a portion of the text, escaped as HTML
 539+ ruler.innerHTML = this.content.render( new es.Range( offset, middle ) );
 540+ // Test for over/under using width of the rendered line
 541+ this.widthCache[cacheKey] = lineWidth = ruler.clientWidth;
 542+ }
 543+
 544+ if ( lineWidth > width ) {
 545+ // Detect impossible fit (the first character won't fit by itself)
 546+ if (middle - offset === 1) {
 547+ start = middle - 1;
 548+ break;
 549+ }
 550+ // Words after "middle" won't fit
 551+ end = middle - 1;
 552+ } else {
 553+ // Words before "middle" will fit
 554+ start = middle;
 555+ }
 556+ } while ( start < end );
 557+ // Check if we ended by moving end to the left of middle
 558+ if ( end === middle - 1 ) {
 559+ // Try for cache hit
 560+ cacheKey = offset + ':' + start;
 561+ if ( cacheKey in this.widthCache ) {
 562+ lineWidth = this.widthCache[cacheKey];
 563+ } else {
 564+ // A final measurement is required
 565+ ruler.innerHTML = this.content.render( new es.Range( offset, start ) );
 566+ lineWidth = this.widthCache[cacheKey] = ruler.clientWidth;
 567+ }
 568+ }
 569+ return { 'end': start, 'width': lineWidth };
 570+};
 571+
 572+es.extend( es.ContentFlow, es.EventEmitter );
\ No newline at end of file
Property changes on: trunk/parsers/wikidom/lib/es/es.ContentFlow.js
___________________________________________________________________
Added: svn:eol-style
1573 + native
Added: svn:mime-type
2574 + text/plain
Index: trunk/parsers/wikidom/lib/es/es.ListBlockItem.js
@@ -19,7 +19,7 @@
2020 .append( this.$content );
2121 this.setStyle( style );
2222 this.setLevel( level );
23 - this.flow = new es.Flow( this.$content, this.content );
 23+ this.flow = new es.ContentFlow( this.$content, this.content );
2424 // Listen to render events and trigger update event upstream
2525 var listBlockItem = this;
2626 this.flow.on( 'render', function() {
Index: trunk/parsers/wikidom/lib/es/es.ParagraphBlock.js
@@ -7,14 +7,14 @@
88 * @param content {es.Content} Paragraph content
99 * @property content {es.Content} Paragraph content
1010 * @property $ {jQuery} Container element
11 - * @property flow {es.Flow} Text flow object
 11+ * @property flow {es.ContentFlow} Text flow object
1212 */
1313 es.ParagraphBlock = function( content ) {
1414 es.Block.call( this );
1515 this.content = content || new es.Content();
1616 this.$ = $( '<div class="editSurface-block editSurface-paragraph"></div>' )
1717 .data( 'block', this );
18 - this.flow = new es.Flow( this.$, this.content );
 18+ this.flow = new es.ContentFlow( this.$, this.content );
1919 var block = this;
2020 this.flow.on( 'render', function() {
2121 block.emit( 'update' );
Index: trunk/parsers/wikidom/lib/es/es.ListBlock.js
@@ -142,7 +142,7 @@
143143 break;
144144 }
145145 globalOffset += itemLength + 1;
146 - lineIndex += this.list.items[i].flow.lines.length; // TODO: add method getLineCount() to es.Flow
 146+ lineIndex += this.list.items[i].flow.lines.length; // TODO: add method getLineCount() to es.ContentFlow
147147 }
148148 return lineIndex;
149149 };
Index: trunk/parsers/wikidom/demos/es/index.html
@@ -61,6 +61,7 @@
6262 <script src="../../lib/es/es.Range.js"></script>
6363 <script src="../../lib/es/es.Selection.js"></script>
6464 <script src="../../lib/es/es.Content.js"></script>
 65+ <script src="../../lib/es/es.ContentFlow.js"></script>
6566 <script src="../../lib/es/es.Container.js"></script>
6667 <script src="../../lib/es/es.DomContainer.js"></script>
6768 <script src="../../lib/es/es.Block.js"></script>
@@ -72,7 +73,6 @@
7374 <script src="../../lib/es/es.Document.HtmlSerializer.js"></script>
7475 <script src="../../lib/es/es.Document.JsonSerializer.js"></script>
7576 <script src="../../lib/es/es.Surface.js"></script>
76 - <script src="../../lib/es/es.Flow.js"></script>
7777 <script src="../../lib/es/es.ParagraphBlock.js"></script>
7878 <script src="../../lib/es/es.ListBlockList.js"></script>
7979 <script src="../../lib/es/es.ListBlockItem.js"></script>

Status & tagging log