1--------------------------------------------------------------------------------
2-- | As 'Identifier' is used to specify a single item, a 'Pattern' is used to
3-- specify a list of items.
5-- In most cases, globs are used for patterns.
7-- A very simple pattern of such a pattern is @\"foo\/bar\"@. This pattern will
8-- only match the exact @foo\/bar@ identifier.
10-- To match more than one identifier, there are different captures that one can
13-- * @\"*\"@: matches at most one element of an identifier;
15-- * @\"**\"@: matches one or more elements of an identifier.
19-- * @\"foo\/*\"@ will match @\"foo\/bar\"@ and @\"foo\/foo\"@, but not
20-- @\"foo\/bar\/qux\"@;
22-- * @\"**\"@ will match any identifier;
24-- * @\"foo\/**\"@ will match @\"foo\/bar\"@ and @\"foo\/bar\/qux\"@, but not
27-- * @\"foo\/*.html\"@ will match all HTML files in the @\"foo\/\"@ directory.
29-- The 'capture' function allows the user to get access to the elements captured
30-- by the capture elements in the pattern.
31module Hakyll.Core.Identifier.Pattern
32 ( -- * The pattern type
35 -- * Creating patterns
41 -- * Manipulating patterns
46 -- * Applying patterns
50 -- * Capturing strings
57--------------------------------------------------------------------------------
58import Control.Applicative (pure, (<$>), (<*>))
59import Control.Arrow ((&&&), (>>>))
60import Control.Monad (msum)
61import Data.Binary (Binary (..), getWord8, putWord8)
62import Data.List (inits, isPrefixOf, tails)
63import Data.Maybe (isJust)
64import Data.Monoid (Monoid, mappend, mempty)
66import qualified Data.Set as S
69--------------------------------------------------------------------------------
70import GHC.Exts (IsString, fromString)
71import Text.Regex.TDFA ((=~))
74--------------------------------------------------------------------------------
75import Hakyll.Core.Identifier
78--------------------------------------------------------------------------------
79-- | Elements of a glob pattern
87--------------------------------------------------------------------------------
88instance Binary GlobComponent where
89 put Capture = putWord8 0
90 put CaptureMany = putWord8 1
91 put (Literal s) = putWord8 2 >> put s
93 get = getWord8 >>= \t -> case t of
97 _ -> error "Data.Binary.get: Invalid GlobComponent"
100--------------------------------------------------------------------------------
101-- | Type that allows matching on identifiers
105 | And Pattern Pattern
106 | Glob [GlobComponent]
107 | List (Set Identifier)
109 | Version (Maybe String)
113--------------------------------------------------------------------------------
114instance Binary Pattern where
115 put Everything = putWord8 0
116 put (Complement p) = putWord8 1 >> put p
117 put (And x y) = putWord8 2 >> put x >> put y
118 put (Glob g) = putWord8 3 >> put g
119 put (List is) = putWord8 4 >> put is
120 put (Regex r) = putWord8 5 >> put r
121 put (Version v) = putWord8 6 >> put v
123 get = getWord8 >>= \t -> case t of
125 1 -> Complement <$> get
126 2 -> And <$> get <*> get
133--------------------------------------------------------------------------------
134instance IsString Pattern where
135 fromString = fromGlob
138--------------------------------------------------------------------------------
139instance Monoid Pattern where
141 mappend x y = optimize $ And x y
144--------------------------------------------------------------------------------
145-- | This is necessary for good 'isLiteral' results
146optimize :: Pattern -> Pattern
147optimize (Complement x) = Complement (optimize x)
148optimize (And x Everything) = x
149optimize (And Everything y) = y
150optimize (And x y) = And (optimize x) (optimize y)
154--------------------------------------------------------------------------------
155-- | Parse a pattern from a string
156fromGlob :: String -> Pattern
157fromGlob = Glob . parse'
160 let (chunk, rest) = break (`elem` "\\*") str
162 ('\\' : x : xs) -> Literal (chunk ++ [x]) : parse' xs
163 ('*' : '*' : xs) -> Literal chunk : CaptureMany : parse' xs
164 ('*' : xs) -> Literal chunk : Capture : parse' xs
165 xs -> Literal chunk : Literal xs : []
168--------------------------------------------------------------------------------
169-- | Create a 'Pattern' from a list of 'Identifier's it should match
170fromList :: [Identifier] -> Pattern
171fromList = List . S.fromList
174--------------------------------------------------------------------------------
175-- | Create a 'Pattern' from a regex
179-- > regex "^foo/[^x]*$
180fromRegex :: String -> Pattern
184--------------------------------------------------------------------------------
185-- | Create a pattern which matches all items with the given version.
186fromVersion :: Maybe String -> Pattern
190--------------------------------------------------------------------------------
191-- | Inverts a pattern, e.g.
193-- > complement "foo/bar.html"
195-- will match /anything/ except @\"foo\/bar.html\"@
196complement :: Pattern -> Pattern
197complement = Complement
200--------------------------------------------------------------------------------
201-- | Specify a version, e.g.
203-- > "foo/*.markdown" `withVersion` "pdf"
204withVersion :: Pattern -> String -> Pattern
205withVersion p v = optimize $ And p $ fromVersion $ Just v
208--------------------------------------------------------------------------------
209-- | Check if a pattern is a literal. @\"*.markdown\"@ is not a literal but
210-- @\"posts.markdown\"@ is.
211fromLiteral :: Pattern -> Maybe Identifier
212fromLiteral pattern = case pattern of
213 Glob p -> fmap fromFilePath $ foldr fromLiteral' (Just "") p
216 fromLiteral' (Literal x) (Just y) = Just $ x ++ y
217 fromLiteral' _ _ = Nothing
221--------------------------------------------------------------------------------
222-- | Check if an identifier matches a pattern
223matches :: Pattern -> Identifier -> Bool
224matches Everything _ = True
225matches (Complement p) i = not $ matches p i
226matches (And x y) i = matches x i && matches y i
227matches (Glob p) i = isJust $ capture (Glob p) i
228matches (List l) i = i `S.member` l
229matches (Regex r) i = toFilePath i =~ r
230matches (Version v) i = identifierVersion i == v
233--------------------------------------------------------------------------------
234-- | Given a list of identifiers, retain only those who match the given pattern
235filterMatches :: Pattern -> [Identifier] -> [Identifier]
236filterMatches = filter . matches
239--------------------------------------------------------------------------------
240-- | Split a list at every possible point, generate a list of (init, tail)
241-- cases. The result is sorted with inits decreasing in length.
242splits :: [a] -> [([a], [a])]
243splits = inits &&& tails >>> uncurry zip >>> reverse
246--------------------------------------------------------------------------------
247-- | Match a glob against a pattern, generating a list of captures
248capture :: Pattern -> Identifier -> Maybe [String]
249capture (Glob p) i = capture' p (toFilePath i)
253--------------------------------------------------------------------------------
254-- | Internal verion of 'capture'
255capture' :: [GlobComponent] -> String -> Maybe [String]
256capture' [] [] = Just [] -- An empty match
257capture' [] _ = Nothing -- No match
258capture' (Literal l : ms) str
259 -- Match the literal against the string
260 | l `isPrefixOf` str = capture' ms $ drop (length l) str
261 | otherwise = Nothing
262capture' (Capture : ms) str =
263 -- Match until the next /
264 let (chunk, rest) = break (== '/') str
265 in msum $ [ fmap (i :) (capture' ms (t ++ rest)) | (i, t) <- splits chunk ]
266capture' (CaptureMany : ms) str =
268 msum $ [ fmap (i :) (capture' ms t) | (i, t) <- splits str ]
271--------------------------------------------------------------------------------
272-- | Create an identifier from a pattern by filling in the captures with a given
277-- > fromCapture (parseGlob "tags/*") "foo"
282fromCapture :: Pattern -> String -> Identifier
283fromCapture pattern = fromCaptures pattern . repeat
286--------------------------------------------------------------------------------
287-- | Create an identifier from a pattern by filling in the captures with the
288-- given list of strings
289fromCaptures :: Pattern -> [String] -> Identifier
290fromCaptures (Glob p) = fromFilePath . fromCaptures' p
291fromCaptures _ = error $
292 "Hakyll.Core.Identifier.Pattern.fromCaptures: fromCaptures only works " ++
296--------------------------------------------------------------------------------
297-- | Internally used version of 'fromCaptures'
298fromCaptures' :: [GlobComponent] -> [String] -> String
299fromCaptures' [] _ = mempty
300fromCaptures' (m : ms) [] = case m of
301 Literal l -> l `mappend` fromCaptures' ms []
302 _ -> error $ "Hakyll.Core.Identifier.Pattern.fromCaptures': "
303 ++ "identifier list exhausted"
304fromCaptures' (m : ms) ids@(i : is) = case m of
305 Literal l -> l `mappend` fromCaptures' ms ids
306 _ -> i `mappend` fromCaptures' ms is
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list