hakyll icon indicating copy to clipboard operation
hakyll copied to clipboard

Return empty field when Listfield has no elements to allow using ()$ …

Open jmatsushita opened this issue 10 years ago • 5 comments

…around a ()$ block.

This is my first ever Haskell pull request, so apologies for the lack of elegance. This PR is meant to allow using $if()$ blocks surrounding a $for()$ block in templates. It returns empty when a Listfield is empty. This helps for the following (quite common) pattern:

$if(list)$
  There is a list!
  $for(list)$
    It contains : $elem$
  $endfor$
$else$
  No list...
$endif$

jmatsushita avatar May 16 '15 11:05 jmatsushita

This solution has several problems:

  1. It breaks compatibility.
  2. It introduces new overrides. Any empty list will be overridden by later contexts.

A better solution would be something like $if(notnull(list))$. It is possible to implement by modifying applyExpr in Hakyll.Web.Template though it looks a bit ugly.

A better choice might be implementing functions over lists in templates. But a change to the definition of Context would be needed which is pretty fundamental.

The simplest solution I can think of is to define listField' with the same type as listField. It would create not only a list field, but also a bool field with key equal key ++ "?" that checks for emptiness of the list. It solves this particular problem, but doesn't help to solve similar ones.

krsch avatar May 16 '15 13:05 krsch

Hi @jaspervdj I just ran into this problem again trying simply check for empty strings in my template. Could it be that a maybeField with the same approach as the haskell Maybe would work? i.e. in $if(aMaybeField)$ it would just "pattern match" on Nothing or Just _ while with $aMaybeField$ it would return empty string for Nothing and value for Just value ?

jmatsushita avatar Jul 28 '15 12:07 jmatsushita

You can achieve this already in a reasonably clean way. The code I am using is based on the canonical example, i.e. the one generated by hakyll-init. In that site, we have the following templates/post-list.html:

<ul>
    $for(posts)$
        <li>
            <a href="$url$">$title$</a> - $date$
        </li>
    $endfor$
</ul>

Let's change that to:

$if(posts)$
    <ul>
        $for(posts)$
            <li>
                <a href="$url$">$title$</a> - $date$
            </li>
        $endfor$
    </ul>
$else$
    <p>There are no posts.</p>
$endif$

Now, an $if(...)$ in Hakyll templating will fail when an error occurs. We can achieve that behaviour by changing site.hs slightly as well. Currently we have, when generating the index page:

let indexCtx =
        listField "posts" postCtx (return posts) `mappend`
        constField "title" "Home"                `mappend`
        defaultContext

The code spitting out the list is simply return posts -- which will never raise an error. So we simply change that to:

let indexCtx =
        listField "posts" postCtx
            (if null posts then fail "No posts" else return posts)
                                  `mappend`
        constField "title" "Home" `mappend`
        defaultContext

Does this work for your use case?

jaspervdj avatar Jul 30 '15 07:07 jaspervdj

Hi Jasper,

Thanks for the answer. I was aware that ~~this worked with listFields~~ (EDIT: it just worked because I use the hack in this pull request, your solution seems less hackish while still I wonder: is there any reason to not make this a default behavior?) but I’m trying to make this work for other type of fields… So for instance in your example how can I wrap the - $date$ in an $if(date) ? And to my point about making this work for other types of field, how do you make it work with $url$ or $title$ fields. So for instance, this would be fairly common (and fairly verbose for a common case of combined field templating):

<ul>
    $for(posts)$
        <li>
            $if(url)$
              <a href="$url$">$title$</a> - $date$
            $else$
              $if(title)$
                $title$ $if(date)$ - $date$ $endif$
              $else$
                $if(date)$ $date$ $endif$
              $endif$
            $endif$
        </li>
    $endfor$
</ul>

Another way might be to allow expressions in the if argument such as $if($date$ == 0 || $title$ == '')$...

If that's of any use, here's an example of what I'm trying to do which doesn't work because $twitter$ et al are strings.

Thanks a lot for your patience (and let me know if it's better to move this into a proper issue rather than stay on this half-baked pull request)!

Jun

jmatsushita avatar Jul 30 '15 08:07 jmatsushita

This sort of behaviour should work -- if twitter is missing from the metadata, then $if(twitter)$ should evaluate to false.

This does not work for date, though, if you are using defaultContext, since that will try to infer a date from your post file name.

jaspervdj avatar Jul 30 '15 11:07 jaspervdj