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

Partial with arguments #806

Open
tobiasBora opened this issue Nov 8, 2020 · 6 comments
Open

Partial with arguments #806

tobiasBora opened this issue Nov 8, 2020 · 6 comments

Comments

@tobiasBora
Copy link
Contributor

tobiasBora commented Nov 8, 2020

Hello,

I'd like to define a block an html code that should be called quite often. To that end, I would like to create a partial myblock.html with something like:

<div>
    <h1>$sectionTitle$</h1>
    <div class="section-body">
        $sectionBody$
    </div>
</div>

and I'd like to insert myblock.html into other templates using something like:

$partial("myblock.html", sectionTitle="My first section", sectionBody="My first section body")$
$partial("myblock.html", sectionTitle="My second section", sectionBody="My second section body")$

However, I can't find in the documentation how to do that. Is it a missing feature, or an undocumented feature? In any case, would it be possible to solve that issue?

Thanks!

@Minoru
Copy link
Collaborator

Minoru commented Nov 8, 2020

Judging by this line, partial really has just one argument, so this feature is indeed missing.

But I think the above can be replaced with:

$for(sections)$
$partial("myblock.html")$
$endfor$

…and then in (untested!) Haskell code:

let ctxFst = \item -> fst (itemBody item)
let ctxSnd = \item -> snd (itemBody item)
let sectionContext = field "sectionTitle" ctxFst <> field "sectionBody" ctxSnd
let postsContext = listField "sections" sectionContext $ do
    section1 <- makeItem ("My first section", "My first section body")
    section2 <- makeItem ("My second section", "My second section body")
    return $ [section1, section2]

In other words:

  1. for each section, create a bogus Item that contains all the properties of the section. Here, I use a tuple for this, but you can defined a new data type if you want named fields;
  2. create a new Context which pulls individual bits of info out of the Item and into keys like $sectionTitle$ and $sectionBody$;
  3. create yet another context with a field "posts", where each element has keys $sectionTitle$ and $sectionBody$.

Does that solve the issue, @tobiasBora?

@tobiasBora
Copy link
Contributor Author

Thanks for your answer. But unfortunately, I'd prefer to have the list contained in the html file directly to keep the html code at the same place.

But I discovered that it's possible to send any function (taking as input a list of strings) in the context, so I created my own cooked solution. I think it's possible to improve it by interpreting the input as JSON directly (it would allow to send lists of items that way). The only issue I have is that I don't think it's possible from functionField to get the context, to the new partial called cannot inherit from the parent context (the context is only specified by the arguments). So here is the hakyll code:


-- Use: $partialWithContext("template.html", "key1", "value1", "key2", "value2")$
partialWithContext :: Context String
partialWithContext = functionField "partialWithContext" $ \args page -> do
  case args of
    [] -> fail "partialWithContext: ERROR: You need to specify a file.\nUsage: $partialWithContext(\"template.html\", \"key1\", \"value1\", \"key2\", \"value2\", ...)$"
    path : key_values -> do
      -- Load the template
      template <- loadBody (fromFilePath path)
      -- TODO: Possible to add the current context?
      -- Convert key_values (looks like ["key1", "val1", "key2", "val2"]) into a context
      context :: Context String <- fromMaybeOrFail "partialWithContext: ERROR: you need to have an even number of arguments (key/value)." $ listOfStringToContext key_values
      -- Apply the template
      itemBody <$> applyTemplate template (context <> partialWithContext) page
  where
    fromMaybeOrFail :: MonadFail m => String -> Maybe a -> m a
    fromMaybeOrFail x maybe_a = maybe (fail x) pure maybe_a

listOfStringToContext :: [String] -> Maybe (Context String)
listOfStringToContext [] = Just mempty
listOfStringToContext [k] = Nothing
listOfStringToContext (k:val:r) = do
  context <- (listOfStringToContext r)
  let currentField = constField k val
  return (currentField <> context)

customDefaultContext :: Context String
customDefaultContext =
  -- Order of <> matters
  partialWithContext
  <> defaultContext

And now you can use it that way: put in your html file something like:

<h1>Publications</h1>
<ul>
    <li>$partialWithContext("templates/reference.html", "citationTitle", "My title", "citationAuthors", "Me and my friend", "citationDate", "2018", "citationPresentedAt", "A nice conference")$</li>
    <li>$partialWithContext("templates/reference.html", "citationTitle", "My title", "citationAuthors", "Me and my other friend", "citationDate", "2018", "citationPresentedAt", "A great conference")$</li>
</ul>

and create your template accordingly:

<h2 class="citationTitle">$citationTitle$</h2>
<span class="citationAuthors">$citationAuthors$</span>
<span class="citationDate">$citationDate$</span>
<span class="citationPresentedAt">$citationPresentedAt$</span>

@jaspervdj : Are you open to pull requests so that I can integrate this command into the project? I don't see lots of PR merged (included my last minor PR), and I'm not sure if it's just a lack of time to integrate them, or if you don't want any PR.

@Minoru
Copy link
Collaborator

Minoru commented Nov 9, 2020

Oh, that's nifty! I'm glad you resolved that, @tobiasBora.

I doubt this should be a part of Hakyll proper, though. Feels like a pretty specialized function to me.

@jaspervdj If you need a hand with merging PRs, please add me as a collaborator. I can field the simpler ones, so you can concentrate on just the intrusive ones whenever you have time.

@tobiasBora
Copy link
Contributor Author

Why is it specialized? It looks to me quite common to include templates and wanting to change its environment... To me, its a bit like having function without arguments: just useful to avoid copy/paste/repetitions, but pretty useless otherwise. Most template systems do have such feature, for example Jinga2/EDE have:

{% include "include.child2" with binding = "123" %}

For example, now, I need a JSON version of it because I need to loop in my template in order to create a menu. And since the templating system does not allow to create "variables" I need to rely on partials + hack.

This also brings me to another question: is it possible to integrate with other templating systems like EDE instead of the default one? Notably, how am I supposed to do the compile step?

Also, stupid question, but it's not possible to access to the parent context when you loop into a list right? Like if my context contains translationGreating = 'Bonjour ' and a list users, I can't do:

$for(users)$
$tanslationGreating$ $name$
$endfor$

to get:

Bonjour Alice
Bonjour Bob

right?

@Minoru
Copy link
Collaborator

Minoru commented Nov 10, 2020

Why is it specialized?

Because it violates Hakyll's separation of data (which should be in Contexts) and markup (which should be in templates). It may be warranted in your case, but it still feels like a specific use-case to me. If I were maintaining Hakyll, I'd wait for more people to tell me that this is something that'd be useful to them, or wait for you to go over different Hakyll sites and collect proofs that people are re-inventing this wheel all the time.

Note that Hakyll has a little ecosystem of its own. A function need not be in Hakyll proper to be re-used by others ;)

is it possible to integrate with other templating systems like EDE instead of the default one? Notably, how am I supposed to do the compile step?

It sure looks like it: there are hakyll-shakespeare and hakyll-blaze-templates already, so you can probably leverage ede to create the same. Note too that there's a Jinja2 re-implementation called gigner.

Also, stupid question, but it's not possible to access to the parent context when you loop into a list right?

Sounds like #490.

@tobiasBora
Copy link
Contributor Author

tobiasBora commented Nov 10, 2020

Because it violates Hakyll's separation of data (which should be in Contexts) and markup (which should be in templates)

And why a template could not provide some data to another template? It would allow reusing of components.

proofs that people are re-inventing this wheel all the time.

People usually take what is simpler to do, and in most cases it's simpler to just write a bit of code that do not respect the DRY guideline, or that is not super well customized, than writing technical code to fix a library. But if you like, I can give a few examples of websites that could benefit from having partial with arguments, or that could benefit to have a function that automatically turns a JSON file into a context:

  • One place where it could be useful is to write menus. Personally, I have an issue with the uikit framework that does not allow to have one code that works for both mobile and desktop, so I need to write two codes for my menu, and I'd love to have access to a partial with argument functions. Otherwise, I'd just have dirty duplicated code in my template. And apparently, I'm not the only one having issues with menu, for example this really nice website hardcode the menu items directly in Haskell. If you want to separate the templates and the data, I guess it's even more important to separate the Haskell code and the (template/data) code.

When the data does not come from the template, like for citations in articles, I found some nice ideas and some less nice:

  • this website for example creates a list of citations using pure Markdown. It has advantages (like being plain markdown), but also some drawbacks : if at some points he decides to change the styling of the references, he needs to change every single line. It is also harder to implement most advanced features, like links to .bib versions, display an abstract that you can toggle... This website on the other side has a nice solution: it creates one .yml per publication, and generates the corresponding .bib locally, and applies styling in a separate file. Really nice, the only drawback I see here is that it requires a big number of files (one per entry), but it may even be possible to avoid that by using nested yaml if hakyll allows that.

Thanks for the links, I'll try to see if I can get inspired.

So anyway, for people interested, I just created a function to call a partial with arguments coming from a json file. You just need to add a serate rule to keep track of the json file (see #809).

    match "menu.json" $ do
      compile getResourceBody

and then:

import           Data.ByteString.UTF8 as B
import qualified Data.List as DL
import qualified Data.Vector as Vector
import qualified Data.Aeson as Aeson
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Text as T

[...]
-- Use: $partialWithContextJSONFile("template.html", "menu.json")$
-- where "menu.json" contains something like:
--- {
---     "date": "10/11/2020",
---     "urls": [
---         {
---             "name": "Home",
---             "link": "/"
---         },
---         {
---             "name": "Contact",
---             "link": "/contact"
---         }
---     ]
--- }
--- And "template.html" contains something like:
--- <ul>
---   The date is $date$.<br/>
---   $for(urls)$
---   <li><a href="$link$">$name$</a></li>
---   $endfor$
--- </ul>
partialWithContextJSONFile :: Context String
partialWithContextJSONFile = functionField "partialWithContextJSONFile" $ \args page -> do
  case args of
    [pathTemplate, pathJson] -> do
      -- Load the template
      template <- loadBody (fromFilePath pathTemplate)
      -- TODO: Possible to add the current context?
      -- ### For now you need to put something like:
      -- match "menu.json" $ do
      --  compile getResourceBody
      -- ### in your compile rules.
      -- NB: this line is not enough:
      jsonBody <- B.fromString <$> loadBody (fromFilePath pathJson)
      let eitherJson = Aeson.eitherDecodeStrict' jsonBody
      case eitherJson of
        Left err -> fail $ "partialWithContextJSONFile: ERROR " ++ err
        Right (json :: Aeson.Value) ->
          let context :: Context String = contextFromJson json in
            -- Apply the template
            itemBody <$> applyTemplate template (context <> partialWithContext) page
    _ -> fail $ "partialWithContextJSONFile: ERROR: You need to specify a file.\n" ++
                "Usage: $partialWithContextJSONFile(\"template.html\", \"yourfile.json\")$"

contextFromJson :: Aeson.Value -> Context String
contextFromJson (Aeson.Object o) =
  HashMap.foldrWithKey
    (\key_txt value oldContext -> do
       let key = T.unpack key_txt
       -- Check if the key is associated with a string/number/object value in the
       -- json file
       case value of
         Aeson.Bool b -> (boolField key $ \_ -> b) <> oldContext
         Aeson.String text -> (constField key $ T.unpack text)
           <> oldContext
         Aeson.Number n -> (constField key $ show n)
           <> oldContext
         Aeson.Null -> (constField key "")
           <> oldContext
         Aeson.Object o' ->
           -- If it's an object, we try to define a notation abc.def
           let contextO' = contextFromJson (Aeson.Object o') in
             (Context $ \k args item -> do
                 case DL.stripPrefix (key ++ ".") k of
                   Nothing -> noResult $ "No result in form " ++ key ++ "."
                   Just k' -> ((unContext contextO') k' args item)) <> oldContext
         Aeson.Array v ->
            -- It's a vector of values: it means that we want to create a list!
            -- Something like "abc": [{"name": "Alice"}, {"name": "Bob"}]
            (Context $ \k args item -> do
                if k == key then
                  return $ ListField (contextInsideLoop item) (Vector.toList (Vector.map (\x -> Item "" x) v))
                else
                  noResult $ "No result in form " ++ key) <> oldContext
          where
            contextInsideLoop :: Item String -> Context Aeson.Value
            contextInsideLoop oldItem = Context f
              where
                f (key :: String) args (item :: Item Aeson.Value) =
                  let contextItem = contextFromJson (itemBody item) in
                    -- Note sure item is really meaningfull here...
                    (unContext contextItem) key args oldItem
    )
    (mempty :: Context String)
    o
contextFromJson _ = mempty

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

2 participants