word-wrap-0.5/0000755000000000000000000000000007346545000011471 5ustar0000000000000000word-wrap-0.5/CHANGELOG.md0000644000000000000000000000514607346545000013310 0ustar0000000000000000 0.5 === API changes: * Line-wrapping now supports optional "filling", i.e., placing prefix strings at the start of wrapped lines. The behavior of filling is configured by new `WrapSettings` fields: `fillStrategy` (what to add to filled lines) and `fillScope` (which lines to affect with filling). Thanks to Brent Yorgey for this work! 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.5/LICENSE0000644000000000000000000000277607346545000012512 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.5/README.md0000644000000000000000000000011007346545000012740 0ustar0000000000000000word-wrap ========= This library provides text-wrapping functionality. word-wrap-0.5/Setup.hs0000644000000000000000000000005607346545000013126 0ustar0000000000000000import Distribution.Simple main = defaultMain word-wrap-0.5/benchmarks/0000755000000000000000000000000007346545000013606 5ustar0000000000000000word-wrap-0.5/benchmarks/Main.hs0000644000000000000000000000230407346545000015025 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.5/src/Text/0000755000000000000000000000000007346545000013204 5ustar0000000000000000word-wrap-0.5/src/Text/Wrap.hs0000644000000000000000000001652007346545000014455 0ustar0000000000000000module Text.Wrap ( FillStrategy(..) , FillScope(..) , WrapSettings(..) , defaultWrapSettings , wrapTextToLines , wrapText ) where import Data.Monoid ((<>)) import Data.Char (isSpace) import qualified Data.Text as T -- | How should wrapped lines be filled (i.e. what kind of prefix -- should be attached?) data FillStrategy = NoFill -- ^ Don't do any filling (default) | FillIndent Int -- ^ Indent by this many spaces | FillPrefix T.Text -- ^ Prepend this text deriving (Eq, Show, Read) fillWidth :: FillStrategy -> Int fillWidth NoFill = 0 fillWidth (FillIndent n) = n fillWidth (FillPrefix t) = T.length t -- | To which lines should the fill strategy be applied? data FillScope = FillAfterFirst -- ^ Apply any fill prefix only to lines after -- the first line (default) | FillAll -- ^ Apply any fill prefix to all lines, even -- if there is only one line deriving (Eq, Show, Read) -- | 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. , fillStrategy :: FillStrategy -- ^ What kind of prefix should be applied to lines -- after wrapping? (default: none) , fillScope :: FillScope -- ^ To which lines should the fill strategy be applied? -- (default: all but the first) } deriving (Eq, Show, Read) defaultWrapSettings :: WrapSettings defaultWrapSettings = WrapSettings { preserveIndentation = False , breakLongWords = False , fillStrategy = NoFill , fillScope = FillAfterFirst } -- | Apply a function to the portion of a list of lines indicated by -- the 'FillScope'. withScope :: FillScope -> (a -> a) -> [a] -> [a] withScope FillAfterFirst = onTail withScope FillAll = map -- | Map a function over the tail of a list. onTail :: (a -> a) -> [a] -> [a] onTail _ [] = [] onTail f (a:as) = a : map f as -- | Apply the fill specified in the 'WrapSettings' to a list of lines. applyFill :: WrapSettings -> [T.Text] -> [T.Text] applyFill settings = let scope = fillScope settings in case fillStrategy settings of NoFill -> id FillIndent n -> withScope scope (T.append (T.replicate n (T.pack " "))) FillPrefix t -> withScope scope (T.append t) -- | 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 prior to having any fill applied. Preserved -- indentation is always placed before any fill. 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 restFillWidth = fillWidth (fillStrategy settings) firstLineFillWidth = if fillScope settings == FillAll then restFillWidth else 0 firstLineLimit = limit - T.length indent - firstLineFillWidth restLimit = limit - T.length indent - restFillWidth go _ [] = [T.empty] go _ [WS _] = [T.empty] go isFirstLine ts = let lim = if isFirstLine then firstLineLimit else restLimit (firstLine, maybeRest) = breakTokens settings lim ts firstLineText = T.stripEnd $ T.concat $ fmap tokenContent firstLine in case maybeRest of Nothing -> [firstLineText] Just rest -> firstLineText : go False 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 True (tokenize modifiedText) in map (indent <>) . applyFill settings $ 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.5/tests/0000755000000000000000000000000007346545000012633 5ustar0000000000000000word-wrap-0.5/tests/Main.hs0000644000000000000000000001225107346545000014054 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"] it "indents all but the first line" $ do let s = defaultWrapSettings { fillStrategy = FillIndent 2 } wrapTextToLines s 8 "Hello there, World!" `shouldBe` ["Hello", " there,", " World!"] it "indents all lines" $ do let s = defaultWrapSettings { fillStrategy = FillIndent 2 , fillScope = FillAll } wrapTextToLines s 8 "Hello there, World!" `shouldBe` [" Hello", " there,", " World!"] it "fills all lines but the first with a prefix" $ do let s = defaultWrapSettings { fillStrategy = FillPrefix "- " } wrapTextToLines s 8 "Hello there, World!" `shouldBe` ["Hello", "- there,", "- World!"] it "fills all lines with a prefix" $ do let s = defaultWrapSettings { fillStrategy = FillPrefix "- " , fillScope = FillAll } wrapTextToLines s 8 "Hello there, World!" `shouldBe` ["- Hello", "- there,", "- World!"] it "takes fill width into account" $ do let s = defaultWrapSettings { fillStrategy = FillPrefix "- " } wrapTextToLines s 3 "a b c d" `shouldBe` ["a b", "- c", "- d"] it "takes fill indent into account" $ do let s = defaultWrapSettings { fillStrategy = FillIndent 2 } wrapTextToLines s 3 "a b c d" `shouldBe` ["a b", " c", " d"] it "takes fill width into account on all lines" $ do let s = defaultWrapSettings { fillStrategy = FillPrefix "- " , fillScope = FillAll } wrapTextToLines s 3 "a b c d" `shouldBe` ["- a", "- b", "- c", "- d"] it "takes fill indent into account on all lines" $ do let s = defaultWrapSettings { fillStrategy = FillIndent 2 , fillScope = FillAll } wrapTextToLines s 3 "a b c d" `shouldBe` [" a", " b", " c", " d"] it "places fill after preserved indent" $ do let s = defaultWrapSettings { preserveIndentation = True , fillStrategy = FillPrefix "- " } wrapTextToLines s 5 " a b c d" `shouldBe` [" a b", " - c", " - d"] it "places fill indent after preserved indent" $ do let s = defaultWrapSettings { preserveIndentation = True , fillStrategy = FillIndent 2 } wrapTextToLines s 5 " a b c d" `shouldBe` [" a b", " c", " d"] word-wrap-0.5/word-wrap.cabal0000644000000000000000000000277607346545000014413 0ustar0000000000000000name: word-wrap version: 0.5 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 >= 4.8 && < 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