Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

phantom snapshot dependency #317

Closed
wferi opened this issue Dec 11, 2014 · 13 comments
Closed

phantom snapshot dependency #317

wferi opened this issue Dec 11, 2014 · 13 comments

Comments

@wferi
Copy link
Contributor

wferi commented Dec 11, 2014

Why does building

{-# LANGUAGE OverloadedStrings #-}
import Hakyll

main :: IO ()
main = hakyll $ do
    match "main.markdown" $ do
        route   $ setExtension "html"
        compile $ pandocCompiler
            >>= saveSnapshot "content"
            >>= loadAndApplyTemplate "templates/nav.html" navCtx

    match "templates/nav.html" $ compile templateCompiler

navCtx :: Context String
navCtx = listField "pages" defaultContext (loadAllSnapshots "*.markdown" "content")

give the

[ERROR] Hakyll.Core.Runtime.chase: Dependency cycle detected: main.markdown depends on main.markdown

error? After all, the snapshot is taken before applying the template, so all should be linear.
Looks like dependency tracking does not handle snapshots. Or I'm mistaken. Could you provide a fix to either, please?

@jaspervdj
Copy link
Owner

Dependency tracking does not take snapshots into account, but it looks like this would be a rather straightforward extension -- I'll have a look at it.

@jaspervdj
Copy link
Owner

I think I fixed the issue. Would you be willing to test if the current master branch of Hakyll solves your issue? It works for the small example you provided but I'm curious if there are any larger instances. In any case, I think it should work.

@vbeffara
Copy link

Nice feature! It works for me.

@wferi
Copy link
Contributor Author

wferi commented Dec 13, 2014

Thanks for the fix, now I'm trying to use this new feature on the site I'm building. Meanwhile, if I omit the >>= saveSnapshot "content" line of the above example, I get the dependency cycle error again, instead of the expected empty pages list field. What does loadAllSnapshots do if it does not find snapshots?
Anyway, my aim is to automatically build the navigation bar from the URLs and titles of the top level files on my site, addig some class to the link of the current page. Should I use some other tool for this job?

@nagisa
Copy link
Contributor

nagisa commented Dec 13, 2014

Final result of the compilation rule is also a snapshot with the name
final.

Kind regards,
S.

On Sat, 13 Dec, 2014 at 12:28 PM, wferi [email protected]
wrote:

Thanks for the fix, now I'm trying to use this new feature on the
site I'm building. Meanwhile, if I omit the >>= saveSnapshot
"content" line of the above example, I get the dependency cycle error
again, instead of the expected empty pages list field. What does
loadAllSnapshots do if it does not find snapshots?
Anyway, my aim is to automatically build the navigation bar from the
URLs and titles of the top level files on my site, addig some class
to the link of the current page. Should I use some other tool for
this job?


Reply to this email directly or view it on GitHub.

@wferi
Copy link
Contributor Author

wferi commented Dec 13, 2014

But doesn't loadAllSnapshots "*.markdown" "content" require content snapshots? If so, final snapshots should not qualify.

@nagisa
Copy link
Contributor

nagisa commented Dec 13, 2014

Ah, indeed.

@krsch
Copy link
Contributor

krsch commented Dec 13, 2014

As far as I understand, hakyll does not know what snapshots will be saved until it finishes processing the compile rule. You might even have something like saveSnapshot =<< getMetadataField' someid "snapshot" which forbids any meaningful analysis. If you want a stricter approach you can use versions instead of snapshots:

{-# LANGUAGE OverloadedStrings #-}
import Hakyll

main :: IO ()
main = hakyll $ do
    match "main.markdown" $ version "content" $
        compile pandocCompiler
    match "main.markdown" $ do
        route   $ setExtension "html"
        compile $ do
            id' <- setVersion (Just "content") `fmap` getUnderlying
            loadBody id' >>= makeItem
            >>= loadAndApplyTemplate "templates/nav.html" navCtx

    match "templates/nav.html" $ compile templateCompiler

navCtx :: Context String
navCtx = listField "pages" defaultContext (loadAll $ "*.markdown" .&&. hasVersion "content")

@wferi
Copy link
Contributor Author

wferi commented Dec 13, 2014

Well, I had a look at the version feature beforehand, but at this point we are getting too messy for the task. Especially in a language which can cope with self-references like

structure = length structure:take 3 structure

I mean, already by using snapshots, we're kludging something which Haskell has for free.
Of course I can understand that Hakyll may have other priorities, or its internals may forbid this, but in principle even this could work:

{-# LANGUAGE OverloadedStrings #-}
import Hakyll

main :: IO ()
main = hakyll $ do
    match "main.markdown" $ do
        route   $ setExtension "html"
        compile $ pandocCompiler >>= loadAndApplyTemplate "templates/nav.html" navCtx

    match "templates/nav.html" $ compile templateCompiler

navCtx :: Context String
navCtx = listField "pages" (urlField "url" :: Context String) (loadAll "*.markdown")

Basically, I'd be good until I used anything generated by loadAndApplyTemplate inside $for(pages)$. Like body. If only site compilation could be done in a non-strict manner... Would this be possible? (Just theoretically, by some other static website generator. :)

@krsch
Copy link
Contributor

krsch commented Dec 13, 2014

We can just add loadWithoutBody :: Identifier -> Item () for that.

loadWithoutBody id' = Item id' ()

@wferi
Copy link
Contributor Author

wferi commented Dec 16, 2014

I finally managed to use this new feature for my purpose. The gist of the thing is:

navCtx :: Identifier -> Context String
navCtx mainPage = listField "pages" (current <> defaultContext) mainPages <> defaultContext
    where current = field "current" (\i -> if mainPage == itemIdentifier i then return "current" else fail "other")
          mainPages = loadAllSnapshots ("*.html" .||. "*.markdown") "content"
                  >>= sortByM (fmap read . flip getMetadataField' "navPrio" . itemIdentifier :: Item a -> Compiler Int)
          -- sortByM from Hakyll/Web/Template/List.hs
          sortByM :: (Monad m, Ord k) => (a -> m k) -> [a] -> m [a]
          sortByM f xs = liftM (map fst . sortBy (comparing snd)) $
                         mapM (\x -> liftM (x,) (f x)) xs

finalize :: Item String -> Compiler (Item String)
finalize i = do
    id' <- getUnderlying
    let mainPage | matches "media/*" id' = "media.html"
                 | otherwise = id'
    loadAndApplyTemplate "templates/default.html" (navCtx mainPage) i
        >>= relativizeUrls

with using metadata blocks like

---
title: About us
navPrio: 30
---

everywhere, even instead of calling create. Then the navigation bar is created in templates/default.html like this:

$for(pages)$<a href="$url$" $if(current)$id="current"$endif$>$title$</a>$endfor$

Btw. I find conditional fields rather awkward to use, what do I overlook?

field "current" (return . (mainPage ==) . itemIdentifier)

would be much more natural for me.
Anyway, thanks a lot for this addition, it's really useful.

@nagisa
Copy link
Contributor

nagisa commented Dec 17, 2014

Btw. I find conditional fields rather awkward to use, what do I overlook?

You might find #311 useful regarding that.

@jaspervdj
Copy link
Owner

I released this as Hakyll 4.6.2.0. Thanks for the input!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants