3

In my Hakyll site I have a stylesheet linked into the page:

<link rel="stylesheet" type="text/css" href="/css/my.css">

This CSS contains a @font-face directive linking to a font file:

@font-face {
 font-family: "Bla";
 src: url("/data/bla.ttf") format("truetype");
}

The problem is that font's URL doesn't get relativized by relativizeUrls even if I move it into a <script> tag inside the page itself. How to solve this problem?

Tikhon Jelvis
  • 67,485
  • 18
  • 177
  • 214
arrowd
  • 33,231
  • 8
  • 79
  • 110

3 Answers3

2

tl;dr – You could use Beerend Lauwers's hakyll-extra package (doesn't seem to be on hackage yet), which provides a relativizeUrl macro. Or, implement your own as follows:

If you don't have too many links, and don't fancy introducing a CSS parser just in order to do this, you could just create a function field - effectively, a macro -- which allows you to call, e.g. relativize("/some/url") from within the page. (I ran into a similar problem, because I wanted to relativize links to a stylesheet for use only by old versions of Internet Explorer; and to TagSoup, the links looked as if they were within comments, so it didn't process them.)

First, we need to write a version of relativizeUrls which just operates on a single URL:

 import Data.List as L

 -- | Relativize URL. Same logic as "relativizeUrlsWith" in
 -- Hakyll.Web.Html.RelativizeUrls, but for just one url.
 relativizeUrl :: String  -- ^ Path to the site root
                    -> String  -- ^ link to relativize
                    -> String  -- ^ Resulting link
 relativizeUrl root = rel
   where
     isRel :: String -> Bool
     isRel x = "/" `L.isPrefixOf` x && not ("//" `L.isPrefixOf` x)
     rel x   = if isRel x then root ++ x else x

Then, we define a "function field" which can be added to contexts.

 import Data.Maybe (maybe)

 -- ugh. ugly name.
 relativizeFuncField :: Context a
 relativizeFuncField = functionField "relativize" relativize
   where 
     relativize :: [String] -> Item a -> Compiler String
     relativize args item = do
       siteRoot <- getRoot <$> (getRoute $ itemIdentifier item)
       arg <- case args of 
               [arg] -> return arg
               _     -> error "relativize: expected only 1 arg"
       return $ relativizeUrl siteRoot arg

     getRoot :: Maybe String -> String
     getRoot = maybe (error "relativize: couldn't get route") toSiteRoot 

Then, anywhere you want to use this macro, instead of using, say, defaultContext, use relativizeFuncField <> defaultContext. e.g.:

 import Data.Monoid( (<>) )

 main = 
   -- ... 

   match (fromList ["about.rst", "contact.markdown"]) $ do
       route   $ setExtension "html"
       compile $ pandocCompiler
           >>= loadAndApplyTemplate "templates/default.html" (relativizeFuncField <> defaultContext)
           >>= relativizeUrls

So, finally, that means that within a file, you can write $relativize("/path/to/file")$ in any spot where TagSoup isn't already relativizing links.

Hope that's of use :)
(This was using Hakyll 4.9.0.0, but I assume other 4.X versions are much the same.)

edited: p.s., many thanks to Beerend Lauwers, who explained Hakyll function fields in his post here

edited again: d'oh. I didn't see that Beerend has actually already put a relativizeUrl function in his hakyll-extra package.

phlummox
  • 234
  • 5
  • 11
1

Hakyll's relativizeURLs uses TagSoup to parse and pretty-print HTML, so it can only work on URLs found inside HTML attributes. I don't know of any existing functionality to extend this to CSS rather than just HTML attributes.

The relevant code goes through every tag parsed by TagSoup and applies a function to attributes it recognizes as URLs:

-- | Apply a function to each URL on a webpage
withUrls :: (String -> String) -> String -> String
withUrls f = withTags tag
  where
    tag (TS.TagOpen s a) = TS.TagOpen s $ map attr a
    tag x                = x
    attr (k, v)          = (k, if isUrlAttribute k then f v else v)

(From Hakyll.Web.HTML)

There's no way to change this traversal logic from the provided relativizeURLs compiler so you'll probably have to write your own. Luckily it's pretty simple: it gets the site root (with toSiteRoot), then uses withURLs to apply a function to every URL that turns absolute paths into relative ones.

relativizeUrls item = do
    route <- getRoute $ itemIdentifier item
    return $ case route of
        Nothing -> item
        Just r  -> fmap (relativizeUrlsWith $ toSiteRoot r) item

relativizeUrlsWith root = withUrls rel
  where
    isRel x = "/" `isPrefixOf` x && not ("//" `isPrefixOf` x)
    rel x   = if isRel x then root ++ x else x

(Excerpts from Hakyll.Web.RelativizeURLs).

You'll need to combine this sort of process with a lightweight CSS parser of some sort. It'll look something like this (in pseudocode):

relativizeCssUrls root = renderCSS . fmap relativize . parseCSS
  where relativize (URL url)
          | isRel url = URL (root <> url)
          | otherwise = URL url
        relativize other = other

I haven't used any CSS parsing/printing libraries so I can't give you a good suggestion here, but css-text seems like a decent starting point.

Tikhon Jelvis
  • 67,485
  • 18
  • 177
  • 214
1

I've gone an easier way. I've used this code to obtain root path relative to a current item:

rootPath :: Compiler String
rootPath = (toSiteRoot . fromJust) <$> (getUnderlying >>= getRoute)

And then created a Context with constant field:

fontCtx = do
    root <- rootPath
    return $ constField "fontRoot" root

Finally, i moved @font-face clause out of a CSS file into HTML one and used my field there:

    <style type="text/css">
        @font-face {
          ...
          src: url("$fontRoot$/data/bla.ttf") format("truetype");
        }
    </style>

That context field turned out to be quite useful in other places, like path strings in Javascript code, which I also use.

arrowd
  • 33,231
  • 8
  • 79
  • 110