News replaced with blog
17 Dec 2015Just a short information that now the ‘news’ section has been replaced with a blog here on yaaf.de.
At the end of this post I show all the code needed to include a basic markdown based blog to your website via FSharp.Formatting
!
Some more features will be added soon:
- Ability to comment blog posts.
- Ability to filter posts by year and tag.
The blog was build with the lovely FSharp.Formatting library (the first time I actually used it as a regular library!).
It’s a very simple in-memory implementation (which is fine for the low number of posts) and all posts are markup files in a folder which are processed to html by FSharp.Formatting
.
I added some trickery to allow embedding of title, author, date and tags into the template file (basically I read the first headline and remove it from the parsed markup file).
A blog entry file looks like this:
# Date: 2015-12-17; Title: News replaced with blog; Tags: blog,yaaf; Author: Matthias Dittrich
Just a short information that now the 'news' section has been replaced with a blog here on yaaf.de.
At the end of this post I show all the code needed to include a basic markdown based blog to your website via `FSharp.Formatting`!
Some more features will be added soon:
- Ability to comment blog posts.
- Ability to filter posts by year and tag.
... continue with markup ....
Of course while adding the blog I found a bug and fixed it… And because I was already at the FSF code I tried to help ademar with a PR which I want to merge a long time now: https://github.com/tpetricek/FSharp.Formatting/pull/331 (see related PR)
Edit 2015-12-18:
Of course I had to update some css scripts such that blog post links are properly wrapped and the embedded code is as well.
The following css does the trick for links:
#content .inbox a {
/* Prevent Links from breaking the Layout (blogposts!) */
display: inline-block;
word-break: break-all;
}
The problem was that long automatically converted links would not wrap properly, so this tells the browser that I want to break links on every character.
Then I had to handle the regular pre tag (because I modified the FSF generation to be comaptible with prismjs):
#content .inbox {
/* basically the same as margin above, but helps with pre tags */
max-width: calc(100% - 50px);
}
It’s strange that the pre-tag seems to ignore the regular wrapping rule and must be forced to show the scroll-bar by setting the max-width property of the parent element. I could see in the browser that the setup was correct as it was floating ok up to the point where it had to show the scroll-bars (and hide/wrap text).
The third problem had to do with the tables FSF is generating for F# scripts (for F# I still use the FSF defaults):
/* Fix that F# code scrolls properly with the page (50px = 2 * Margin of the inbox) */
table.pre {
table-layout: fixed;
width: calc(100% - 50px);
}
table.pre pre {
/* Show scrollbar when size is too small */
overflow: auto;
}
table.pre td.lines {
/* Align on top such that line numbers are at the correct place when the scrollbar is shown */
vertical-align: top;
}
It seems to be the regular behavior that tables do not wrap out of the box, see this.
And finally here is the box with some F# code to test the css
changes (Note: this is all the code I used to process the markdown files to html via FSF):
/// Simple in-memory database for my (quite limited number of) blogpost.
namespace Yaaf.Website.Blog
open System
/// Html content of a post
type Html = RawHtml of string
/// The title of a post
type Title = Title of string
/// The stripped title of a post
type StrippedTitle = StrippedTitle of string
type Post =
{ Date : DateTime
Title : Title
Content : Html
Teaser : Html
TipsHtml : Html
Tags : string list
Author: string }
open System.Collections.Generic
type PostDb = IDictionary<DateTime * StrippedTitle, Post>
module BlogDatabase =
open System.IO
open System.Web
open FSharp.Markdown
open FSharp.Literate
let private formattingContext templateFile format generateAnchors replacements layoutRoots =
{ TemplateFile = templateFile
Replacements = defaultArg replacements []
GenerateLineNumbers = true
IncludeSource = false
Prefix = "fs"
OutputKind = defaultArg format OutputKind.Html
GenerateHeaderAnchors = defaultArg generateAnchors false
LayoutRoots = defaultArg layoutRoots [] }
let rec private replaceCodeBlocks ctx = function
| Matching.LiterateParagraph(special) ->
match special with
| LanguageTaggedCode(lang, code) ->
let inlined =
match ctx.OutputKind with
| OutputKind.Html ->
let code = HttpUtility.HtmlEncode code
let codeHtmlKey = sprintf "language-%s" lang
sprintf "<pre class=\"line-numbers %s\"><code class=\"%s\">%s</code></pre>" codeHtmlKey codeHtmlKey code
| OutputKind.Latex ->
sprintf "\\begin{lstlisting}\n%s\n\\end{lstlisting}" code
Some(InlineBlock(inlined))
| _ -> Some (EmbedParagraphs special)
| Matching.ParagraphNested(pn, nested) ->
let nested = List.map (List.choose (replaceCodeBlocks ctx)) nested
Some(Matching.ParagraphNested(pn, nested))
| par -> Some par
let private editLiterateDocument ctx (doc:LiterateDocument) =
doc.With(paragraphs = List.choose (replaceCodeBlocks ctx) doc.Paragraphs)
let parseRawDate (rawDate:string) = DateTime.ParseExact(rawDate, "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture)
let parseRawTitle (rawTitle:string) =
// 2015-12-17: Testpost with some long title
let splitString = " - "
let firstColon = rawTitle.IndexOf(splitString)
if firstColon < 0 then failwithf "invalid title (expected instance of ' - ' to split date from title): '%s'" rawTitle
let rawDate = rawTitle.Substring(0, firstColon)
let realTitle = rawTitle.Substring(firstColon + splitString.Length)
Title (realTitle.Trim()), parseRawDate rawDate
let parseHeaderLine (line:string) =
let splitString = ": "
let firstColon = line.IndexOf(splitString)
if firstColon < 0 then failwithf "invalid header line (expected instance of ': ' to split header key from value): '%s'" line
let rawKey = line.Substring(0, firstColon)
let rawValue = line.Substring(firstColon + splitString.Length)
rawKey, rawValue
let extractHeading (doc:LiterateDocument) =
let filtered, heading =
doc.Paragraphs
|> Seq.fold (fun (collected, oldHeading) item ->
let takeItem, heading =
match oldHeading, item with
| None, (Heading(1, text)) ->
let doc = MarkdownDocument([Span(text)], dict [])
false, Some(Formatting.format doc false OutputKind.Html)
| None, _ -> true, None
| _ -> true, oldHeading
(if takeItem then item :: collected else collected), heading) ([], None)
heading, doc.With(paragraphs = List.rev filtered)
let evalutator = lazy (Some <| (FsiEvaluator() :> IFsiEvaluator))
let readPost filePath =
// parse the post markup file
let doc = Literate.ParseMarkdownFile (filePath, ?fsiEvaluator = evalutator.Value)
// generate html code from the markdown
let ctx = formattingContext None (Some OutputKind.Html) (Some true) None None
let doc =
doc
|> editLiterateDocument ctx
|> Transformations.replaceLiterateParagraphs ctx
let heading, doc = extractHeading doc
let content = Formatting.format doc.MarkdownDocument ctx.GenerateHeaderAnchors ctx.OutputKind
let rec getTeaser (currentTeaser:string) (paragraphs:MarkdownParagraphs) =
if currentTeaser.Length > 150 then currentTeaser
else
match paragraphs with
| p :: t ->
let convert = Formatting.format (doc.With(paragraphs = [p]).MarkdownDocument) ctx.GenerateHeaderAnchors ctx.OutputKind
getTeaser (currentTeaser + convert) t
| _ -> currentTeaser
let title, date, tags, author =
match heading with
| Some header ->
let headerValues =
header.TrimEnd().Split([|"; "|], StringSplitOptions.RemoveEmptyEntries)
|> Seq.map parseHeaderLine
|> dict
Title headerValues.["Title"], parseRawDate headerValues.["Date"],
(match headerValues.TryGetValue "Tags" with
| true, tags -> tags.Split([|","|], StringSplitOptions.RemoveEmptyEntries)
| _ -> [||]),
match headerValues.TryGetValue "Author" with
| true, author -> author
| _ -> "Unknown"
| None ->
let name = Path.GetFileNameWithoutExtension filePath
let title, date = parseRawTitle name
title, date, [||], "Unknown"
let tipsHtml = doc.FormattedTips
{ Date = date; Title = title; Content = RawHtml content; TipsHtml = RawHtml tipsHtml;
Tags = tags |> List.ofArray; Author = author; Teaser = RawHtml (getTeaser "" doc.Paragraphs) }
let toStrippedTitle (Title title) =
StrippedTitle (title.Substring(0, Math.Min(title.Length, 50)))
let readDatabase path : PostDb =
// Blogposts are *.md files within the given path
Directory.EnumerateFiles(path, "*.md")
|> Seq.map (readPost >> (fun p -> (p.Date, toStrippedTitle p.Title), p))
|> dict