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
43 -- * Composing patterns
48 -- * Applying patterns
52 -- * Capturing strings
59--------------------------------------------------------------------------------
60import Control.Applicative (pure, (<$>), (<*>))
61import Control.Arrow ((&&&), (>>>))
62import Control.Monad (msum)
63import Data.Binary (Binary (..), getWord8, putWord8)
64import Data.List (inits, isPrefixOf, tails)
65import Data.Maybe (isJust)
66import Data.Monoid (Monoid, mappend, mempty)
68import qualified Data.Set as S
71--------------------------------------------------------------------------------
72import GHC.Exts (IsString, fromString)
73import Text.Regex.TDFA ((=~))
76--------------------------------------------------------------------------------
77import Hakyll.Core.Identifier
80--------------------------------------------------------------------------------
81-- | Elements of a glob pattern
89--------------------------------------------------------------------------------
90instance Binary GlobComponent where
91 put Capture = putWord8 0
92 put CaptureMany = putWord8 1
93 put (Literal s) = putWord8 2 >> put s
95 get = getWord8 >>= \t -> case t of
99 _ -> error "Data.Binary.get: Invalid GlobComponent"
102--------------------------------------------------------------------------------
103-- | Type that allows matching on identifiers
107 | And Pattern Pattern
108 | Glob [GlobComponent]
109 | List (Set Identifier)
111 | Version (Maybe String)
115--------------------------------------------------------------------------------
116instance Binary Pattern where
117 put Everything = putWord8 0
118 put (Complement p) = putWord8 1 >> put p
119 put (And x y) = putWord8 2 >> put x >> put y
120 put (Glob g) = putWord8 3 >> put g
121 put (List is) = putWord8 4 >> put is
122 put (Regex r) = putWord8 5 >> put r
123 put (Version v) = putWord8 6 >> put v
125 get = getWord8 >>= \t -> case t of
127 1 -> Complement <$> get
128 2 -> And <$> get <*> get
135--------------------------------------------------------------------------------
136instance IsString Pattern where
137 fromString = fromGlob
140--------------------------------------------------------------------------------
141instance Monoid Pattern where
146--------------------------------------------------------------------------------
147-- | Parse a pattern from a string
148fromGlob :: String -> Pattern
149fromGlob = Glob . parse'
152 let (chunk, rest) = break (`elem` "\\*") str
154 ('\\' : x : xs) -> Literal (chunk ++ [x]) : parse' xs
155 ('*' : '*' : xs) -> Literal chunk : CaptureMany : parse' xs
156 ('*' : xs) -> Literal chunk : Capture : parse' xs
157 xs -> Literal chunk : Literal xs : []
160--------------------------------------------------------------------------------
161-- | Create a 'Pattern' from a list of 'Identifier's it should match.
163-- /Warning/: use this carefully with 'hasNoVersion' and 'hasVersion'. The
164-- 'Identifier's in the list /already/ have versions assigned, and the pattern
165-- will then only match the intersection of both versions.
167-- A more concrete example,
169-- > fromList ["foo.markdown"] .&&. hasVersion "pdf"
171-- will not match anything! The @"foo.markdown"@ 'Identifier' has no version
172-- assigned, so the LHS of '.&&.' will only match this 'Identifier' with no
173-- version. The RHS only matches 'Identifier's with version set to @"pdf"@ --
174-- hence, this pattern matches nothing.
176-- The correct way to use this is:
178-- > fromList $ map (setVersion $ Just "pdf") ["foo.markdown"]
179fromList :: [Identifier] -> Pattern
180fromList = List . S.fromList
183--------------------------------------------------------------------------------
184-- | Create a 'Pattern' from a regex
188-- > regex "^foo/[^x]*$
189fromRegex :: String -> Pattern
193--------------------------------------------------------------------------------
194-- | Create a pattern which matches all items with the given version.
195fromVersion :: Maybe String -> Pattern
199--------------------------------------------------------------------------------
200-- | Specify a version, e.g.
202-- > "foo/*.markdown" .&&. hasVersion "pdf"
203hasVersion :: String -> Pattern
204hasVersion = fromVersion . Just
207--------------------------------------------------------------------------------
208-- | Match only if the identifier has no version set, e.g.
210-- > "foo/*.markdown" .&&. hasNoVersion
211hasNoVersion :: Pattern
212hasNoVersion = fromVersion Nothing
215--------------------------------------------------------------------------------
216-- | '&&' for patterns: the given identifier must match both subterms
217(.&&.) :: Pattern -> Pattern -> Pattern
222--------------------------------------------------------------------------------
223-- | '||' for patterns: the given identifier must match any subterm
224(.||.) :: Pattern -> Pattern -> Pattern
225x .||. y = complement (complement x `And` complement y) -- De Morgan's law
229--------------------------------------------------------------------------------
230-- | Inverts a pattern, e.g.
232-- > complement "foo/bar.html"
234-- will match /anything/ except @\"foo\/bar.html\"@
235complement :: Pattern -> Pattern
236complement = Complement
239--------------------------------------------------------------------------------
240-- | Check if an identifier matches a pattern
241matches :: Pattern -> Identifier -> Bool
242matches Everything _ = True
243matches (Complement p) i = not $ matches p i
244matches (And x y) i = matches x i && matches y i
245matches (Glob p) i = isJust $ capture (Glob p) i
246matches (List l) i = i `S.member` l
247matches (Regex r) i = toFilePath i =~ r
248matches (Version v) i = identifierVersion i == v
251--------------------------------------------------------------------------------
252-- | Given a list of identifiers, retain only those who match the given pattern
253filterMatches :: Pattern -> [Identifier] -> [Identifier]
254filterMatches = filter . matches
257--------------------------------------------------------------------------------
258-- | Split a list at every possible point, generate a list of (init, tail)
259-- cases. The result is sorted with inits decreasing in length.
260splits :: [a] -> [([a], [a])]
261splits = inits &&& tails >>> uncurry zip >>> reverse
264--------------------------------------------------------------------------------
265-- | Match a glob against a pattern, generating a list of captures
266capture :: Pattern -> Identifier -> Maybe [String]
267capture (Glob p) i = capture' p (toFilePath i)
271--------------------------------------------------------------------------------
272-- | Internal verion of 'capture'
273capture' :: [GlobComponent] -> String -> Maybe [String]
274capture' [] [] = Just [] -- An empty match
275capture' [] _ = Nothing -- No match
276capture' (Literal l : ms) str
277 -- Match the literal against the string
278 | l `isPrefixOf` str = capture' ms $ drop (length l) str
279 | otherwise = Nothing
280capture' (Capture : ms) str =
281 -- Match until the next /
282 let (chunk, rest) = break (== '/') str
283 in msum $ [ fmap (i :) (capture' ms (t ++ rest)) | (i, t) <- splits chunk ]
284capture' (CaptureMany : ms) str =
286 msum $ [ fmap (i :) (capture' ms t) | (i, t) <- splits str ]
289--------------------------------------------------------------------------------
290-- | Create an identifier from a pattern by filling in the captures with a given
295-- > fromCapture (parseGlob "tags/*") "foo"
300fromCapture :: Pattern -> String -> Identifier
301fromCapture pattern = fromCaptures pattern . repeat
304--------------------------------------------------------------------------------
305-- | Create an identifier from a pattern by filling in the captures with the
306-- given list of strings
307fromCaptures :: Pattern -> [String] -> Identifier
308fromCaptures (Glob p) = fromFilePath . fromCaptures' p
309fromCaptures _ = error $
310 "Hakyll.Core.Identifier.Pattern.fromCaptures: fromCaptures only works " ++
314--------------------------------------------------------------------------------
315-- | Internally used version of 'fromCaptures'
316fromCaptures' :: [GlobComponent] -> [String] -> String
317fromCaptures' [] _ = mempty
318fromCaptures' (m : ms) [] = case m of
319 Literal l -> l `mappend` fromCaptures' ms []
320 _ -> error $ "Hakyll.Core.Identifier.Pattern.fromCaptures': "
321 ++ "identifier list exhausted"
322fromCaptures' (m : ms) ids@(i : is) = case m of
323 Literal l -> l `mappend` fromCaptures' ms ids
324 _ -> 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