HaskellDown

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

1. HaskellDown syntax

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.

1.1 The Markdown block rule

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.

1.2 The Markdown line rule

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.

1.3 The literal block rule

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 -&gt; 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).

1.4 Final remarks on the HaskellDown syntax

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.

2. The converter functions and document generation

2.1 The converters

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 Strings. 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.

2.2 The default function call and the HaskellDown manual

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 ()

Appendix A. Implementation

A.1 The exports of the HaskellDown module

module 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

A.2 The imports

  import Text.Pandoc (writeHtmlString, defaultWriterOptions, readMarkdown, defaultParserState)

A.3 Type synonyms

  type Haskell      = String
  type Markdown     = String
  type Html         = String
  type Css          = String
  type HaskellFile  = FilePath
  type MarkdownFile = FilePath
  type HtmlFile     = FilePath
  type CssFile      = FilePath

A.4 The converters

  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

A.5 Document generation and the HaskellDown Manual

  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"

Appendix B. References