pencil-1.0.0: src/Pencil/Content/Internal.hs
{-# LANGUAGE DeriveGeneric #-}
{-|
Internal implementation for functions that content.
-}
module Pencil.Content.Internal where
import Data.Hashable (Hashable)
import Data.List.NonEmpty (NonEmpty(..)) -- Import the NonEmpty data constructor, (:|)
import GHC.Generics (Generic)
import Pencil.Env.Internal
import qualified Data.Char as Char
import qualified Data.HashMap.Strict as H
import qualified Data.Maybe as M
import qualified Data.Text as T
import qualified System.FilePath as FP
-- | The @Page@ is an important data type in Pencil.
--
-- Source files like Markdown and HTML are loaded (e.g. via 'Pencil.Content.load') as a @Page@.
-- A page contains the contents of the file, their un-evaluated [template
-- directives](https://elbenshira.com/pencil/guides/templates/) (e.g.
-- @${body}@), the variables defined in the preamble, and the destination file
-- path.
--
-- The contents /may/ be in its converted form. 'Pencil.Content.load' will convert Markdown to
-- HTML, for example.
--
-- Pages can be /combined/ together into a 'Structure', or inserted into the
-- environment (see 'Pencil.Content.insertPages'). But at the end of the day, even a structure
-- is converted back into a page on 'Pencil.Content.render'. This is because it is the page
-- that is finally rendered into an actual web page when you run your program.
--
data Page = Page
{ pageEnv :: Env
, pageFilePath :: FilePath
-- ^ The rendered output path of this page. Defaults to the input file path.
-- This file path is used to generate the self URL that is injected into the
-- environment.
, pageUseFilePath :: Bool
-- ^ Whether or not this Page's URL should be used as the final URL in the
-- render.
, pageEscapeXml :: Bool
-- ^ Whether or not XML/HTML tags should be escaped when rendered.
} deriving (Eq, Show)
-- | Gets the Env of a 'Page'.
getPageEnv :: Page -> Env
getPageEnv = pageEnv
-- | Sets the Env of a 'Page'.
setPageEnv :: Env -> Page -> Page
setPageEnv env p = p { pageEnv = env }
-- | Sets this 'Page' as the designated final 'FilePath'.
--
-- This is useful when you are building a 'Structure' but don't want the file
-- path of the last 'Page' in the structure to be the destination file path on
-- render.
--
-- The [Pages and
-- Structures](https://elbenshira.com/pencil/guides/pages-and-structures/) guide
-- describes this in detail.
--
-- @
-- a <- load "a.html"
-- b <- load "b.html"
-- c <- load "c.html"
--
-- -- Rendered file path is "c.html"
-- render $ a <|| b <| c
--
-- -- Rendered file path is "b.html"
-- render $ a <|| useFilePath b <| c
-- @
--
useFilePath :: Page -> Page
useFilePath p = p { pageUseFilePath = True }
-- | Sets this 'Page' to render with escaped XML/HTML tags.
--
-- This is useful when you are building an RSS feed, and you need the /contents/
-- of each item in the feed to HTML-escaped.
--
-- @
-- rss <- load "rss.xml"
-- item1 <- load "item1.html"
--
-- render $ rss <|| escapeXml item1
-- @
--
escapeXml :: Page -> Page
escapeXml p = p { pageEscapeXml = True }
-- | A @Structure@ is a list of 'Page's, defining a nesting order. Think of them
-- like <https://en.wikipedia.org/wiki/Matryoshka_doll Russian nesting dolls>.
-- The first element defines the outer-most container, and subsequent elements
-- are /inside/ the previous element.
--
-- You commonly use @Structure@s to insert a page containing content (e.g. a blog
-- post) into a container (e.g. a layout shared across all your web pages).
--
-- Build structures using 'Pencil.Content.struct', 'Pencil.Content.<||' and 'Pencil.Content.<|'.
--
-- @
-- layout <- load "layout.html"
-- index <- load "index.markdown"
-- about <- load "about.markdown"
-- render (layout <|| index)
-- render (layout <|| about)
-- @
--
-- In the example above we load a layout page, which defines the outer HTML
-- structure like @\<html\>\<\/html\>@. We then "push" the index page and the
-- about page into the layout.
--
-- When we 'Pencil.Content.render' @layout <|| index@, the contents of the index (and about)
-- page is injected into the layout page through the variable @${body}@. So
-- @layout.html@ must use @${body}@ somewhere in its own body.
--
-- Structures also control the closure of variables. Variables defined in a
-- page are accessible both by pages above and below. This allows inner
-- pages to define variables like the blog post title, which may be used in
-- the outer page to, say, set the @\<title\>@ tag.
--
-- In this way, structures allows efficient page reuse. See the private function
-- 'Pencil.Content.Internal.apply' to learn more about how structures are evaluated.
--
-- /The Default File Path Rule/. When a structure is rendered, the /last/
-- non-collection page in the structure is used as the destination file path.
-- You can select a different page via 'useFilePath'.
--
-- The [Pages and
-- Structures](https://elbenshira.com/pencil/guides/pages-and-structures/) guide
-- also describes structures in detail.
--
-- Note that structures differ from the @${partial(...)}@ directive, which has no
-- such variable closures. The partial directive is much simpler—think of them
-- as copy-and-pasting snippets from one file to another. A partial has
-- the same environment as the context in which the partial directive appears.
--
--
data Structure = Structure
{ structureNodes :: NonEmpty Node
, structureFilePath :: FilePath
, structureFilePathFrozen :: Bool
-- ^ True if the file path should no longer be changed. This happens when a
-- page with `useFilePath = True` was pushed into the structure.
}
-- | An inner element in the Structure. Either a singular Page, or a collection
-- of Pages. The Text element is the variable name that the inner page's content
-- is injected as. Defaults to @"body"@.
data Node =
Node T.Text Page
| Nodes T.Text [Page]
-- | @Resource@ is used to copy static binary files to the destination, and to
-- load and render files that just needs conversion without template directives
-- or structures.
--
-- This is how Pencil handles files like images, compiled JavaScript, or text
-- files that require only a straight-forward conversion.
--
-- Use 'Pencil.Content.passthrough', 'Pencil.Content.loadResource' and
-- 'Pencil.Content.loadResources' to build a @Resource@ from a file.
--
-- In the example below, @robots.txt@ and everything in the @images/@ directory
-- will be rendered as-is.
--
-- @
-- passthrough "robots.txt" >>= render
-- passthrough "images/" >>= render
-- @
--
data Resource
= Single Page
| Passthrough FilePath FilePath
-- ^ in and out file paths (can be dir or files)
-- | Enum for file types that can be parsed and converted by Pencil.
data FileType = Html
| Markdown
| Css
| Sass
| Other
deriving (Eq, Generic)
-- | 'Hashable' instance of @FileType@.
instance Hashable FileType
-- | A 'H.HashMap' of file extensions (e.g. @markdown@) to 'FileType'.
--
-- * 'Html': @html, htm@
-- * 'Markdown': @markdown, md@
-- * 'Css': @css@
-- * 'Sass': @sass, scss@
--
fileTypeMap :: H.HashMap String FileType
fileTypeMap = H.fromList
[ ("html", Html)
, ("htm", Html)
, ("markdown", Markdown)
, ("md", Markdown)
, ("css", Css)
, ("sass", Sass)
, ("scss", Sass)
]
-- | Mapping of 'FileType' to the final converted format. Only contains
-- 'FileType's that Pencil will convert.
--
-- * 'Markdown': @html@
-- * 'Sass': @css@
--
extensionMap :: H.HashMap FileType String
extensionMap = H.fromList
[ (Markdown, "html")
, (Sass, "css")]
-- | Converts a 'FileType' into its converted webpage extension, if Pencil would
-- convert it (e.g. Markdown to HTML).
--
-- >>> toExtension Markdown
-- Just "html"
--
toExtension :: FileType -> Maybe String
toExtension ft = H.lookup ft extensionMap
-- | Takes a file path and returns the 'FileType', defaulting to 'Other' if it's
-- not a supported extension.
fileType :: FilePath -> FileType
fileType fp =
-- takeExtension returns ".markdown", so drop the "."
M.fromMaybe Other (H.lookup (map Char.toLower (drop 1 (FP.takeExtension fp))) fileTypeMap)
-- | Returns True if the file path is a directory.
-- Examples: foo/bar/
-- Examples of not directories: /foo, foo/bar, foo/bar.baz
isDir :: FilePath -> Bool
isDir fp = null (FP.takeBaseName fp)
-- | Replaces the file path's extension with @.html@.
--
-- @
-- rename toHtml \<$\> 'Pencil.Content.load' "about.htm"
-- @
--
toHtml :: FilePath -> FilePath
toHtml fp = FP.dropExtension fp ++ ".html"
-- | Converts a file path into a directory name, dropping the extension.
-- Pages with a directory as its file path is rendered as an index file in that
-- directory.
--
-- For example, @pages/about.html@ is transformed into @pages\/about\/@, which
-- upon 'Pencil.Content.render' results in the destination file path @pages\/about\/index.html@:
--
-- @
-- toDir "pages/about.html"
-- @
--
-- Load and render as @pages\/about\/@:
--
-- @
-- render $ 'rename' toDir \<$\> 'Pencil.Content.load' "pages/about.html"
-- @
--
toDir :: FilePath -> FilePath
toDir fp = FP.replaceFileName fp (FP.takeBaseName fp) ++ "/"
-- | Replaces the file path's extension with @.css@.
--
-- @
-- rename toCss \<$\> 'Pencil.Content.load' "style.sass"
-- @
--
toCss :: FilePath -> FilePath
toCss fp = FP.dropExtension fp ++ ".css"
-- | Converts file path into the expected extensions. This means @.markdown@
-- become @.html@, @.sass@ becomes @.css@, and so forth. See 'extensionMap' for
-- conversion table.
toExpected :: FilePath -> FilePath
toExpected fp = maybe fp ((FP.dropExtension fp ++ ".") ++) (toExtension (fileType fp))
-- | Transforms the file path.
--
-- @
-- about <- load "about.htm"
-- render $ struct (rename 'toHtml' about)
-- @
rename :: HasFilePath a => (FilePath -> FilePath) -> a -> a
rename f a = setFilePath (f (getFilePath a)) a
-- | Sets the target file path to the specified file path. If the given file path
-- is a directory, the file name set to @index.html@. If the file path is a file
-- name, then the file is renamed.
--
-- Move @stuff/about.html@ to @about/blah.html@ on render:
--
-- > about <- to "about/blah.html" <$> load "stuff/about.htm"
--
-- Convert the destination file path to @about/index.html@:
--
-- > about <- to "about/" <$> load "stuff/about.htm"
-- > render about
--
-- Equivalent to the above example:
--
-- > about <- load "stuff/about.htm"
-- > render $ to "about/" about
--
to :: HasFilePath a => FilePath -> a -> a
to = move' "index.html"
-- | Moves the target file path to the specified file path. Behaves similar to
-- the UNIX @mv@ command: if the given file path is a directory, the file name
-- is kept the same. If the file path is a file name, then the file is renamed.
--
-- Move @assets/style.css@ to @stylesheets/style.css@:
--
-- > move "stylesheets/" <$> load "assets/style.css"
--
-- Move @assets/style.css@ to @stylesheets/base.css@.
--
-- > move "stylesheets/base.css" <$> load "assets/style.css"
--
move :: HasFilePath a => FilePath -> a -> a
move fp a = move' (FP.takeFileName (getFilePath a)) fp a
-- | Internal implemenation for 'move' and 'to'.
--
-- Moves the target file path to the specified FilePath. If the given FilePath
-- is a directory, the file name is kept the same. If the FilePath is a file
-- name, then @fromFileName@ is used as the file name.
move' :: HasFilePath a => FilePath -> FilePath -> a -> a
move' fromFileName fp a =
let dir = FP.takeDirectory fp
fp' = if isDir fp
then dir ++ "/" ++ fromFileName
else dir ++ "/" ++ FP.takeFileName fp
in setFilePath fp' a
----------------------------------------------------------------------
-- HasFilePath class
----------------------------------------------------------------------
-- | Class for types that has a final file path for rendering.
--
-- This allows file-path-changing methods to be re-used across 'Pencil.Content.Internal.Page',
-- 'Pencil.Content.Internal.Structure' and 'Pencil.Content.Internal.Resource' types.
class HasFilePath a where
getFilePath :: a -> FilePath
setFilePath :: FilePath -> a -> a
instance HasFilePath Page where
getFilePath = pageFilePath
setFilePath fp p = p { pageFilePath = fp }
instance HasFilePath Resource where
getFilePath (Single p) = getFilePath p
getFilePath (Passthrough _ fp) = fp
setFilePath fp (Single p) = Single $ setFilePath fp p
setFilePath fp (Passthrough ofp _) = Passthrough ofp fp
instance HasFilePath Structure where
getFilePath = structureFilePath
setFilePath fp s = s { structureFilePath = fp }