packages feed

titlecase-1.0.2: src/Data/Text/Titlecase.hs

{-# LANGUAGE MultiWayIf #-}

module Data.Text.Titlecase ( titlecase ) where

import Data.Text.Titlecase.Internal
import Data.Char ( isSpace )

-- | Capitalize all English words except 'articles' (a, an, the), coordinating
-- 'conjunctions' (for, and, nor, but, or, yet, so), and 'prepositions' (unless
-- they begin or end the title).
-- ASCII hyphens separate components, which follow the same rules as words.
-- Whitespace and hyphens are preserved exactly.

titlecase :: String -> String
titlecase t = restoreSeparators t (words (go 1 ws))
  where
    -- The parser changes capitalization, but never the number of words.
    -- Put its output words back between the original separator runs.
    restoreSeparators :: String -> [String] -> String
    restoreSeparators original [] = original
    restoreSeparators original (w:rest) =
      let (spacing, text) = span isSeparator original
          remaining = dropWhile (not . isSeparator) text
      in spacing ++ w ++ restoreSeparators remaining rest

    ws :: [String]
    ws        = words (map (\c -> if c == '-' then ' ' else c) t)
    isSeparator c = isSpace c || c == '-'
    isFirst i = i == 1

    go :: Int -> [String] -> String
    go _ []           = ""
    go i (a:b:c:d:tt) = parse4 i a b c d tt
    go i (a:b:c  :tt) = parse3 i a b c   tt
    go i (a:b    :tt) = parse2 i a b     tt
    go i (a      :tt) = parse1 i a       tt

    parse4 i a b c d tt =
      if isFourWordPreposition a b c d
      then if | isFirst i && null tt
                             -> unwords [toTitle a, b, c, toTitle d]
              | isFirst   i  -> unwords [toTitle a, b, c,         d] <#> go (succ i) tt
              | null tt      -> unwords [        a, b, c, toTitle d]
              | otherwise    -> unwords [        a, b, c,         d] <#> go (succ i) tt
      else parse3 i a b c (d:tt)

    parse3 i a b c tt =
      if isThreeWordPreposition a b c
      then if | isFirst i && null tt
                             -> unwords [toTitle a, b, toTitle c]
              | isFirst   i  -> unwords [toTitle a, b,         c] <#> go (succ i) tt
              | null tt      -> unwords [        a, b, toTitle c]
              | otherwise    -> unwords [        a, b,         c] <#> go (succ i) tt
      else parse2 i a b (c:tt)

    parse2 i a b tt =
      if isTwoWordPreposition a b
      then if | isFirst i && null tt
                             -> unwords [toTitle a, toTitle b]
              | isFirst   i  -> unwords [toTitle a,         b] <#> go (succ i) tt
              | null tt      -> unwords [        a, toTitle b]
              | otherwise    -> unwords [        a,         b] <#> go (succ i) tt
      else parse1 i a (b:tt)

    parse1 i a tt =
      if isOneWordPreposition a || isConjunction a || isArticle a
      then if isFirst i || null tt
           then toTitle a <#> go (succ i) tt
           else         a <#> go (succ i) tt
      else      toTitle a <#> go (succ i) tt