Commit e1ad92fd authored by Dario Pinto's avatar Dario Pinto
Browse files

Add blog features: list by categories/authors, List categories and List...

Add blog features: list by categories/authors, List categories and List authors, link all pages together
parent a1fc79e2
type article =
{ date : int * int * int
; title : string
; authors : string list
; author : string
; tags : string list
; category : string
; content : string
; url : string
}
let string_of_file file =
let chan = open_in ("src/content/" ^ file) in
let rec aux acc chan =
match input_line chan with
| exception End_of_file -> acc
| line -> aux (acc ^ "\n" ^ line) chan
in
let res = aux "" chan in
close_in chan;
res
(** [allowed_categories] is a list of allowed categories for any article posted
on the blog *)
let allowed_categories =
[ "tooling"
; "blockchains"
; "oCamlPro"
; "formal methods"
; "trainings"
; "oCaml"
; "rust"
]
(** [raw_articles] List of all raw text in all articles in /content/blog/
subdirectory *)
let raw_articles =
List.find_all
(fun file ->
(String.length file >= 5 && String.equal (String.sub file 0 5) "blog/")
&& Filename.check_suffix file ".md" )
Content.file_list
let get_value field = List.hd (List.rev (String.split_on_char ':' field))
(** [get_meta_value field] extract the second field of meta_data required at the
beginning of the article *)
let get_meta_value field = List.hd (List.rev (String.split_on_char ':' field))
(** [extract_date date] convert date meta_data into a [(int * int * int)] type *)
let extract_date date =
match String.split_on_char '-' date with
| [ year; month; day ] ->
(int_of_string year, int_of_string month, int_of_string day)
| _ -> (0, 0, 0)
(** [article_of_string post url] convert a given raw_text article into an
[article] type *)
let article_of_string post url =
match String.split_on_char '\n' post with
| title :: authors :: date :: category :: tags :: r ->
| title :: author :: date :: category :: tags :: r ->
Some
{ date = extract_date (get_value date)
; title = get_value title
; authors =
(let authors = get_value authors in
match String.split_on_char ',' authors with
| [] -> [ "Unspecified authors!" ]
| auth -> auth )
{ date = extract_date (get_meta_value date)
; title = get_meta_value title
; author = get_meta_value author
; tags =
(let tags = get_value tags in
(let tags = get_meta_value tags in
match String.split_on_char ',' tags with
| [] -> [ "Unspecified tags!" ]
| tags -> tags )
; category = get_value category
; category =
begin
let c = get_meta_value category in
if not @@ List.mem c allowed_categories then
failwith "Category unknown";
c
end
; content = String.concat "\n" r
; url
}
| _ -> None
let post_header title authors (year, month, day) category tags =
Format.asprintf
{|<h1 id="page-title">%s</h1>
(** [get_article_data raw_articles] Returns a list of [article] types *)
let get_article_data raw_articles =
List.map
(fun article ->
match Content.read article with
| None -> failwith "invalid article data"
| Some data -> (
match
article_of_string data
(Filename.basename (Filename.chop_suffix article ".md"))
with
| None -> failwith "invalid article data"
| Some data -> data ) )
raw_articles
(** [articles_data] A list of [article] types *)
let articles_data = get_article_data raw_articles
(** [authors] The list of authors in the current pool of available articles *)
let authors =
let articles_by_authors =
List.sort_uniq (fun a1 a2 -> compare a1.author a2.author) articles_data
in
List.map (fun article -> article.author) articles_by_authors
(** [authors_count] A list of [(string * int)] when for each author [string],
his/her respective number of written articles *)
let authors_count =
let rec count acc current_author = function
| [] -> (current_author, acc)
| article :: r ->
if article.author = current_author then
count (acc + 1) current_author r
else
count acc current_author r
in
let rec map_authors_count acc = function
| [] -> acc
| current_author :: r ->
map_authors_count (count 0 current_author articles_data :: acc) r
in
List.rev (map_authors_count [] authors)
(** [categories_count] A list of [(string * int)] when for each category
[string] its corresponding number of articles *)
let categories_count =
let allowed_categories =
List.sort (fun c1 c2 -> compare c1 c2) allowed_categories
in
let rec count acc category = function
| [] -> (category, acc)
| article :: r ->
if article.category = category then
count (acc + 1) category r
else
count acc category r
in
let rec map_categories_count acc = function
| [] -> acc
| allowed_cat :: r ->
map_categories_count (count 0 allowed_cat articles_data :: acc) r
in
List.rev (map_categories_count [] allowed_categories)
let pp_cat_count fmt _categories_count = Format.fprintf fmt ""
let pp_article_excerpt fmt article =
let year, month, day = article.date in
Format.fprintf fmt
{|<div class="row">
<h3>
<a href="/blog/%s">%s</a>
</h3>
<div class="row">
<div class="col-lg-3">
<img src="/blog/assets/img/icon_person.svg" style="max-width:1em"/>
Authors: %a
<img class="icon" src="/blog/assets/img/icon_person.svg"/>
Author: %s
</div>
<div class="col-lg-3">
<img src="/blog/assets/img/icon_calendar.svg" style="max-width:1em"/>
Date: %4d-%2d-%2d
<img class="icon" src="/blog/assets/img/icon_calendar.svg"/>
Date: %4d-%02d-%02d
</div>
<div class="col-lg-3">
<img src="/blog/assets/img/icon_category.svg" style="max-width:1em"/>
<img class="icon" src="/blog/assets/img/icon_category.svg"/>
Category: %s
</div>
<div class="col-lg-3">
<img src="/blog/assets/img/icon_tags.svg" style="max-width:1em"/>
<img class="icon" src="/blog/assets/img/icon_tags.svg"/>
Tags: %a
</div>
</div>
<br/>
<br/>
<a href="/blog/%s">Read more...</a>
<hr class="featurette-divider"/>
|}
title
</div>
<br />|}
article.url article.title article.author year month day article.category
(Format.pp_print_list
~pp_sep:(fun fmt () -> Format.fprintf fmt ", ")
Format.pp_print_string )
authors year month day category
article.tags article.url
let pp_blog_posts fmt articles_data_list =
List.iter (Format.fprintf fmt "%a" pp_article_excerpt) articles_data_list
(** [specific_article_header title author (year, month, day) category tags]
prints the header for a given blog post *)
let specific_article_header title author (year, month, day) category tags =
Format.asprintf
{|<h1 id="page-title">%s</h1>
<div class="row">
<div class="col-lg-3">
<img class="icon" src="/blog/assets/img/icon_person.svg"/>
Author: %s
</div>
<div class="col-lg-3">
<img class="icon" src="/blog/assets/img/icon_calendar.svg"/>
Date: %4d-%02d-%02d
</div>
<div class="col-lg-3">
<img class="icon" src="/blog/assets/img/icon_category.svg"/>
Category: %s
</div>
<div class="col-lg-3">
<img class="icon" src="/blog/assets/img/icon_tags.svg"/>
Tags: %a
</div>
</div>
<hr class="featurette-divider"/>
|}
title author year month day category
(Format.pp_print_list
~pp_sep:(fun fmt () -> Format.fprintf fmt ", ")
Format.pp_print_string )
tags
let pp_articles fmt articles_data_list =
List.iter
(fun article ->
Format.fprintf fmt {|<a href="%s">%s</a>@.<br />|} article.url
article.title )
articles_data_list
let home_page home =
let articles =
List.find_all
(fun file ->
(String.length file >= 5 && String.equal (String.sub file 0 5) "blog/")
&& Filename.check_suffix file ".md" )
Content.file_list
(** [sub_cat category] Displays the list of articles corresponding to the
request category *)
let sub_cat category =
let articles_by_date =
List.sort (fun a1 a2 -> compare a2.date a1.date) articles_data
in
let articles_data =
List.map
(fun article ->
match Content.read article with
| None -> failwith "invalid article data"
| Some data -> (
match article_of_string data (Filename.chop_suffix article ".md") with
| None -> failwith "invalid article data"
| Some data -> data ) )
articles
let articles_of_category =
List.filter
(fun article -> String.equal article.category category)
articles_by_date
in
Format.asprintf
{|<h1 id="page-title">Articles on %s</h1>
<hr class="featurette-divider"/>
%a
|}
(String.capitalize_ascii category)
pp_blog_posts articles_of_category
(*
*
* /!\ NEED SOME PROCESSING ON AUTHOR NAMES
* REMOVE WHITESPACES FOR PROCESSING ??
*
* *)
(** [author ocp_author] Displays the list of articles written by a given
[ocp_author] *)
let author ocp_author =
let articles_by_date =
List.sort (fun a1 a2 -> compare a1.author a2.author) articles_data
in
let articles_of_author =
List.filter
(fun article -> String.equal article.author ocp_author)
articles_by_date
in
Format.asprintf
{|<h1 id="page-title">Articles on %s</h1>
<hr class="featurette-divider"/>
%a
|}
ocp_author pp_blog_posts articles_of_author
(** [category_home] List of all available categories on the Blog, along with
number of articles of given category *)
let category_home =
Format.asprintf
{|<h1 id="page-title">Blog Categories</h1>
<hr class="featurette-divider"/>
%a
|}
(Format.pp_print_list
~pp_sep:(fun fmt () -> Format.fprintf fmt "<br />")
(fun fmt (category, count) ->
Format.fprintf fmt
{|<h3><a href="/blog/category/%s">%s</a> (%d articles)</h3>|}
category
(String.capitalize_ascii category)
count ) )
categories_count
(** [authors_home] List of all available authors on the Blog, along with number
of articles for a given author *)
let authors_home =
Format.asprintf
{|<h1 id="page-title">Blog Authors</h1>
<hr class="featurette-divider"/>
%a
|}
(Format.pp_print_list
~pp_sep:(fun fmt () -> Format.fprintf fmt "<br /> ")
(fun fmt (author, count) ->
Format.fprintf fmt
{|<h3><a href="/blog/authors/%s">%s (%d articles)</h3>|} author
author count ) )
authors_count
(** [home_page] this is the home page for the blog, articles appear as excerpts
from most recent to oldest *)
let home_page =
let articles_by_date =
List.sort (fun a1 a2 -> compare a2.date a1.date) articles_data
in
Format.asprintf "%s%a" home pp_articles articles_by_date
Format.asprintf
{|<h1 id="page-title">Blog</h1>
<div class="row">
<h5>
<a href="/blog/authors">All authors here!</a>
</h5>
<h5>
<a href="/blog/category">All categories here!</a>
</h5>
</div>
<hr class="featurette-divider"/>
%a
|}
pp_blog_posts articles_by_date
......@@ -27,6 +27,10 @@ body {
max-width: 10em;
}
.icon {
max-width:1em;
}
#page-title {
text-align: center;
}
......
<h1 id="page-title">Blog</h1>
......@@ -14,10 +14,19 @@ let blog_asset_loader _root path _request =
let page path =
match Content.read (path ^ ".md") with
| None -> None
| Some page -> (
match path with
| "blog" -> Some (Omd.of_string (Blog.home_page page) |> Omd.to_html)
| _any_other_page -> Some (Omd.of_string page |> Omd.to_html) )
| Some page -> Some (Omd.of_string page |> Omd.to_html)
let sub_cat cat =
if List.exists (String.equal cat) Blog.allowed_categories then
Some (Blog.sub_cat cat)
else
None
let author ocp_author =
if List.exists (String.equal ocp_author) Blog.authors then
Some (Blog.author ocp_author)
else
None
let article path =
match Content.read ("blog/" ^ path ^ ".md") with
......@@ -27,7 +36,7 @@ let article path =
| None -> None
| Some article ->
let header =
Blog.post_header article.title article.authors article.date
Blog.specific_article_header article.title article.author article.date
article.category article.tags
in
Some (header ^ (Omd.of_string article.content |> Omd.to_html))
......@@ -44,12 +53,36 @@ let () =
| Some content ->
Dream.html
(Template.render_unsafe ~lang:"en" ~title:"title" ~content) )
; Dream.get "/blog/category" (fun _request ->
let content = Blog.category_home in
Dream.html
(Template.render_unsafe ~lang:"en" ~title:"Category" ~content) )
; Dream.get "/blog/category/:cat" (fun request ->
match sub_cat (Dream.param "cat" request) with
| None -> Dream.empty `Not_Found
| Some content ->
Dream.html
(Template.render_unsafe ~lang:"en" ~title:"cat" ~content) )
; Dream.get "/blog/authors" (fun _request ->
let content = Blog.authors_home in
Dream.html
(Template.render_unsafe ~lang:"en" ~title:"Authors" ~content) )
; Dream.get "/blog/authors/:author" (fun request ->
match author (Dream.param "author" request) with
| None -> Dream.empty `Not_Found
| Some content ->
Dream.html
(Template.render_unsafe ~lang:"en" ~title:"cat" ~content) )
; Dream.get "/blog/:title" (fun request ->
match article (Dream.param "title" request) with
| None -> Dream.empty `Not_Found
| Some content ->
Dream.html
(Template.render_unsafe ~lang:"en" ~title:"title" ~content) )
; Dream.get "/blog" (fun _request ->
let content = Blog.home_page in
Dream.html
(Template.render_unsafe ~lang:"en" ~title:"title" ~content) )
; Dream.get "/:page" (fun request ->
match page (Dream.param "page" request) with
| None -> Dream.empty `Not_Found
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment