diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eaf0cd..9a570a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,25 @@ +## 2.0.0-dev + +- Minimal SDK version: 3.0.0. +- Refactoring. + ## 1.2.0 - Minimal SDK version: 2.18.0. - Refactoring. - ## 1.1.0 - `Pattern.split` no empty trailing strings. ## 1.0.2 -- internal changes. +- Internal changes. ## 1.0.1 -- update meta & dart format. +- Update meta. ## 1.0.0 -- initial release. +- Initial release. diff --git a/lib/src/extensions.dart b/lib/src/extensions.dart new file mode 100644 index 0000000..eea8ccf --- /dev/null +++ b/lib/src/extensions.dart @@ -0,0 +1,66 @@ +/// Extends [Pattern]. +extension PatternUtils on Pattern { + /// Split string by the occurrences of pattern. + List split(String text) { + var result = []; + var start = 0, end = 0; + + for (var match in allMatches(text)) { + end = match.start; + + if (start < end) { + result.add(text.substring(start, end)); + } + + for (var i = 0, count = match.groupCount; i < count; i++) { + result.add(match.group(i + 1)!); + } + + start = match.end; + } + + if (start < text.length) { + result.add(text.substring(start)); + } + + return result; + } +} + +/// Extends [String]. +extension StringUtils on String { + /// Return a copy where all tab characters are expanded using spaces. + String expandTabs([int tabSize = 8]) { + var buffer = StringBuffer(); + var units = runes.toList(); + var length = units.length; + + for (var i = 0, line = 0; i < length; i += 1, line += 1) { + var char = units[i]; + + if (char == 13 || char == 10) { + line = -1; + buffer.writeCharCode(char); + } else if (char == 9) { + var size = tabSize - (line % tabSize); + buffer.write(' ' * size); + line = -1; + } else { + buffer.writeCharCode(char); + } + } + + return buffer.toString(); + } + + /// Replace each character in the string using the given translation table. + String translate(Map table) { + var buffer = StringBuffer(); + + for (var rune in runes) { + buffer.writeCharCode(table.containsKey(rune) ? table[rune]! : rune); + } + + return buffer.toString(); + } +} diff --git a/lib/src/functions.dart b/lib/src/functions.dart new file mode 100644 index 0000000..c95c2bb --- /dev/null +++ b/lib/src/functions.dart @@ -0,0 +1,206 @@ +import 'package:textwrap/src/extensions.dart'; +import 'package:textwrap/src/patterns.dart'; + +void fixEndings(List chunks) { + for (var i = 0; i < chunks.length - 1;) { + var chunk = chunks[i]; + var length = chunk.length; + var input = length > 2 ? chunk.substring(length - 2) : chunk; + + if (chunks[i + 1] == ' ' && sentenceEndRe.hasMatch(input)) { + chunks[i + 1] = ' '; + i += 2; + } else { + i += 1; + } + } +} + +void handleLongWord( + List reversedChunks, + List currentLine, + int currentLength, + int width, { + bool breakLongWords = true, + bool breakOnHyphens = true, +}) { + var spaceLeft = width < 1 ? 1 : width - currentLength; + + if (breakLongWords) { + var chunk = reversedChunks.last; + var end = spaceLeft; + + if (breakOnHyphens && chunk.length > spaceLeft) { + var hyphen = chunk.lastIndexOf('-'); + + if (hyphen > 0 && chunk.substring(0, hyphen).contains('-')) { + end = hyphen + 1; + } + } + + currentLine.add(chunk.substring(0, end)); + reversedChunks[reversedChunks.length - 1] = chunk.substring(end); + } else if (currentLine.isEmpty) { + currentLine.add(reversedChunks.removeLast()); + } +} + +String mungeWhitespace( + String text, { + bool expandTabs = true, + int tabSize = 8, + bool replaceWhitespace = true, +}) { + if (expandTabs) { + text = text.expandTabs(tabSize); + } + + if (replaceWhitespace) { + text = text.translate(const { + 9: 32, + 10: 32, + 11: 32, + 12: 32, + 13: 32, + }); + } + + return text; +} + +List split(String text, {bool breakOnHyphens = true}) { + List chunks; + + if (breakOnHyphens) { + chunks = wordSeparatorRe.split(text); + } else { + chunks = wordSeparatorSimpleRe.split(text); + } + + return chunks; +} + +List splitChunks( + String text, { + bool expandTabs = true, + bool replaceWhitespace = true, + bool breakOnHyphens = true, + int tabSize = 8, +}) { + text = mungeWhitespace(text, + expandTabs: expandTabs, + replaceWhitespace: replaceWhitespace, + tabSize: tabSize); + return split(text, breakOnHyphens: breakOnHyphens); +} + +List wrapChunks( + List chunks, { + int width = 70, + String initialIndent = '', + String subsequentIndent = '', + bool breakLongWords = true, + bool dropWhitespace = true, + bool breakOnHyphens = true, + int maxLines = -1, + String placeholder = ' ...', +}) { + if (width < 0) { + throw Exception('Invalid width $width (must be > 0).'); + } + + var lines = []; + + if (maxLines != -1) { + var indent = maxLines > 1 ? subsequentIndent : initialIndent; + + if (indent.length + placeholder.trimLeft().length > width) { + throw StateError('Placeholder too large for max width.'); + } + } + + chunks = chunks.reversed.toList(); + + while (chunks.isNotEmpty) { + var currentLine = []; + var currentLength = 0; + + var indent = lines.isNotEmpty ? subsequentIndent : initialIndent; + var contentWidth = width - indent.length; + + if (dropWhitespace && chunks.last.trim().isEmpty && lines.isNotEmpty) { + chunks.removeLast(); + } + + while (chunks.isNotEmpty) { + var length = chunks.last.length; + + if (currentLength + length <= contentWidth) { + currentLine.add(chunks.removeLast()); + currentLength += length; + } else { + break; + } + } + + if (chunks.isNotEmpty && chunks.last.length > contentWidth) { + handleLongWord(chunks, currentLine, currentLength, contentWidth, + breakLongWords: breakLongWords, breakOnHyphens: breakOnHyphens); + currentLength = 0; + + for (var line in currentLine) { + currentLength += line.length; + } + } + + if (dropWhitespace && + currentLine.isNotEmpty && + currentLine.last.trim().isEmpty) { + var last = currentLine.removeLast(); + currentLength -= last.length; + } + + if (currentLine.isNotEmpty) { + if (maxLines == -1 || + lines.length + 1 < maxLines || + (chunks.isEmpty || + dropWhitespace && + chunks.length == 1 && + chunks.first.trim().isEmpty) && + currentLength <= width) { + lines.add(indent + currentLine.join()); + } else { + var not = true; + + while (currentLine.isNotEmpty) { + if (currentLine.last.trim().isNotEmpty && + currentLength + placeholder.length <= contentWidth) { + currentLine.add(placeholder); + lines.add(indent + currentLine.join()); + not = false; + break; + } + + var last = currentLine.removeLast(); + currentLength -= last.length; + } + + if (not) { + if (lines.isNotEmpty) { + var previousLine = lines.last.trimRight(); + + if (previousLine.length + placeholder.length <= contentWidth) { + lines[lines.length - 1] = previousLine + placeholder; + } + + lines.add(indent + placeholder.trimLeft()); + } + } + + break; + } + } + } + + return lines; +} diff --git a/lib/src/patterns.dart b/lib/src/patterns.dart new file mode 100644 index 0000000..55f5103 --- /dev/null +++ b/lib/src/patterns.dart @@ -0,0 +1,10 @@ +final RegExp spaceRe = RegExp('\\s+'); + +final RegExp sentenceEndRe = RegExp('\\w[\\.\\!\\?][\\"\']?\$'); + +final RegExp wordSeparatorSimpleRe = RegExp('([\t\n\v\r ])+'); + +final RegExp wordSeparatorRe = RegExp( + '([\t\n\v\r ]+|(?<=[\\w!"\'&.,?])-{2,}(?=\\w)|[^\t\n\v\r ]+?' + '(?:-(?:(?<=[^\\d\\W]{2}-)|(?<=[^\\d\\W]-[^\\d\\W]-))(?=[^\\d\\W]-?[^\\d\\W])|' + '(?=[\t\n\v\r ]|\$)|(?<=[\\w!"\'&.,?])(?=-{2,}\\w)))'); diff --git a/lib/textwrap.dart b/lib/textwrap.dart index 7d588e8..1fee9d6 100644 --- a/lib/textwrap.dart +++ b/lib/textwrap.dart @@ -1,6 +1,5 @@ -import 'package:meta/meta.dart'; - -import 'utils.dart'; +import 'package:textwrap/src/functions.dart'; +import 'package:textwrap/src/patterns.dart'; /// Wrap a single paragraph of text. List wrap( @@ -16,24 +15,27 @@ List wrap( bool breakOnHyphens = true, int tabSize = 8, int maxLines = -1, - String placeholder = ' ...', + String placeholder = '...', }) { - var wrapper = TextWrapper( - width: width, - initialIndent: initialIndent, - subsequentIndent: subsequentIndent, - expandTabs: expandTabs, - replaceWhitespace: replaceWhitespace, - fixSentenceEndings: fixSentenceEndings, - breakLongWords: breakLongWords, - dropWhitespace: dropWhitespace, - breakOnHyphens: breakOnHyphens, - tabSize: tabSize, - maxLines: maxLines, - placeholder: placeholder, - ); + var chunks = splitChunks(text, + breakOnHyphens: breakLongWords, + expandTabs: expandTabs, + tabSize: tabSize, + replaceWhitespace: replaceWhitespace); + + if (fixSentenceEndings) { + fixEndings(chunks); + } - return wrapper.wrap(text); + return wrapChunks(chunks, + width: width, + initialIndent: initialIndent, + subsequentIndent: subsequentIndent, + breakLongWords: breakLongWords, + dropWhitespace: dropWhitespace, + breakOnHyphens: breakOnHyphens, + maxLines: maxLines, + placeholder: placeholder); } /// Fill a single paragraph of text. @@ -50,24 +52,22 @@ String fill( bool breakOnHyphens = true, int tabSize = 8, int maxLines = -1, - String placeholder = ' ...', + String placeholder = '...', }) { - var wrapper = TextWrapper( - width: width, - initialIndent: initialIndent, - subsequentIndent: subsequentIndent, - expandTabs: expandTabs, - replaceWhitespace: replaceWhitespace, - fixSentenceEndings: fixSentenceEndings, - breakLongWords: breakLongWords, - dropWhitespace: dropWhitespace, - breakOnHyphens: breakOnHyphens, - tabSize: tabSize, - maxLines: maxLines, - placeholder: placeholder, - ); - - return wrapper.fill(text); + return wrap(text, + width: width, + initialIndent: initialIndent, + subsequentIndent: subsequentIndent, + expandTabs: expandTabs, + replaceWhitespace: replaceWhitespace, + fixSentenceEndings: fixSentenceEndings, + breakLongWords: breakLongWords, + dropWhitespace: dropWhitespace, + breakOnHyphens: breakOnHyphens, + tabSize: tabSize, + maxLines: maxLines, + placeholder: placeholder) + .join('\n'); } /// Collapse and truncate the given text to fit in the given width. @@ -86,49 +86,36 @@ String shorten( int tabSize = 8, String placeholder = ' ...', }) { - var wrapper = TextWrapper( - width: width, - initialIndent: initialIndent, - subsequentIndent: subsequentIndent, - expandTabs: expandTabs, - replaceWhitespace: replaceWhitespace, - fixSentenceEndings: fixSentenceEndings, - breakLongWords: breakLongWords, - dropWhitespace: dropWhitespace, - breakOnHyphens: breakOnHyphens, - tabSize: tabSize, - maxLines: maxLines, - placeholder: placeholder, - ); - return wrapper.fill(text.split(RegExp('\\s+')).join(' ')); + return fill(text.split(spaceRe).join(' '), + width: width, + initialIndent: initialIndent, + subsequentIndent: subsequentIndent, + expandTabs: expandTabs, + replaceWhitespace: replaceWhitespace, + fixSentenceEndings: fixSentenceEndings, + breakLongWords: breakLongWords, + dropWhitespace: dropWhitespace, + breakOnHyphens: breakOnHyphens, + tabSize: tabSize, + maxLines: maxLines, + placeholder: placeholder); } class TextWrapper { - static final RegExp _wordSeparatorRe = RegExp( - '([\t\n\v\r ]+|(?<=[\\w!"\'&.,?])-{2,}(?=\\w)|[^\t\n\v\r ]+?' - '(?:-(?:(?<=[^\\d\\W]{2}-)|(?<=[^\\d\\W]-[^\\d\\W]-))(?=[^\\d\\W]-?[^\\d\\W])|' - '(?=[\t\n\v\r ]|\$)|(?<=[\\w!"\'&.,?])(?=-{2,}\\w)))'); - - static final RegExp _wordSeparatorSimpleRe = RegExp('([\t\n\v\r ])+'); - - static final RegExp _sentenceEndRe = RegExp('\\w[\\.\\!\\?][\\"\']?\$'); - - TextWrapper( - {this.width = 70, - this.initialIndent = '', - this.subsequentIndent = '', - this.expandTabs = true, - this.replaceWhitespace = true, - this.fixSentenceEndings = false, - this.breakLongWords = true, - this.dropWhitespace = true, - this.breakOnHyphens = true, - this.tabSize = 8, - this.maxLines = -1, - this.placeholder = ' ...'}) - : wordSeparatorRe = _wordSeparatorRe, - wordSeparatorSimpleRe = _wordSeparatorSimpleRe, - sentenceEndRe = _sentenceEndRe; + TextWrapper({ + this.width = 70, + this.initialIndent = '', + this.subsequentIndent = '', + this.expandTabs = true, + this.replaceWhitespace = true, + this.fixSentenceEndings = false, + this.breakLongWords = true, + this.dropWhitespace = true, + this.breakOnHyphens = true, + this.tabSize = 8, + this.maxLines = -1, + this.placeholder = ' ...', + }); final int width; @@ -154,209 +141,33 @@ class TextWrapper { final String placeholder; - final RegExp wordSeparatorRe; - - final RegExp wordSeparatorSimpleRe; - - final RegExp sentenceEndRe; - - @protected - String mungeWhitespace(String text) { - if (expandTabs) { - text = text.expandTabs(tabSize); - } - - if (replaceWhitespace) { - text = text.translate(const { - 9: 32, - 10: 32, - 11: 32, - 12: 32, - 13: 32, - }); - } - - return text; - } - - @protected - List split(String text) { - List chunks; - - if (breakOnHyphens) { - chunks = wordSeparatorRe.split(text); - } else { - chunks = wordSeparatorSimpleRe.split(text); - } - - return chunks; - } - - @protected - void fixEndings(List chunks) { - for (var i = 0; i < chunks.length - 1;) { - var chunk = chunks[i]; - var length = chunk.length; - var input = length > 2 ? chunk.substring(length - 2) : chunk; - - if (chunks[i + 1] == ' ' && sentenceEndRe.hasMatch(input)) { - chunks[i + 1] = ' '; - i += 2; - } else { - i += 1; - } - } - } - - @protected - void handleLongWord( - List reversedChunks, - List currentLine, - int currentLength, - int width, - ) { - var spaceLeft = width < 1 ? 1 : width - currentLength; - - if (breakLongWords) { - var chunk = reversedChunks.last; - var end = spaceLeft; - - if (breakOnHyphens && chunk.length > spaceLeft) { - var hyphen = chunk.lastIndexOf('-'); - - if (hyphen > 0 && chunk.substring(0, hyphen).contains('-')) { - end = hyphen + 1; - } - } - - currentLine.add(chunk.substring(0, end)); - reversedChunks[reversedChunks.length - 1] = chunk.substring(end); - } else if (currentLine.isEmpty) { - currentLine.add(reversedChunks.removeLast()); - } - } - - @protected - List wrapChunks(List chunks) { - if (width < 0) { - throw Exception('Invalid width $width (must be > 0).'); - } - - var lines = []; - - if (maxLines != -1) { - var indent = maxLines > 1 ? subsequentIndent : initialIndent; - - if (indent.length + placeholder.trimLeft().length > width) { - throw Exception('Placeholder too large for max width.'); - } - } - - chunks = chunks.reversed.toList(); - - while (chunks.isNotEmpty) { - var currentLine = []; - var currentLength = 0; - - var indent = lines.isNotEmpty ? subsequentIndent : initialIndent; - var width = this.width - indent.length; - - if (dropWhitespace && chunks.last.trim().isEmpty && lines.isNotEmpty) { - chunks.removeLast(); - } - - while (chunks.isNotEmpty) { - var length = chunks.last.length; - - if (currentLength + length <= width) { - currentLine.add(chunks.removeLast()); - currentLength += length; - } else { - break; - } - } - - if (chunks.isNotEmpty && chunks.last.length > width) { - handleLongWord(chunks, currentLine, currentLength, width); - currentLength = 0; - - for (var line in currentLine) { - currentLength += line.length; - } - } - - if (dropWhitespace && - currentLine.isNotEmpty && - currentLine.last.trim().isEmpty) { - var last = currentLine.removeLast(); - currentLength -= last.length; - } - - if (currentLine.isNotEmpty) { - if (maxLines == -1 || - lines.length + 1 < maxLines || - (chunks.isEmpty || - dropWhitespace && - chunks.length == 1 && - chunks.first.trim().isEmpty) && - currentLength <= width) { - lines.add(indent + currentLine.join()); - } else { - var not = true; - - while (currentLine.isNotEmpty) { - if (currentLine.last.trim().isNotEmpty && - currentLength + placeholder.length <= width) { - currentLine.add(placeholder); - lines.add(indent + currentLine.join()); - not = false; - break; - } - - var last = currentLine.removeLast(); - currentLength -= last.length; - } - - if (not) { - if (lines.isNotEmpty) { - var previousLine = lines.last.trimRight(); - - if (previousLine.length + placeholder.length <= width) { - lines[lines.length - 1] = previousLine + placeholder; - } - - lines.add(indent + placeholder.trimLeft()); - } - } - - break; - } - } - } - - return lines; - } - - @protected - List splitChunks(String text) { - text = mungeWhitespace(text); - return split(text); - } - - /// Reformat the single paragraph in 'text' so it fits in lines of no more than 'self.width' columns, - /// and return a list of wrapped lines. + /// Reformat the single paragraph in [text] so it fits in lines of no more + /// than [width] columns, and return a list of wrapped lines. List wrap(String text) { - var chunks = splitChunks(text); + var chunks = splitChunks(text, + breakOnHyphens: breakLongWords, + expandTabs: expandTabs, + tabSize: tabSize, + replaceWhitespace: replaceWhitespace); if (fixSentenceEndings) { fixEndings(chunks); } - return wrapChunks(chunks); + return wrapChunks(chunks, + width: width, + initialIndent: initialIndent, + subsequentIndent: subsequentIndent, + breakLongWords: breakLongWords, + dropWhitespace: dropWhitespace, + breakOnHyphens: breakOnHyphens, + maxLines: maxLines, + placeholder: placeholder); } - /// Reformat the single paragraph in 'text' to fit in lines of no more than 'self.width' columns, - /// and return a new string containing the entire wrapped paragraph. + /// Reformat the single paragraph in [text] to fit in lines of no more than + /// [width] columns, and return a new string containing the entire wrapped + /// ßparagraph. String fill(String text) { return wrap(text).join('\n'); } diff --git a/lib/utils.dart b/lib/utils.dart index eea8ccf..480584f 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -1,66 +1 @@ -/// Extends [Pattern]. -extension PatternUtils on Pattern { - /// Split string by the occurrences of pattern. - List split(String text) { - var result = []; - var start = 0, end = 0; - - for (var match in allMatches(text)) { - end = match.start; - - if (start < end) { - result.add(text.substring(start, end)); - } - - for (var i = 0, count = match.groupCount; i < count; i++) { - result.add(match.group(i + 1)!); - } - - start = match.end; - } - - if (start < text.length) { - result.add(text.substring(start)); - } - - return result; - } -} - -/// Extends [String]. -extension StringUtils on String { - /// Return a copy where all tab characters are expanded using spaces. - String expandTabs([int tabSize = 8]) { - var buffer = StringBuffer(); - var units = runes.toList(); - var length = units.length; - - for (var i = 0, line = 0; i < length; i += 1, line += 1) { - var char = units[i]; - - if (char == 13 || char == 10) { - line = -1; - buffer.writeCharCode(char); - } else if (char == 9) { - var size = tabSize - (line % tabSize); - buffer.write(' ' * size); - line = -1; - } else { - buffer.writeCharCode(char); - } - } - - return buffer.toString(); - } - - /// Replace each character in the string using the given translation table. - String translate(Map table) { - var buffer = StringBuffer(); - - for (var rune in runes) { - buffer.writeCharCode(table.containsKey(rune) ? table[rune]! : rune); - } - - return buffer.toString(); - } -} +export 'package:textwrap/src/extensions.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 28915f1..81abe8f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,16 +1,16 @@ name: textwrap -version: 1.2.0 +version: 2.0.0-dev description: Text wrapping and filling. It's a pure port of textwrap from Python. repository: https://github.com/ykmnkmi/textwrap.dart funding: - https://www.buymeacoffee.com/ykmnkmi environment: - sdk: ">=2.18.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: - meta: ^1.8.0 + meta: ^1.9.0 dev_dependencies: - lints: ^2.0.1 - test: ^1.22.1 + lints: ^2.1.0 + test: ^1.24.0