// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. // @dart = 2.9 part of view; // TODO(jacobr): handle splitting lines on symbols such as '-' that aren't // whitespace but are valid word breaking points. /** * Utility class to efficiently word break and measure text without requiring * access to the DOM. */ class MeasureText { static CanvasRenderingContext2D _context; final String font; num _spaceLength; num _typicalCharLength; static const String ELLIPSIS = '...'; MeasureText(this.font) { if (_context == null) { CanvasElement canvas = new Element.tag('canvas'); _context = canvas.getContext('2d'); } if (_spaceLength == null) { _context.font = font; _spaceLength = _context.measureText(' ').width; _typicalCharLength = _context.measureText('k').width; } } // TODO(jacobr): we are DOA for i18N... // the right solution is for the server to send us text perparsed into words // perhaps even with hints on the guess for the correct breaks so on the // client all we have to do is verify and fix errors rather than perform the // full calculation. static bool isWhitespace(String character) { return character == ' ' || character == '\t' || character == '\n'; } num get typicalCharLength { return _typicalCharLength; } String quickTruncate(String text, num lineWidth, int maxLines) { int targetLength = lineWidth * maxLines ~/ _typicalCharLength; // Advance to next word break point. while (targetLength < text.length && !isWhitespace(text[targetLength])) { targetLength++; } if (targetLength < text.length) { return '${text.substring(0, targetLength)}$ELLIPSIS'; } else { return text; } } /** * Add line broken text as html separated by
elements. * Returns the number of lines in the output. * This function is safe to call with [:sb == null:] in which case just the * line count is returned. */ int addLineBrokenText( StringBuffer sb, String text, num lineWidth, int maxLines) { // Strip surrounding whitespace. This ensures we create zero lines if there // is no visible text. text = text.trim(); // We can often avoid performing a full line break calculation when only // the number of lines and not the actual linebreaks is required. if (sb == null) { _context.font = font; int textWidth = _context.measureText(text).width.toInt(); // By the pigeon hole principle, the resulting text will require at least // maxLines if the raw text is longer than the amount of text that will // fit on maxLines - 1. We add the length of a whitespace // character to the lineWidth as each line is separated by a whitespace // character. We assume all whitespace characters have the same length. if (textWidth >= (lineWidth + _spaceLength) * (maxLines - 1)) { return maxLines; } else if (textWidth == 0) { return 0; } else if (textWidth < lineWidth) { return 1; } // Fall through to the regular line breaking calculation as the number // of lines required is unclear. } int lines = 0; lineBreak(text, lineWidth, maxLines, (int start, int end, num width) { lines++; if (lines == maxLines) { // Overflow case... there may be more lines of text than we can handle. // Add a few characters to the last line so that the browser will // render ellipses correctly. // TODO(jacobr): make this optional and only add characters until // the first whitespace character encountered. end = Math.min(end + 50, text.length); } if (sb != null) { if (lines > 1) { sb.write('
'); } // TODO(jacobr): HTML escape this text. sb.write(text.substring(start, end)); } }); return lines; } void lineBreak(String text, num lineWidth, int maxLines, Function callback) { _context.font = font; int lines = 0; num currentLength = 0; int startIndex = 0; int wordStartIndex = null; int lastWordEndIndex = null; bool lastWhitespace = true; // TODO(jacobr): optimize this further. // To simplify the logic, we simulate injecting a whitespace character // at the end of the string. for (int i = 0, len = text.length; i <= len; i++) { // Treat the char after the end of the string as whitespace. bool whitespace = i == len || isWhitespace(text[i]); if (whitespace && !lastWhitespace) { num wordLength = _context.measureText(text.substring(wordStartIndex, i)).width; // TODO(jimhug): Replace the line above with this one to workaround // dartium bug - error: unimplemented code // num wordLength = (i - wordStartIndex) * 17; currentLength += wordLength; if (currentLength > lineWidth) { // Edge case: // It could be the very first word we ran into was too long for a // line in which case we let it have its own line. if (lastWordEndIndex != null) { lines++; callback(startIndex, lastWordEndIndex, currentLength - wordLength); } if (lines == maxLines) { return; } startIndex = wordStartIndex; currentLength = wordLength; } lastWordEndIndex = i; currentLength += _spaceLength; wordStartIndex = null; } else if (wordStartIndex == null && !whitespace) { wordStartIndex = i; } lastWhitespace = whitespace; } if (currentLength > 0) { callback(startIndex, text.length, currentLength); } } }