Why? Because I can, and it is a rainy Sunday.
I post my notes on my blog, Hashnode and dev.to, which require slightly different markdown formats. I have been doing the sane thing to fix formats so far. But it is a rainy Sunday and I am bored, so I decided to make this blog post an executable Haskell program to do the same.
The Non-Problem
I am blogging on https://thenegation.com. It is a static site generated by Zola in a Nix shell. I write my blog posts in Markdown.
After I publish a post on my blog, I cross-post it to my dev.to and Hashnode blogs with the hope of reaching a wider audience. This is a manual process. One of the steps in this process is to copy the post content in Markdown format that is compatible with the target platform.
The Friendly Solution
I do not care much about the specifics of the original and target Markdown formats. Following assumptions worked so far: The original format uses newlines as soft-wraps, and the target format may use them as hard-wraps. Also, the output format does not need a front-matter.
So, following was sufficient and convenient:
pandoc \
--from markdown \
--to markdown \
--wrap=none \
--strip-comments=true \
content/posts/2024-08-04_abuse-haskell.md
What is wrong with this solution? It is too boring to my liking.
Alternative Solutions
There are a few alternatives to this solution:
- Stop blogging
- Do not cross-post
- Use standard Markdown everywhere
- Write a Visual Basic script to do the conversion
Clearly, none of these solutions would offend anyone enough to be interesting. So, pass!
The Ultimate Solution
What if I would write a blog post that would be an executable that I can use to convert my blog posts to the target format?
This is a terrible idea. So, let’s do it!
The Plan
This very blog post is going to be a literate Haskell program that I can compile with ghc
. Then, I will use the compiled executable to convert the format of my Markdown files removing soft line-wraps, front-matter and HTML comments.
Since I am using a Nix shell for my blog’s codebase, I will provision a GHC inside it:
{
##...
ghc = pkgs.haskellPackages.ghcWithPackages (hpkgs: [
hpkgs.markdown-unlit
hpkgs.pandoc
]);
thisShell = pkgs.mkShell {
buildInputs = [
## ...
ghc
## ...
];
NIX_GHC = "${ghc}/bin/ghc";
NIX_GHCPKG = "${ghc}/bin/ghc-pkg";
NIX_GHC_DOCDIR = "${ghc}/share/doc/ghc/html";
NIX_GHC_LIBDIR = "${ghc}/lib/ghc-9.6.5/lib";
};
# ...
}
I am so ready for the implementation…
The Implementation
Let’s start with adding some MSG for tastier Haskell, also known as GHC Language Extensions:
{-# LANGUAGE OverloadedStrings #-}
We need some imports:
import System.Environment (getArgs)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import qualified Text.Pandoc as P
Our workhorse function is unfortunately quite simple:
convert :: T.Text -> IO T.Text
convert txt = P.runIOorExplode $ do
md <- P.readMarkdown readerOptions txt
P.writeMarkdown writerOptions md
where
readerOptions = P.def
{ P.readerExtensions = P.enableExtension P.Ext_yaml_metadata_block $ P.getDefaultExtensions "markdown"
, P.readerStripComments = True
}
writerOptions = P.def
{ P.writerExtensions = P.githubMarkdownExtensions
, P.writerWrapText = P.WrapNone
}
Now, we can implement our entrypoint function:
main :: IO ()
main = do
path <- head <$> getArgs
iTxt <- TIO.readFile path
oTxt <- convert iTxt
TIO.putStrLn oTxt
We are done with the program. We can run our blog post via runhaskell
on our blog post. But first, we need to symlink our Markdown file (.md
) with a literate Haskell file extension (.lhs
) so that GHC is not upset:
ln -sr \
content/posts/2024-08-04_abuse-haskell.md \
content/posts/2024-08-04_abuse-haskell.lhs
Then, we can run the blog post on the blog post itself:
runhaskell \
-pgmLmarkdown-unlit \
content/posts/2024-08-04_abuse-haskell.lhs \
content/posts/2024-08-04_abuse-haskell.md
We can even compile our blog post into an executable and use it later:
$ ghc -pgmLmarkdown-unlit content/posts/2024-08-04_abuse-haskell.lhs
[1 of 2] Compiling Main ( content/posts/2024-08-04_abuse-haskell.lhs, content/posts/2024-08-04_abuse-haskell.o )
[2 of 2] Linking content/posts/2024-08-04_abuse-haskell
$ content/posts/2024-08-04_abuse-haskell content/posts/2024-08-04_abuse-haskell.md
I even added a script in my Nix shell that aliases above so that I can use it all my life:
dev-md-format content/posts/2024-08-04_abuse-haskell.md
Wrap-Up
This solution will help me continue abusing Haskell in the future. It was stupid enough, as well, to deserve its own GitHub template repository:
https://github.com/vst/literate-haskell-nix-example
You can play with it, or see it in action in my blog’s source code.
Joke aside; rain is over.