word-wrap-0.4.1/0000755000000000000000000000000013142164773011634 5ustar0000000000000000word-wrap-0.4.1/CHANGELOG.md0000644000000000000000000000441013142164773013444 0ustar0000000000000000 0.4.1 ===== Bug fixes: * Fixed a bug that caused breakTokens to diverge for lines with indentation longer than the indentation width when preserveIndentation was enabled. (Thanks Callum Oakley.) The resulting fix does the following: * When breakLongWords is enabled, this change reduces the indentation of the indented lines to result in lines that are no longer than the wrap limit, so they will have reduced indentation and word fragments. This is a trade-off with other options that are open to evaluation. * When breakLongWords is disabled, this change reduces the indentation of the indented lines and leaves whole words, unbroken, on them, resulting in lines that are longer than the indentation limit. This behavior is similar to non-indented lines with over-long tokens. This is also a trade-off with other options that are open to evaluation. 0.4 === Bug fixes: * Fixed a bug where each line was being wrapped after every word because only one case in breakTokens was reached (thanks Callum Oakley) Package changes: * Added a simple benchmark suite 0.3.3 ===== Bug fixes: * Fixed accidental breaking of long tokens when they could be wrapped instead. 0.3.2 ===== Bug fixes: * Fixed a bug that prevented wrapping sometimes. 0.3.1 ===== Bug fixes: * Fix inconsistent long token breaking (long tokens anywhere but the beginning of a line) 0.3 === API changes: * Added the breakLongWords setting to WrapSettings. This setting makes it possible to cause words to get broken up over multiple lines if their lengths exceed the wrapping width. 0.2 === API changes: * Added a WrapSettings data type for controlling wrapping behavior. * All functions now require a WrapSettings. * Added defaultWrapSettings for prior behavior. * Wrap settings now include a setting to control how indentation is preserved in broken lines. Bug fixes: * Lines with only whitespace are preserved as empty lines. 0.1.2 ===== Bug fixes: * Fixed a bug where multiple consecutive newlines were not properly preserved as advertised (#2) 0.1.1 ===== Package changes: * Removed a duplicate mention of the changelog file in the cabal package description that used the wrong filename case (#1) 0.1 === * First version. word-wrap-0.4.1/LICENSE0000644000000000000000000000277613142164773012655 0ustar0000000000000000Copyright (c) 2017, Jonathan Daugherty All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Jonathan Daugherty nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. word-wrap-0.4.1/README.md0000644000000000000000000000011013142164773013103 0ustar0000000000000000word-wrap ========= This library provides text-wrapping functionality. word-wrap-0.4.1/Setup.hs0000644000000000000000000000005613142164773013271 0ustar0000000000000000import Distribution.Simple main = defaultMain word-wrap-0.4.1/word-wrap.cabal0000644000000000000000000000277013142164773014550 0ustar0000000000000000name: word-wrap version: 0.4.1 synopsis: A library for word-wrapping description: A library for wrapping long lines of text. license: BSD3 license-file: LICENSE author: Jonathan Daugherty maintainer: cygnus@foobox.com copyright: 2017 Jonathan Daugherty category: Text build-type: Simple cabal-version: >=1.18 Homepage: https://github.com/jtdaugherty/word-wrap/ Bug-reports: https://github.com/jtdaugherty/word-wrap/issues extra-doc-files: README.md CHANGELOG.md Source-Repository head type: git location: git://github.com/jtdaugherty/word-wrap.git library exposed-modules: Text.Wrap hs-source-dirs: src default-language: Haskell2010 ghc-options: -Wall build-depends: base < 5, text benchmark word-wrap-benchmarks type: exitcode-stdio-1.0 default-language: Haskell2010 hs-source-dirs: benchmarks main-is: Main.hs ghc-options: -Wall build-depends: base < 5, word-wrap, criterion, text test-suite word-wrap-tests type: exitcode-stdio-1.0 default-language: Haskell2010 hs-source-dirs: tests main-is: Main.hs ghc-options: -Wall build-depends: base < 5, word-wrap, hspec >= 2.4 word-wrap-0.4.1/benchmarks/0000755000000000000000000000000013142164773013751 5ustar0000000000000000word-wrap-0.4.1/benchmarks/Main.hs0000644000000000000000000000230413142164773015170 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} module Main where import Criterion.Main import qualified Data.Text as T import Text.Wrap smallDocument :: T.Text smallDocument = T.unlines $ replicate 10 $ T.concat $ replicate 1000 "foobar stuff things x " mediumDocument :: T.Text mediumDocument = T.unlines $ replicate 100 $ T.concat $ replicate 1000 "foobar stuff things x " largeDocument :: T.Text largeDocument = T.unlines $ replicate 1000 $ T.concat $ replicate 1000 "foobar stuff things x " breakSettings :: WrapSettings breakSettings = defaultWrapSettings { breakLongWords = True } cases :: [Benchmark] cases = [ bench "small_default" $ nf (wrapTextToLines defaultWrapSettings 10) smallDocument , bench "medium_default" $ nf (wrapTextToLines defaultWrapSettings 10) mediumDocument , bench "large_deafult" $ nf (wrapTextToLines defaultWrapSettings 10) largeDocument , bench "small_break" $ nf (wrapTextToLines breakSettings 5) smallDocument , bench "medium_break" $ nf (wrapTextToLines breakSettings 5) mediumDocument , bench "large_break" $ nf (wrapTextToLines breakSettings 5) largeDocument ] main :: IO () main = defaultMain cases word-wrap-0.4.1/src/0000755000000000000000000000000013142164773012423 5ustar0000000000000000word-wrap-0.4.1/src/Text/0000755000000000000000000000000013142164773013347 5ustar0000000000000000word-wrap-0.4.1/src/Text/Wrap.hs0000644000000000000000000001170013142164773014613 0ustar0000000000000000module Text.Wrap ( WrapSettings(..) , defaultWrapSettings , wrapTextToLines , wrapText ) where import Data.Monoid ((<>)) import Data.Char (isSpace) import qualified Data.Text as T -- | Settings to control how wrapping is performed. data WrapSettings = WrapSettings { preserveIndentation :: Bool -- ^ Whether to indent new lines created by wrapping -- when their original line was indented. , breakLongWords :: Bool -- ^ Whether to break in the middle of the first word -- on a line when that word exceeds the wrapping width. } deriving (Eq, Show, Read) defaultWrapSettings :: WrapSettings defaultWrapSettings = WrapSettings { preserveIndentation = False , breakLongWords = False } -- | Wrap text at the specified width. Newlines and whitespace in the -- input text are preserved. Returns the lines of text in wrapped form. -- New lines introduced due to wrapping will have leading whitespace -- stripped. wrapTextToLines :: WrapSettings -> Int -> T.Text -> [T.Text] wrapTextToLines settings amt s = concat $ fmap (wrapLine settings amt) $ T.lines s -- | Like 'wrapTextToLines', but returns the wrapped text reconstructed -- with newlines inserted at wrap points. wrapText :: WrapSettings -> Int -> T.Text -> T.Text wrapText settings amt s = T.intercalate (T.pack "\n") $ wrapTextToLines settings amt s data Token = WS T.Text | NonWS T.Text deriving (Show) tokenLength :: Token -> Int tokenLength = T.length . tokenContent tokenContent :: Token -> T.Text tokenContent (WS t) = t tokenContent (NonWS t) = t -- | Tokenize text into whitespace and non-whitespace chunks. tokenize :: T.Text -> [Token] tokenize t | T.null t = [] tokenize t = let leadingWs = T.takeWhile isSpace t leadingNonWs = T.takeWhile (not . isSpace) t tok = if T.null leadingWs then NonWS leadingNonWs else WS leadingWs in tok : tokenize (T.drop (tokenLength tok) t) -- | Wrap a single line of text into a list of lines that all satisfy -- the wrapping width. wrapLine :: WrapSettings -- ^ Settings. -> Int -- ^ The wrapping width. -> T.Text -- ^ A single line of text. -> [T.Text] wrapLine settings limit t = let go _ [] = [T.empty] go _ [WS _] = [T.empty] go lim ts = let (firstLine, maybeRest) = breakTokens settings lim ts firstLineText = T.stripEnd $ T.concat $ fmap tokenContent firstLine in case maybeRest of Nothing -> [firstLineText] Just rest -> firstLineText : go lim rest (indent, modifiedText) = if preserveIndentation settings then let i = T.takeWhile isSpace t in (T.take (limit - 1) i, T.drop (T.length i) t) else (T.empty, t) result = go (limit - T.length indent) (tokenize modifiedText) in (indent <>) <$> result -- | Break a token sequence so that all tokens up to but not exceeding -- a length limit are included on the left, and if any remain on the -- right, return Just those too (or Nothing if there weren't any). If -- this breaks a sequence at at point where the next token after the -- break point is whitespace, that whitespace token is removed. breakTokens :: WrapSettings -> Int -> [Token] -> ([Token], Maybe [Token]) breakTokens _ _ [] = ([], Nothing) breakTokens settings limit ts = -- Take enough tokens until we reach the point where taking more -- would exceed the line length. let go _ [] = ([], []) -- Check to see whether the next token exceeds the limit. If so, bump -- it to the next line and terminate. Otherwise keep it and continue to -- the next token. go acc (tok:toks) = if tokenLength tok + acc <= limit then let (nextAllowed, nextDisallowed) = go (acc + tokenLength tok) toks in (tok : nextAllowed, nextDisallowed) else case tok of WS _ -> ([], toks) NonWS _ -> if acc == 0 && breakLongWords settings then let (h, tl) = T.splitAt limit (tokenContent tok) in ([NonWS h], NonWS tl : toks) else if acc == 0 then ([tok], toks) else ([], tok:toks) -- Allowed tokens are the ones we keep on this line. The rest go -- on the next line, to be wrapped again. (allowed, disallowed') = go 0 ts disallowed = maybeTrim disallowed' -- Trim leading whitespace on wrapped lines. maybeTrim [] = [] maybeTrim (WS _:toks) = toks maybeTrim toks = toks result = if null disallowed then (allowed, Nothing) else (allowed, Just disallowed) in result word-wrap-0.4.1/tests/0000755000000000000000000000000013142164773012776 5ustar0000000000000000word-wrap-0.4.1/tests/Main.hs0000644000000000000000000000514613142164773014224 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} module Main where import Test.Hspec import Text.Wrap main :: IO () main = hspec $ do it "leaves short lines untouched" $ do wrapTextToLines defaultWrapSettings 7 "foo bar" `shouldBe` ["foo bar"] it "wraps long lines" $ do wrapTextToLines defaultWrapSettings 7 "Hello, World!" `shouldBe` ["Hello,", "World!"] it "preserves leading whitespace" $ do wrapTextToLines defaultWrapSettings 10 " Hello, World!" `shouldBe` [" Hello,", "World!"] it "honors preexisting newlines" $ do wrapTextToLines defaultWrapSettings 100 "Hello,\n\n \nWorld!" `shouldBe` ["Hello,", "", "", "World!"] it "wraps long lines without truncation" $ do wrapTextToLines defaultWrapSettings 2 "Hello, World!" `shouldBe` ["Hello,", "World!"] it "preserves indentation" $ do let s = defaultWrapSettings { preserveIndentation = True } wrapTextToLines s 10 " Hello, World!" `shouldBe` [" Hello,", " World!"] it "preserves indentation (2)" $ do let s = defaultWrapSettings { preserveIndentation = True } wrapTextToLines s 10 " Hello, World!\n Things And Stuff" `shouldBe` [" Hello,", " World!", " Things", " And", " Stuff"] it "breaks long non-whitespace tokens" $ do let s = defaultWrapSettings { breakLongWords = True } wrapTextToLines s 7 "HelloCrazyWorld!\nReallyLong Token" `shouldBe` ["HelloCr", "azyWorl", "d!", "ReallyL", "ong", "Token"] it "breaks long non-whitespace tokens and indents" $ do let s = defaultWrapSettings { breakLongWords = True , preserveIndentation = True } wrapTextToLines s 7 " HelloCrazyWorld!\n ReallyLong Token" `shouldBe` [ " Hello", " Crazy", " World", " !" , " Reall", " yLong", " Token" ] it "gracefully handles indentation longer than the target width when breaking is off" $ do let s = defaultWrapSettings { breakLongWords = False , preserveIndentation = True } wrapTextToLines s 4 " foo bar" `shouldBe` [" foo", " bar"] it "gracefully handles indentation longer than the target width when breaking is on" $ do let s = defaultWrapSettings { breakLongWords = True , preserveIndentation = True } wrapTextToLines s 4 " foo bar" `shouldBe` [" f", " o", " o", " b", " a", " r"]