HaskellDown is a simple method to generate HTML documents from Haskell files, by using the plain Markdown text formatting syntax.
HaskellDown.hs
is the Haskell module that provides the functions to conveniently perform these conversions.
HaskellDown is thus similar to the standard Haskell documentation tool Haddock, but uses a different approach.
Table of contents
1. HaskellDown syntax
1.1 The Markdown block rule
1.2 The Markdown line rule
1.3 The literal block rules
1.4 Final remarks on the Markdown syntax
2. The converter functions and document generation
2.1 The converters
2.2 The default function call and the HaskellDown manual
Appendix A. Implementation
Appendix B. References
HakellDown comprises just three simple syntax rules that modify ordinary Haskell comments so that they become text parts that will be converted into first Markdown and then HTML.
A Markdown block has the form
{--- ... this is the Markdown text part ... ---}
It starts after a single line beginning with
{---
and ends before a single line that begins with---}
.
For example, this Hakell comment with Markdown code
{---
### __Haskell__ properties
* purely functional
* strongly typed
* really lazy
---}
will turn into
<h3><strong>Haskell</strong> properties</h3>
<ul>
<li>purely functional</li>
<li>strongly typed</li>
<li>really lazy</li>
</ul>
Note, that the comment delimiters {---
and ---}
both have to be at the beginning of a line.
A Markdown line has the form
-- -- ... this is the Markdown text part ...
Everything after
-- --
(i.e. two dashes, one space, two dashes, one space) is considered Markdown and converted accordingly.
Note, the delimiter -- --
has to be at the beginning of a line and that the Markdown really starts after the last space symbol, not directly after the last dash. For example, a line beginning with
-- -- # A new chapter in _literal programming_
will turn into
<h1>A new chapter in <em>literal programming</em></h1>
But this line
-- --# A new chapter in _literal programming_
will be ignored and not recognized as a Markup line.
A literal block has the form
-- -- -- ... Haskell source code ... -- -- --
The beginning and end of a literal block is indicated by a line that starts with
-- -- --
(i.e. three double slashes, separated by a space, each). Everything between these delimiter lines is considered literal Haskell code and will be displayed as such.
For example,
-- -- --
triple :: Int -> Int
triple n = 3 * n
-- -- --
will after the conversion to Markdown be turned into the same block, but all lines indented by four spaces
triple :: Int -> Int
triple n = 3 * n
and that will be translated into HTML as
<pre><code>triple :: Int -> Int
triple n = 3 * n
</code></pre>
Note, that everything else in a line that starts with -- -- --
is cut off. This means, we can use additional comments to help structuring literal blocks. We could have written our previous example as
-- -- -- START OF LITERAL BLOCK
triple :: Int -> Int
triple n = 3 * n
-- -- -- FINISH OF LITERAL BLOCK
and would still have obtained the same results.
Also note, that with this rule, we can not only expose the type signature of a function, as in Haddock, like so:
-- -- --
triple :: Int -> Int
-- -- --
triple n = 3 * n
but we can also choose to automatically put the whole function definition into the documentation (as one of the key ideas in Literal Programming).
You should not try to nest these rules in any way. For example, don't put a literal block inside a Markdown block.
Everything else, i.e. all code of the Haskell source, which is neither in a Markdown block, or on a Markdown line, or inside a literal block, is ignored by HaskellDown and will not appear after the conversion.
The main converter has the following type
haskellToHtml
Haskell ---------------------------------------------------------------> Html
and that is the composition of two converters
haskellToMarkdown markdownToHtml
Haskell --------------------------> Markdown --------------------------> Html
A call of haskellToMarkdown
takes the Haskell source code, extracts the Markdown parts from the {--- ... ---}
and -- --
comment and indents the literal blocks to suit the Markdown syntax for code blocks; all that as described in part 1. The markdownToHtml
then does the conversion as described in the Markdown standard. I used the implementation provided by the Pandoc module, which is part of the Haskell Platform.
In these type signatures Haskell
, Markdown
, and Html
are type synonyms for String
s. But there are also versions, that work on files (i.e. file names), containing the Haskell
, Markdown
, and Html
code, respectively. In each of the functions, the first argument is the source code file and the second argument is the (name of the) target file:
haskellToHtmlFile :: HaskellFile -> HtmlFile -> IO ()
haskellToMarkdownFile :: HaskellFile -> MarkdownFile -> IO ()
markdownToHtmlFile :: MarkdownFile -> HtmlFile -> IO ()
For example, we can apply that to this HaskellDown.hs
file. Start the ghci
, load the module
Prelude> :l HaskellDown.hs
and call
*HaskellDown> haskellToHtmlFile "HaskellDown.hs" "HaskellDownManual.html"
for the generation of the HTML file HaskellDownManual.html
and
*HaskellDown> haskellToMarkdownFile "HaskellDown.hs" "HaskellDownManual.markdown"
if you rather need the Markdown version in HaskellDownManual.markdown
.
haskellToHtml
(or haskellToHtmlFile
) is indeed the core conversion. But next to this pure HTML result, it would be nice for most practical purposes to have some more options. In a compromise of flexibility and simplicity, I choose two more standard arguments: a HTML document title
and a cssFile
. So our name and type for the default converter is:
haskellDown :: HaskellFile -> String -> CssFile -> HtmlFile -> IO ()
For example, if HaskellDown.css
is the name of a default CSS file, we can generate a nicely displayed document called HaskellDownManual.html
of this given HaskellDown.hs
source file by calling
haskellDown "HaskellDown.hs" "The HaskellDown Manual" "HaskellDown.css" "HaskellDownManual.html"
Afterwards, the content of HaskellDownManual.html
will be something like this:
<html>
<head>
<title>The HaskellDown Manual</title>
<style type="text/css"> ... content of HaskellDown.css ... </style>
</head>
<body>
... content of HaskellDown.hs, converted to HTML ...
</body>
</html>
To produce the same result file HaskellDownManual.html
, we actually provide another function, that does exactly that:
makeHaskellManual :: IO ()
HaskellDown
modulemodule HaskellDown (
-- * Type synonyms
Haskell, Markdown, Html, Css,
HaskellFile, MarkdownFile, HtmlFile, CssFile,
-- * The converter functions
haskellToMarkdown,
markdownToHtml,
haskellToHtml,
haskellToMarkdownFile,
markdownToHtmlFile,
haskellToHtmlFile,
-- * The default function call and the HaskellDown manual
haskellDown,
makeHaskellDownManual,
) where
import Text.Pandoc (writeHtmlString, defaultWriterOptions, readMarkdown, defaultParserState)
type Haskell = String
type Markdown = String
type Html = String
type Css = String
type HaskellFile = FilePath
type MarkdownFile = FilePath
type HtmlFile = FilePath
type CssFile = FilePath
data Mode = HASKELL | MARKDOWN | LITERAL -- only for use in the following function
haskellToMarkdown :: Haskell -> Markdown
haskellToMarkdown = unlines . (iter HASKELL) . lines
where iter :: Mode -> [Haskell] -> [Markdown]
iter HASKELL [] = []
iter _ [] = error "Source code did not terminate in proper HASKELL mode!"
iter HASKELL (row:rows) = if (take 4 row) == "{---"
then (drop 4 row) : (iter MARKDOWN rows)
else if (take 8 row) == "-- -- --"
then "" : (iter LITERAL rows)
else if (take 6 row) == "-- -- "
then (drop 6 row) : (iter HASKELL rows)
else iter HASKELL rows
iter MARKDOWN (row:rows) = if (take 4 row) == "---}"
then iter HASKELL rows
else row : (iter MARKDOWN rows)
iter LITERAL (row:rows) = if (take 8 row) == "-- -- --"
then "" : (iter HASKELL rows)
else (" " ++ row) : (iter LITERAL rows)
markdownToHtml :: Markdown -> Html
markdownToHtml = writeHtmlString defaultWriterOptions . readMarkdown defaultParserState
haskellToHtml :: Haskell -> Html
haskellToHtml = markdownToHtml . haskellToMarkdown
haskellToMarkdownFile :: HaskellFile -> MarkdownFile -> IO ()
haskellToMarkdownFile haskellSourceFile markdownTargetFile =
do haskell <- readFile haskellSourceFile
let markdown = haskellToMarkdown haskell
writeFile markdownTargetFile markdown
markdownToHtmlFile :: MarkdownFile -> HtmlFile -> IO ()
markdownToHtmlFile markdownSourceFile htmlTargetFile =
do markdown <- readFile markdownSourceFile
let html = markdownToHtml markdown
writeFile htmlTargetFile html
haskellToHtmlFile :: HaskellFile -> HtmlFile -> IO ()
haskellToHtmlFile haskellSourceFile htmlTargetFile =
do haskell <- readFile haskellSourceFile
let html = haskellToHtml haskell
writeFile htmlTargetFile html
htmlDocument :: String -> Css -> Html -> Html
htmlDocument title css htmlBody =
"<html>\n" ++
"<head>\n" ++
"<title>" ++ title ++ "</title>\n" ++
"<style type=\"text/css\">\n" ++ css ++ "</style>" ++
"</head>\n" ++
"<body>\n" ++ htmlBody ++ "</body>\n" ++
"</html>\n"
defaultCssFile :: CssFile
defaultCssFile = "HaskellDown.css"
haskellDown :: HaskellFile -> String -> CssFile -> HtmlFile -> IO ()
haskellDown haskellSourceFile title cssFile htmlTargetFile =
do haskell <- readFile haskellSourceFile
css <- if null cssFile
then return ""
else readFile cssFile
let htmlBody = haskellToHtml haskell
let doc = htmlDocument title css htmlBody
writeFile htmlTargetFile doc
haskellDownManual :: HtmlFile
haskellDownManual = "HaskellDownManual.html"
makeHaskellDownManual :: IO ()
makeHaskellDownManual =
haskellDown "HaskellDown.hs" "The HaskellDown Manual" defaultCssFile "HaskellDownManual.html"