@@ -1,162 +1,180 @@
2−Module : Text.Inflections
2+-- Module : Text.Inflections
3−Description : Rails-like inflections library for common String transformations.
3+-- Description : Rails-like inflections library for common String transformations.
4−Copyright : (c) Justin Leitgeb
4+-- Copyright : (c) Justin Leitgeb
5−License : MIT
5+-- License : MIT
7−Maintainer : justin@stackbuilders.com
7+-- Maintainer : justin@stackbuilders.com
8−Stability : unstable
8+-- Stability : unstable
9−Portability : portable
9+-- Portability : portable
11−This module provides methods for common String transformations, similar to the
11+-- This module provides methods for common String transformations, similar
12−"Inflections" library found in Rails:
12+-- to the Inflections library found in Rails:
14−<http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html>
14+-- <http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html>
16−While many of the functions in this library are the same as in implementations
16+-- While many of the functions in this library are the same as in
17−in Rails' ActiveSupport, the philosophy of this library is fundamentally
17+-- implementations in Rails' ActiveSupport, the philosophy of this library
18−different. Where Rails tries to be as permissive as possible, and return a
18+-- is fundamentally different. Where Rails tries to be as permissive as
19−String when given any input, this library tries to output strings that make
19+-- possible, and return a String when given any input, this library tries to
20−sense according to the function that is called.
20+-- output strings that make sense according to the function that is called.
22−When you look closely at many of the functions in Rails' inflections
22+-- When you look closely at many of the functions in Rails' inflections
23−library, you will notice that many of them are partial. That is, they only
23+-- library, you will notice that many of them are partial. That is, they
24−have well-defined output for some of the possible inputs to the function allowed
24+-- only have well-defined output for some of the possible inputs to the
25−by the type system. As an example, let's take the @underscore@ function. In
25+-- function allowed by the type system. As an example, let's take the
26−Rails, it works like this:
26+-- @underscore@ function. In Rails, it works like this:
28−>>> "fooBar".underscore
28+-- >>> "fooBar".underscore
29−"foo_bar"
29+-- "foo_bar"
31−Looks ok so far. However, it's also easy to produce less expected results:
31+-- Looks OK so far. However, it's also easy to produce less expected results:
33−>>> "foo bar".underscore
33+-- >>> "foo bar".underscore
34−"foo bar"
34+-- "foo bar"
36−The output isn't underscored - it contains a space! It turns out that some of
36+-- The output isn't underscored - it contains a space! It turns out that
37−the functions from Inflections in ActiveSupport are /partial/. Ie., the outputs
37+-- some of the functions from Inflections in ActiveSupport are /partial/.
38−are really only specified for a certain range of the inputs allowed by the
38+-- I.e., the outputs are really only specified for a certain range of the
39−String type.
39+-- inputs allowed by the String type.
41−In the Haskell inflections library, we aim to deliver more predictable results
41+-- In the Haskell inflections library, we aim to deliver more predictable
42−by separating the parsing of strings into tokens from the application of
42+-- results by separating the parsing of strings into tokens from the
43−transformations. Let's see an example.
43+-- application of transformations. Let's see an example.
45−First, we tokenize an underscored String using 'parseSnakeCase':
45+-- First, we tokenize an underscored String using 'parseSnakeCase':
47−>>> parseSnakeCase [] "foo_bar"
47+-- >>> parseSnakeCase [] "foo_bar"
48−Right [Word "foo",Word "bar"]
48+-- Right [Word "foo",Word "bar"]
50−We can chain together the tokenization of the input String and the
50+-- We can chain together the tokenization of the input String and the
51−transformation to CamelCase by using 'Control.Monad.LiftM':
51+-- transformation to CamelCase by using 'fmap':
53−>>> import Control.Monad (liftM)
54−>>> liftM camelize $ parseSnakeCase "foo_bar"
53+-- >>> camelize <$> parseSnakeCase [] "foo_bar"
56−By separating out the tokenization from the application of inflections, we also
56+-- By separating out the tokenization from the application of inflections,
57−end up with useful libraries for validating input which can be used
57+-- we also end up with useful libraries for validating input which can be
58−independently:
58+-- used independently:
60−>>> parseSnakeCase [] "fooBar"
60+-- >>> parseSnakeCase [] "fooBar"
61−Left "(unknown)" (line 1, column 4):
61+-- 1:4:
62−unexpected 'B'
62+-- unexpected 'B'
63−expecting lowercase letter, "_" or end of input
63+-- expecting '_', end of input, or lowercase letter
65+-- As of version 0.3.0.0, we don't permit creation of invalid 'Word's by
66+-- using of the smart constructors 'mkWord' and 'mkAcronym'. This is done
67+-- because not every 'Text' value is a valid 'Word', as it should not
68+-- contain whitespace, for example. Normal words have the type @'Word'
69+-- 'Normal'@, while acronyms have the type @'Word' 'Acronym'@. If you need
70+-- to have several words\/acronyms in a single list, use the existential
71+-- wrapper 'SomeWord'. Parsing functions now produce 'SomeWord's.
65−This library is still a work-in-progress, and contributions are welcome for
73+-- This library is still a work-in-progress, and contributions are welcome
66−missing pieces and to fix bugs. Please see the Github page to contribute with
74+-- for missing pieces and to fix bugs. Please see the Github page to
67−code or bug reports:
75+-- contribute with code or bug reports:
69−<https://github.com/stackbuilders/inflections-hs>
77+-- <https://github.com/stackbuilders/inflections-hs>
71−-}
79+{-# LANGUAGE CPP #-}
73module Text.Inflections
81module Text.Inflections
74− ( camelize
86+ , camelize
75− , camelizeCustom
87+ , camelizeCustom
77− , dasherize
88+ , dasherize
79− , humanize
89+ , humanize
81− , underscore
90+ , underscore
83− , titleize
91+ , titleize
85− , Transliterations
92+ , Transliterations
86− , defaultMap
93+ , defaultTransliterations
88− , parameterize
94+ , parameterize
89− , parameterizeCustom
95+ , parameterizeCustom
91− , transliterate
96+ , transliterate
92− , transliterateCustom
97+ , transliterateCustom
94− , ordinal
99+ , ordinal
95− , ordinalize
100+ -- * Types and helpers
97− , parseSnakeCase
102+ , WordType (..)
98− , parseCamelCase
103+ , mkWord
108+ , InflectionException (..)
99 -- * Often used combinators
109 -- * Often used combinators
100− , toUnderscore
110+ , toUnderscore
101− , toDashed
111+ , toDashed
102− , toCamelCased
112+ , toCamelCased
103− )
113+ , betterThrow )
106−import Control.Monad (liftM)
107import Control.Monad.Catch (MonadThrow (..))
116import Control.Monad.Catch (MonadThrow (..))
108import Data.Text (Text)
117import Data.Text (Text)
109import Text.Inflections.Camelize (camelize, camelizeCustom)
118import Text.Inflections.Camelize (camelize, camelizeCustom)
110import Text.Inflections.Dasherize (dasherize)
119import Text.Inflections.Dasherize (dasherize)
111−import Text.Inflections.Data (Transliterations, defaultMap)
120+import Text.Inflections.Data (Transliterations, defaultTransliterations)
112import Text.Inflections.Humanize (humanize)
121import Text.Inflections.Humanize (humanize)
113import Text.Inflections.Ordinal (ordinal, ordinalize)
122import Text.Inflections.Ordinal (ordinal, ordinalize)
114import Text.Inflections.Parameterize (parameterize, parameterizeCustom)
123import Text.Inflections.Parameterize (parameterize, parameterizeCustom)
115import Text.Inflections.Parse.CamelCase (parseCamelCase)
124import Text.Inflections.Parse.CamelCase (parseCamelCase)
116import Text.Inflections.Parse.SnakeCase (parseSnakeCase)
125import Text.Inflections.Parse.SnakeCase (parseSnakeCase)
117−import Text.Inflections.Parse.Types
118import Text.Inflections.Titleize (titleize)
126import Text.Inflections.Titleize (titleize)
119import Text.Inflections.Transliterate (transliterate, transliterateCustom)
127import Text.Inflections.Transliterate (transliterate, transliterateCustom)
128+import Text.Inflections.Types
120import Text.Inflections.Underscore (underscore)
129import Text.Inflections.Underscore (underscore)
121import Text.Megaparsec
130import Text.Megaparsec
132+#if MIN_VERSION_base(4,8,0)
133+import Prelude hiding (Word)
123−-- | Transforms CamelCasedString to snake_cased_string_with_underscores. In
136+-- | Transforms CamelCasedString to snake_cased_string_with_underscores.
124−-- case of failed parsing 'InflectionException' is thrown.
137+--
138+-- > toUnderscore = fmap underscore . parseCamelCase []
126-- >>> toUnderscore "FooBarBazz"
140-- >>> toUnderscore "FooBarBazz"
127-- "foo_bar_bazz"
141-- "foo_bar_bazz"
128−toUnderscore :: MonadThrow m => Text -> m Text
142+toUnderscore :: Text -> Either (ParseError Char Dec) Text
129−toUnderscore = liftM underscore . handleEither . parseCamelCase []
143+toUnderscore = fmap underscore . parseCamelCase []
130−{-# INLINE toUnderscore #-}
132−-- | Transforms CamelCasedString to snake-cased-string-with-dashes. In case
145+-- | Transforms CamelCasedString to snake-cased-string-with-dashes.
133−-- of failed parsing 'InflectionException' is thrown.
146+--
147+-- > toDashed = fmap dasherize . parseCamelCase []
135-- >>> toDashed "FooBarBazz"
149-- >>> toDashed "FooBarBazz"
136-- "foo-bar-bazz"
150-- "foo-bar-bazz"
137−toDashed :: MonadThrow m => Text -> m Text
151+toDashed :: Text -> Either (ParseError Char Dec) Text
138−toDashed = liftM dasherize . handleEither . parseCamelCase []
152+toDashed = fmap dasherize . parseCamelCase []
139−{-# INLINE toDashed #-}
141-- | Transforms underscored_text to CamelCasedText. If first argument is
154-- | Transforms underscored_text to CamelCasedText. If first argument is
142-- 'True' then FirstCharacter in result string will be in upper case. If
155-- 'True' then FirstCharacter in result string will be in upper case. If
143−-- 'False' then firstCharacter will be in lower case. In case of failed
156+-- 'False' then firstCharacter will be in lower case.
144−-- parsing 'InflectionException' is thrown.
157+--
158+-- > toCamelCased t = fmap (camelizeCustom t) . parseSnakeCase []
146-- >>> toCamelCased True "foo_bar_bazz"
160-- >>> toCamelCased True "foo_bar_bazz"
147-- "FooBarBazz"
161-- "FooBarBazz"
148-- >>> toCamelCased False "foo_bar_bazz"
162-- >>> toCamelCased False "foo_bar_bazz"
149-- "fooBarBazz"
163-- "fooBarBazz"
150−toCamelCased :: MonadThrow m
164+toCamelCased
151− => Bool -- ^ Capitalize the first character
165+ :: Bool -- ^ Capitalize the first character
152 -> Text -- ^ Input
166 -> Text -- ^ Input
153− -> m Text -- ^ Ouput
167+ -> Either (ParseError Char Dec) Text -- ^ Ouput
154−toCamelCased t = liftM (camelizeCustom t) . handleEither . parseSnakeCase []
168+toCamelCased t = fmap (camelizeCustom t) . parseSnakeCase []
155−{-# INLINE toCamelCased #-}
169+
170+-- | Lift something of type @'Either' ('ParseError' 'Char' 'Dec') a@ to
171+-- an instance of 'MonadThrow'. Useful when you want to shortcut on parsing
172+-- failures and you're in an instance of 'MonadThrow'.
174+-- This throws 'InflectionParsingFailed' if given value is inside 'Left'.
157−-- | Take an 'Either' that can contain a parser error and throw it if
158−-- necessary. If everything is OK, just return 'Right' value.
159−handleEither :: MonadThrow m => Either (ParseError Char Dec) a -> m a
178+betterThrow :: MonadThrow m => Either (ParseError Char Dec) a -> m a
160−handleEither (Left err) = throwM (InflectionParsingFailed err)
179+betterThrow (Left err) = throwM (InflectionParsingFailed err)
161−handleEither (Right x) = return x
180+betterThrow (Right x) = return x
162−{-# INLINE handleEither #-}