r91237 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r91236‎ | r91237 | r91238 >
Date:23:54, 30 June 2011
Author:tparscal
Status:deferred
Tags:
Comment:
Refactored flow to be more efficient and well structured and added more documentation.
Modified paths:
  • /trunk/parsers/wikidom/demos/es/index.html (modified) (history)
  • /trunk/parsers/wikidom/lib/es/es.ParagraphBlock.js (modified) (history)
  • /trunk/parsers/wikidom/lib/es/es.Surface.css (modified) (history)
  • /trunk/parsers/wikidom/lib/es/es.TextFlow.js (modified) (history)

Diff [purge]

Index: trunk/parsers/wikidom/lib/es/es.ParagraphBlock.js
@@ -6,12 +6,20 @@
77 function ParagraphBlock( lines ) {
88 Block.call( this );
99 this.lines = lines || [];
10 - this.lineMetrics = [];
1110 this.$ = $( '<div class="editSurface-block editSurface-paragraph"></div>' )
1211 .data( 'block', this );
1312 this.flow = new TextFlow( this.$ );
 13+ this.updateText();
1414 }
1515
 16+Block.prototype.updateText = function() {
 17+ var text = [];
 18+ for ( var i = 0; i < this.lines.length; i++ ) {
 19+ text.push( this.lines[i].text );
 20+ }
 21+ this.flow.setText( text.join( '\n' ) );
 22+};
 23+
1624 /**
1725 * Inserts content into a block at an offset.
1826 *
@@ -29,6 +37,7 @@
3038 }
3139 lineOffset += line.text.length;
3240 }
 41+ this.updateText();
3342 };
3443
3544 /**
@@ -74,6 +83,7 @@
7584 // Remove lines after "from" up to and including "to"
7685 this.lines = this.lines.splice( from.index + 1, to.index - from.index );
7786 }
 87+ this.updateText();
7888 };
7989
8090 /**
@@ -82,11 +92,7 @@
8393 * @param $container {jQuery Selection} Container to render into
8494 */
8595 Block.prototype.renderContent = function() {
86 - var text = [];
87 - for ( var i = 0; i < this.lines.length; i++ ) {
88 - text.push( this.lines[i].text );
89 - }
90 - this.lineMetrics = this.flow.render( text.join( '\n' ) );
 96+ this.flow.render();
9197 };
9298
9399 /**
Index: trunk/parsers/wikidom/lib/es/es.Surface.css
@@ -1,20 +1,24 @@
22 body {
33 font-family: "Arial";
4 - font-size: 0.8em;
 4+ font-size: 1em;
55 width: 100%;
66 margin: 0;
77 padding: 0;
 8+ overflow-y: scroll;
89 }
910
1011 .editSurface-document {
11 - /*border: solid 1px silver;*/
12 - background-color: lightgreen;
 12+ border: solid 1px silver;
 13+ background-color: #dddddd;
1314 cursor: text;
14 - width: 50%;
 15+ margin-left: 12.5%;
 16+ margin-top: 1em;
 17+ width: 75%;
1518 }
1619
1720 .editSurface-paragraph {
18 - /*margin: 2em;*/
 21+ background-color: #eeeeee;
 22+ margin: 2em;
1923 }
2024
2125 .editSurface-line {
@@ -23,8 +27,8 @@
2428 line-height: 1.5em;
2529 cursor: text;
2630 white-space: nowrap;
27 - color: #eeeeee;
28 - background-color: #333333;
 31+ color: #000000;
 32+ background-color: #ffffff;
2933 }
3034
3135 .editSurface-line.empty {
Index: trunk/parsers/wikidom/lib/es/es.TextFlow.js
@@ -4,18 +4,25 @@
55 * @param $container {jQuery Selection} Element to render into
66 * @returns {TextFlow}
77 */
8 -function TextFlow( $container ) {
 8+function TextFlow( $container, text ) {
99 this.$ = $container;
10 - this.lines = [];
1110 this.boundaries = [];
1211 this.words = [];
 12+ this.lines = [];
 13+ this.width = null;
 14+ this.text = '';
 15+ if ( text !== undefined ) {
 16+ this.setText( text );
 17+ }
1318 }
1419
15 -TextFlow.prototype.htmlEncode = function( text, trim ) {
16 - if ( trim ) {
17 - // Trailing whitespace
18 - text = text.replace( / $/, '' );
19 - }
 20+/**
 21+ * Encodes text as an HTML string.
 22+ *
 23+ * @param text {String} Text to escape
 24+ * @return {String} HTML escaped text
 25+ */
 26+TextFlow.prototype.escape = function( text ) {
2027 return text
2128 // Tags
2229 .replace( /&/g, '&amp;' )
@@ -31,10 +38,11 @@
3239 };
3340
3441 /**
35 - * Gets an offset within from x and y coordinates.
 42+ * Gets offset within content closest to of a given position.
3643 *
37 - * @param x {Integer}
38 - * @param y {Integer}
 44+ * @param x {Integer} Horizontal position in pixels
 45+ * @param y {Integer} Vertical position in pixels
 46+ * @return {Integer} Offset within content nearest the given coordinates
3947 */
4048 TextFlow.prototype.getOffset = function( x, y ) {
4149 var line = 0,
@@ -64,6 +72,15 @@
6573 return offset;
6674 };
6775
 76+/**
 77+ * Gets position coordinates of a given offset.
 78+ *
 79+ * Offsets are boundaries between content. Results are given in left, top and bottom positions,
 80+ * which could be used to draw a cursor, highlighting painting, etc.
 81+ *
 82+ * @param offset {Integer} Offset within content
 83+ * @return {Object} Object containing left, top and bottom properties, each positions in pixels
 84+ */
6885 TextFlow.prototype.getPosition = function( offset ) {
6986 if ( offset < 0 ) {
7087 throw 'Out of range error. Offset is expected to be greater than or equal to 0.';
@@ -119,7 +136,7 @@
120137 if ( lines[line].start < offset ) {
121138 var $ruler = $( '<div class="editSurface-line"></div>' ).appendTo( this.$ ),
122139 ruler = $ruler[0];
123 - ruler.innerHTML = this.htmlEncode( text.substring( lines[startLine].start, offset ) );
 140+ ruler.innerHTML = this.escape( text.substring( lines[startLine].start, offset ) );
124141 position.left = ruler.clientWidth;
125142 $ruler.remove();
126143 }
@@ -127,14 +144,66 @@
128145 return position;
129146 };
130147
 148+TextFlow.prototype.setText = function( text ) {
 149+ // Don't reevaluate boundaries if the text hasn't actually changed
 150+ if ( text === this.text ) {
 151+ return;
 152+ }
 153+ this.text = text;
 154+
 155+ /*
 156+ * Word boundary scan
 157+ *
 158+ * To perform binary-search on words, rather than characters, we need to collect word boundary
 159+ * offsets into an array. The offset of the right side of the breaking character is stored, so
 160+ * the gaps between stored offsets always include the breaking character at the end.
 161+ *
 162+ * To avoid encoding the same words as HTML over and over while fitting text to lines, we also
 163+ * build a list of HTML escaped strings for each gap between the offsets stored in the
 164+ * "boundaries" array. Slices of the "words" array can be joined, producing the escaped HTML of
 165+ * the words.
 166+ */
 167+ // Purge "boundaries" and "words" arrays
 168+ this.boundaries = [];
 169+ this.words = [];
 170+ // Iterate over each word+boundary sequence, capturing offsets and encoding text as we go
 171+ var boundary = /([ \.\,\;\:\-\t\r\n\f])/g,
 172+ match,
 173+ start = 0,
 174+ end;
 175+ while ( match = boundary.exec( text ) ) {
 176+ // Include the boundary character in the range
 177+ end = match.index + 1;
 178+ // Store the boundary offset
 179+ this.boundaries.push( end );
 180+ // Store the word's escaped HTML
 181+ this.words.push( this.escape( text.substring( start, end ) ) );
 182+ // Remember the previous match
 183+ start = end;
 184+ }
 185+ // If the last character is not a boundary character, we need to append the final range to the
 186+ // "boundaries" and "words" arrays
 187+ if ( end < text.length ) {
 188+ this.boundaries.push( text.length );
 189+ this.words.push( this.escape( text.substring( end, text.length ) ) );
 190+ }
 191+ // Force re-flow
 192+ this.width = null;
 193+};
 194+
131195 /**
132196 * Renders text into a series of HTML elements, each a single line of wrapped text.
133197 *
134 - * TODO: Allow re-flowing from a given offset on to make re-flow faster when modifying the text
 198+ * In cases where a single word is too long to fit on a line, the word will be "virtually" wrapped,
 199+ * causing them to be fragmented. Word fragments are rendered on their own lines, except for their
 200+ * remainder, which is combined with whatever proceeding words can fit on the same line.
135201 *
136 - * @param text {String} Text to render
 202+ * The offset parameter can be used to reduce the amount of work involved in re-rendering the same
 203+ * text, but will be automatically ignored if the text or width of the container has changed.
 204+ *
 205+ * @param offset {Integer} Offset to re-render from, if possible (not yet implemented)
137206 */
138 -TextFlow.prototype.render = function( text ) {
 207+TextFlow.prototype.render = function( offset ) {
139208 /*
140209 * Container measurement
141210 *
@@ -146,107 +215,71 @@
147216 var width = $ruler.innerWidth()
148217 $ruler.remove();
149218
150 - /*
151 - * Word boundary scan
152 - *
153 - * To perform binary-search on words, rather than characters, we need to collect word boundary
154 - * offsets into an array. This list of offsets always starts with 0 and ends with the length of
155 - * the text, e.g. [0, ..., text.length]. The offset of the right side of the breaking character
156 - * is stored, so the gaps between stored offsets always include the breaking character at the
157 - * end.
158 - *
159 - * To avoid encoding the same words as HTML over and over while fitting text to lines, we also
160 - * build a list of HTML encoded strings for each gap between the offsets stored in the
161 - * "boundaries" array. Slices of the "words" array can be joined, producing the encoded HTML of
162 - * the words. In the final pass, each line will get encoded 1 more time, to allow for whitespace
163 - * trimming.
164 - *
165 - * Both "boundaries" and "words" data is kept around between renders.
166 - */
167 - var boundaries = this.boundaries = [],
168 - words = this.words = [],
169 - boundary = /([ \.\,\;\:\-\t\r\n\f])/g,
170 - match,
171 - right,
172 - left = 0;
173 - while ( match = boundary.exec( text ) ) {
174 - // Include the boundary character in the range
175 - right = match.index + 1;
176 - // Store the boundary offset
177 - boundaries.push( right );
178 - // Store the word's encoded HTML
179 - words.push( this.htmlEncode( text.substring( left, right ) ) );
180 - // Remember the previous match
181 - left = right;
 219+ // Ignore offset optimization if the width has changed or the text has never been flowed before
 220+ if (this.width !== width) {
 221+ offset = undefined;
182222 }
183 - // Ensure the "boundaries" array ends in a boundary, which may automatically happen if the text
184 - // ends in a period, for instance, but may not in other cases
185 - if ( right < text.length ) {
186 - boundaries.push( text.length );
187 - words.push( this.htmlEncode( text.substring( right, text.length ) ) );
188 - }
189223
190 - /*
191 - * Line wrapping
192 - *
193 - * Now that we have linear access to the offsets around non-breakable areas within the text, we
194 - * can perform a binary-search for the best fit of words within a line. Line data is kept around
195 - * between renders.
196 - *
197 - * TODO: It may be possible to improve the efficiency of this code by making a best guess and
198 - * working from there, rather than always starting with [i .. boundaries.length], which results
199 - * in reducing the right position in all but the last line, and in most cases 2 or 3 times.
200 - */
 224+ // TODO: Take offset into account and only work from there
 225+
 226+ // Reset lines in the DOM and the "lines" array
201227 this.$.empty();
202 - var lines = this.lines = [],
203 - offset = 0,
204 - subOffset,
205 - start = 0,
206 - end,
207 - fit,
208 - subFit,
 228+ this.lines = [];
 229+ // Iterate over each word that will fit in a line, appending them to the DOM as we go
 230+ var wordOffset = 0,
 231+ lineStart = 0,
 232+ lineEnd,
 233+ wordFit,
 234+ charOffset,
 235+ charFit,
209236 $ruler = $( '<div class="editSurface-line"></div>' ).appendTo( this.$ ),
210 - ruler = $ruler[0],
211 - lineText,
212 - $line;
213 - while ( offset < boundaries.length ) {
214 - fit = this.fitWords( words, offset, ruler, width );
215 - if ( fit.width > width ) {
 237+ ruler = $ruler[0];
 238+ while ( wordOffset < this.boundaries.length ) {
 239+ wordFit = this.fitWords( wordOffset, this.words.length, ruler, width );
 240+ if ( wordFit.width > width ) {
216241 // The first word didn't fit, we need to split it up
217 - subOffset = start;
218 - offset++;
219 - end = boundaries[offset];
 242+ charOffset = lineStart;
 243+ wordOffset++;
 244+ lineEnd = this.boundaries[wordOffset];
220245 do {
221 - subFit = this.fitCharacters( text, subOffset, end, ruler, width );
 246+ charFit = this.fitCharacters( charOffset, lineEnd, ruler, width );
222247 // If we were able to get the rest of the characters on the line OK
223 - if (subFit.end === end) {
 248+ if (charFit.end === lineEnd) {
224249 // Try to fit more words on the line
225 - fit = this.fitWords( words, offset, ruler, width - subFit.width );
226 - if ( fit.end > offset ) {
227 - offset = fit.end - 1;
228 - subFit.end = end = boundaries[offset];
 250+ wordFit = this.fitWords(
 251+ wordOffset, this.words.length, ruler, width - charFit.width
 252+ );
 253+ if ( wordFit.end > wordOffset ) {
 254+ wordOffset = wordFit.end - 1;
 255+ charFit.end = lineEnd = this.boundaries[wordOffset];
229256 }
230257 }
231 - this.appendLine( text, subOffset, subFit.end );
 258+ this.appendLine( charOffset, charFit.end );
232259 // Move on to another line
233 - subOffset = subFit.end;
234 - } while (subOffset < end);
 260+ charOffset = charFit.end;
 261+ } while ( charOffset < lineEnd );
235262 } else {
236 - offset = fit.end - 1;
237 - end = boundaries[offset];
238 - this.appendLine( text, start, end );
 263+ wordOffset = wordFit.end - 1;
 264+ lineEnd = this.boundaries[wordOffset];
 265+ this.appendLine( lineStart, lineEnd );
239266 }
240 - start = end;
241 - offset++;
 267+ lineStart = lineEnd;
 268+ wordOffset++;
242269 }
243270 // Cleanup
244271 $ruler.remove();
245272 };
246273
247 -TextFlow.prototype.appendLine = function( text, start, end ) {
248 - var lineText = text.substring( start, end );
 274+/**
 275+ * Adds a line containing a given range of text to the end of the DOM and the "lines" array.
 276+ *
 277+ * @param start {Integer} Beginning of text range for line
 278+ * @param end {Integer} Ending of text range for line
 279+ */
 280+TextFlow.prototype.appendLine = function( start, end ) {
 281+ var lineText = this.text.substring( start, end );
249282 $line = $( '<div class="editSurface-line" line-index="'
250 - + this.lines.length + '">' + this.htmlEncode( lineText, true ) + '</div>' )
 283+ + this.lines.length + '">' + this.escape( lineText ) + '</div>' )
251284 .appendTo( this.$ );
252285 // Collect line information
253286 this.lines.push({
@@ -259,25 +292,35 @@
260293 };
261294
262295 /**
263 - * Gets the index of the last word that fits inside the line
 296+ * Gets the index of the boundary of last word that fits inside the line
264297 *
265 - * @param words {Array} List of HTML encoded strings, each a word to be fit
266 - * @param offset {Integer} Index within "words" to begin fitting from
267 - * @param line {HTMLElement} Element to take measurements with
 298+ * The "words" and "boundaries" arrays provide linear access to the offsets around non-breakable
 299+ * areas within the text. Using these, we can perform a binary-search for the best fit of words
 300+ * within a line, just as we would with characters.
 301+ *
 302+ * Results are given as an object containing both an index and a width, the later of which can be
 303+ * used to detect when the first word was too long to fit on a line. In such cases the result will
 304+ * contain the index of the boundary of the first word and it's width.
 305+ *
 306+ * TODO: Because limit is most likely given as "words.length", it may be possible to improve the
 307+ * efficiency of this code by making a best guess and working from there, rather than always
 308+ * starting with [offset .. limit], which usually results in reducing the end position in all but
 309+ * the last line, and in most cases more than 3 times, before changing directions.
 310+ *
 311+ * @param start {Integer} Index within "words" to begin fitting from
 312+ * @param end {Integer} Index within "words" to stop fitting to
 313+ * @param ruler {HTMLElement} Element to take measurements with
268314 * @param width {Integer} Maximum width to allow the line to extend to
269315 * @return {Integer} Last index within "words" that contains a word that fits
270 - * @return {Null} If not even the first word can fit
271316 */
272 -TextFlow.prototype.fitWords = function( words, offset, ruler, width ) {
273 - var start = offset,
274 - end = words.length,
275 - middle,
276 - finalWidth;
 317+TextFlow.prototype.fitWords = function( start, end, ruler, width ) {
 318+ var offset = start,
 319+ middle;
277320 do {
278321 // Place "middle" directly in the center of "start" and "end"
279322 middle = Math.ceil( ( start + end ) / 2 );
280 - // Prepare the line for measurement using pre-encoded HTML
281 - ruler.innerHTML = words.slice( offset, middle ).join( '' );
 323+ // Prepare the line for measurement using pre-escaped HTML
 324+ ruler.innerHTML = this.words.slice( offset, middle ).join( '' );
282325 // Test for over/under using width of the rendered line
283326 if ( ruler.clientWidth > width ) {
284327 // Detect impossible fit (the first word won't fit by itself)
@@ -293,20 +336,31 @@
294337 }
295338 } while ( start < end );
296339 // Final measurment
297 - ruler.innerHTML = words.slice( offset, start ).join( '' );
 340+ ruler.innerHTML = this.words.slice( offset, start ).join( '' );
298341 return { 'end': start, 'width': ruler.clientWidth };
299342 };
300343
301 -TextFlow.prototype.fitCharacters = function( text, offset, limit, ruler, width ) {
302 - var start = offset,
303 - end = limit,
304 - middle,
305 - finalWidth;
 344+/**
 345+ * Gets the index of the boundary of the last character that fits inside the line
 346+ *
 347+ * Results are given as an object containing both an index and a width, the later of which can be
 348+ * used to detect when the first character was too long to fit on a line. In such cases the result
 349+ * will contain the index of the first character and it's width.
 350+ *
 351+ * @param start {Integer} Index within "text" to begin fitting from
 352+ * @param end {Integer} Index within "text" to stop fitting to
 353+ * @param ruler {HTMLElement} Element to take measurements with
 354+ * @param width {Integer} Maximum width to allow the line to extend to
 355+ * @return {Integer} Last index within "text" that contains a character that fits
 356+ */
 357+TextFlow.prototype.fitCharacters = function( start, end, ruler, width ) {
 358+ var offset = start,
 359+ middle;
306360 do {
307361 // Place "middle" directly in the center of "start" and "end"
308362 middle = Math.ceil( ( start + end ) / 2 );
309 - // Fill the line with a portion of the text, encoded as HTML
310 - ruler.innerHTML = this.htmlEncode( text.substring( offset, middle ) );
 363+ // Fill the line with a portion of the text, escaped as HTML
 364+ ruler.innerHTML = this.escape( this.text.substring( offset, middle ) );
311365 // Test for over/under using width of the rendered line
312366 if ( ruler.clientWidth > width ) {
313367 // Detect impossible fit (the first character won't fit by itself)
@@ -322,6 +376,6 @@
323377 }
324378 } while ( start < end );
325379 // Final measurement
326 - ruler.innerHTML = this.htmlEncode( text.substring( offset, start ) );
 380+ ruler.innerHTML = this.escape( this.text.substring( offset, start ) );
327381 return { 'end': start, 'width': ruler.clientWidth };
328382 };
Index: trunk/parsers/wikidom/demos/es/index.html
@@ -6,7 +6,6 @@
77 <link rel="stylesheet" href="../../lib/es/es.Surface.css">
88 </head>
99 <body>
10 - <h1>EditSurface Demo</h1>
1110 <div id="es"></div>
1211
1312 <!-- EditSurface -->

Status & tagging log