Compare commits

..

4 Commits

7 changed files with 160 additions and 64 deletions

View File

@ -1,5 +1,16 @@
# Revision history for hablo
## 1.1.0.1 -- 2021-01-20
* Ensure compilation on Nix as far as 18.09
* Fix missing metadata when landing on articles
## 1.1.0.0 -- 2020-12-13
* Implement static pages
* Implement RSS feeds
* Use SJW to pack JS into a single script and simplify deployment
## 1.0.3.0 -- 2019-12-21
* Fix OpenGraph cards displayed for links to hablo-generated pages posted on the Fediverse (should work elsewhere too but I don't care and have never tested)

27
ghc-template.scm Normal file
View File

@ -0,0 +1,27 @@
(use-modules (gnu packages haskell-xyz)
(gnu packages haskell-web)
(guix build-system haskell)
(guix download)
(guix gexp)
(guix git-download)
(guix licenses)
(guix packages))
(package
(name "ghc-template")
(version "0.2.0.10")
(source (origin
(method url-fetch)
(uri (hackage-uri "template" version))
(sha256
(base32
"10mcnhi2rdflmv79z0359nn5sylifvk9ih38xnjqqby6n4hs7mcg"))))
(build-system haskell-build-system)
(properties '((upstream-name . "template")))
(home-page "http://hackage.haskell.org/package/template")
(synopsis "Simple string substitution")
(description
"Simple string substitution library that supports \\\"$\\\"-based substitution.
Meant to be used when Text.Printf or string concatenation would lead to code
that is hard to read but when a full blown templating system is overkill.")
(license bsd-3))

44
guix.scm Normal file
View File

@ -0,0 +1,44 @@
(use-modules (gnu packages haskell-xyz)
(gnu packages haskell-web)
(guix build-system haskell)
(guix download)
(guix gexp)
(guix git-download)
(guix licenses)
(guix packages)
(loom packages sjw))
(let
((%source-dir (dirname (current-filename)))
(ghc-template (load "ghc-template.scm")))
(package
(name "hablo")
(version "devel")
(source
(local-file %source-dir
#:recursive? #t
#:select? (git-predicate %source-dir)))
(build-system haskell-build-system)
(inputs
(list ghc-aeson
ghc-attoparsec
ghc-lucid
ghc-optparse-applicative
ghc-parsec
ghc-random
ghc-sjw
ghc-template
ghc-xdg-basedir))
(native-search-paths
(list
(search-path-specification (variable "SJW_PATH")
(files '("lib/SJW")))))
(home-page "https://git.marvid.fr/Tissevert/SJW")
(synopsis "The Simple Javascript Wrench")
(description
"SJW is a very simple tool to pack several JS modules into a single
script. It doesn't really do proper compilation work (yet) except
resolving the modules dependencies and detecting import loops but it
provides each module with an independent execution context in the
resulting script.")
(license gpl3+)))

View File

@ -3,16 +3,16 @@ cabal-version: >= 1.10
-- For further documentation, see http://haskell.org/cabal/users-guide/
name: hablo
version: 1.0.3.0
version: 1.1.0.1
synopsis: A minimalist static blog generator
description:
Hablo is a fediverse-oriented static blog generator for articles written
in Markdown. It tries to generate as little HTML as needed and uses
Javascript to implement dynamic features in the browser.
Those features include the handling of comments and a cached navigation
to minimize the queries to the server. Hablo also generate cards for all
pages, including articles for prettier shares on social-networks.
Those features include the handling of comments and a cached navigation to
minimize the number of queries to the server. Hablo also generates RSS feeds
and Open Graph cards for prettier shares on social networks.
homepage: https://git.marvid.fr/Tissevert/hablo
-- bug-reports:
license: BSD3
@ -50,15 +50,15 @@ library
, Pretty
, RSS
-- other-extensions:
build-depends: aeson >= 1.4.0 && < 1.6
, base >= 4.9.1 && < 4.15
build-depends: aeson >= 1.2.0 && < 2.1
, base >= 4.9.1 && < 4.17
, bytestring >= 0.10.8 && < 0.12
, containers >= 0.5.11 && < 0.7
, directory >= 1.3.1 && < 1.4
, filepath >= 1.4.2 && < 1.5
, lucid >= 2.9.11 && < 2.10
, lucid >= 2.8.0 && < 2.12
, mtl >= 2.2.2 && < 2.3
, optparse-applicative >= 0.14.3 && < 0.17
, optparse-applicative >= 0.14.0 && < 0.18
, parsec >= 3.1.13 && < 3.2
, template >= 0.2.0 && < 0.3
, text >= 1.2.3 && < 1.3
@ -73,7 +73,7 @@ executable hablo
main-is: src/Main.hs
other-modules: Paths_hablo
-- other-extensions:
build-depends: base >= 4.9.1 && < 4.15
build-depends: base
, hablo
, mtl >= 2.2.2 && < 2.3
ghc-options: -Wall

View File

@ -7,15 +7,37 @@ import {defined} from UnitJS.Fun;
return {
articlesList: articlesList,
getResource: getResource,
render: render,
replaceMarkdown: replaceMarkdown
};
function getResource(url) {
var i = url.lastIndexOf('/');
var path = url.slice(1, i);
if(path == blog.path.articlesPath) {
return {type: 'article', key: url.slice(i+1).replace(/\.html/, '')};
} else if(path == blog.path.pagesPath) {
return {type: 'page', key: url.slice(i+1).replace(/\.html/, '')};
} else if(path == '' || blog.tags[path] != undefined) {
var tag = path.length > 0 ? path : undefined;
return {type: 'list', tag: tag, all: url.slice(i+1) == 'all.html'};
} else {
return {type: 'unknown'};
}
}
function resourceUrl(resource, limit) {
var directory = blog.path[resource.type + 'sPath'];
var extension = limit != undefined ? '.html' : '.md';
return ["", directory, resource.key + extension].join('/');
}
function replaceMarkdown() {
var div = document.getElementById('contents');
if(div.children[0] && div.children[0].tagName.toLowerCase() == 'article') {
var contentType = window.location.pathname.slice(1).replace(/\/.*/, '');
convertContent(contentType, div.children[0], true);
var resourceType = getResource(window.location.pathname).type;
convertContent(resourceType, div.children[0], true);
} else {
var articles = div.getElementsByClassName('articles')[0];
if(articles != undefined) {
@ -28,15 +50,15 @@ function replaceMarkdown() {
}
}
function convertContent(contentType, article, comments) {
function convertContent(resourceType, article, comments) {
var header = article.getElementsByTagName('header')[0];
if(contentType == 'article') {
if(resourceType == 'article') {
header.appendChild(Metadata.get(article.id));
}
var text = article.getElementsByTagName('pre')[0];
if(text != undefined) {
article.replaceChild(getDiv(text.innerText), text);
if(contentType == 'article' && comments) {
if(resourceType == 'article' && comments) {
Metadata.getComments(article.id)
.forEach(article.appendChild.bind(article));
}
@ -59,59 +81,53 @@ function getDiv(markdown) {
return d;
}
function contentUrl(contentType, key, limit) {
var directory = blog.path[contentType + 'sPath'];
var extension = limit != undefined ? '.html' : '.md';
return ["", directory, key + extension].join('/');
}
function commentsSection(contentType, key, limit) {
if(contentType != 'article' || limit != undefined) {
function commentsSection(resource, limit) {
if(resource.type != 'article' || limit != undefined) {
return [];
} else {
return Metadata.getComments(key);
return Metadata.getComments(resource.key);
}
}
function render(contentType, key, markdown, limit) {
var url = contentUrl(contentType, key, limit);
var resource = blog[contentType + 's'][key];
var lines = markdown.split(/\n/).slice(resource.bodyOffset);
function render(resource, markdown, limit) {
var url = resourceUrl(resource, limit);
var content = blog[resource.type + 's'][resource.key];
var lines = markdown.split(/\n/).slice(content.bodyOffset);
var div = getDiv(lines.slice(0, limit).join('\n'));
return Dom.make('article', {}, [
Dom.make('header', {}, [
Dom.make('h1', {}, [
Dom.make('a', {href: url, innerText: resource.title})
])].concat(contentType == 'article' ? Metadata.get(key) : [])
Dom.make('a', {href: url, innerText: content.title})
])].concat(resource.type == 'article' ? Metadata.get(resource.key) : [])
),
div
].concat(commentsSection(contentType, key, limit)));
].concat(commentsSection(resource, limit)));
}
function pageTitle(tag, all) {
return Template.render(all ? 'allPage' : 'latestPage', {tag: tag});
function pageTitle(resource) {
return Template.render(resource.all ? 'allPage' : 'latestPage', {tag: resource.tag});
}
function otherUrl(tag, all) {
var path = [tag, all ? '' : 'all.html'];
function otherUrl(resource) {
var path = [resource.tag, resource.all ? '' : 'all.html'];
return '/' + path.filter(defined).join('/');
}
function articlesList(tag, all) {
function articlesList(resource) {
return function(articlePreviews) {
return [
Dom.make('h2', {innerText: pageTitle(tag, all)}),
Dom.make('ul', {}, articlesListLinks(tag, all)),
Dom.make('h2', {innerText: pageTitle(resource)}),
Dom.make('ul', {}, articlesListLinks(resource)),
Dom.make('div', {class: 'articles'}, articlePreviews.filter(defined))
];
};
}
function articlesListLinks(tag, all) {
function articlesListLinks(resource) {
var links = [
Dom.make('a', {
innerText: all ? blog.wording.latestLink : blog.wording.allLink,
href: otherUrl(tag, all),
innerText: resource.all ? blog.wording.latestLink : blog.wording.allLink,
href: otherUrl(resource),
class: 'other'
})
];
@ -120,7 +136,7 @@ function articlesListLinks(tag, all) {
innerText: blog.wording.rssLink,
href: 'rss.xml',
class: 'RSS',
title: Template.render('rssTitle', {tag: tag})
title: Template.render('rssTitle', {tag: resource.tag})
}));
}
return links.map(function(e) {return Dom.make('li', {}, [e]);});

View File

@ -1,4 +1,4 @@
import {articlesList, render} from DomRenderer;
import {articlesList, getResource, render} from DomRenderer;
import blog from Hablo.Config;
import * as Async from UnitJS.Async;
import * as Cache from UnitJS.Cache;
@ -60,23 +60,20 @@ function visit(url) {
}
function navigate(url) {
var path = decodeURI(url).split("/").slice(1);
if(blog.tags[path[0]] != undefined) {
show(getArticlesList(path[0], path[1] == "all.html"));
} else if(path[0] == blog.path.articlesPath) {
show(getResource('article', path[1].replace(/\.html$/, '')));
} else if(path[0] == blog.path.pagesPath) {
show(getResource('page', path[1].replace(/\.html$/, '')));
} else {
show(getArticlesList(null, path[0] == "all.html"));
var resource = getResource(url);
switch(resource.type) {
case 'list': show(getArticlesList(resource)); break;
case 'article':
case 'page': show(getCached(resource)); break;
default: console.log("No idea how to navigate to " + url);
}
}
function getResource(contentType, key) {
function getCached(resource) {
return Async.bind(
cache[contentType].get(key),
cache[resource.type].get(resource.key),
Async.map(
function(contents) {return [render(contentType, key, contents)];}
function(contents) {return [render(resource, contents)];}
)
);
}
@ -86,23 +83,23 @@ function preview(key) {
cache.article.get(key),
function(contents) {
return Async.wrap(
render('article', key, contents, blog.skin.previewLinesCount)
render({type: 'article', key: key}, contents, blog.skin.previewLinesCount)
);
}
);
}
function articleIds(tag, all) {
var ids = tag != undefined ? blog.tags[tag] : Object.keys(blog.articles);
function articleIds(resource) {
var ids = resource.tag != undefined ? blog.tags[resource.tag] : Object.keys(blog.articles);
var reverseDate = function (id) {return -blog.articles[id].metadata.date;};
ids.sort(Fun.compare(reverseDate));
return ids.slice(0, all ? undefined : blog.skin.previewArticlesCount);
return ids.slice(0, resource.all ? undefined : blog.skin.previewArticlesCount);
}
function getArticlesList(tag, all) {
function getArticlesList(resource) {
return Async.bind(
Async.parallel.apply(null, articleIds(tag, all).map(preview)),
Async.map(articlesList(tag, all))
Async.parallel.apply(null, articleIds(resource).map(preview)),
Async.map(articlesList(resource))
);
}

View File

@ -16,7 +16,7 @@ import Data.Text.Encoding (encodeUtf8)
import JSON (exportBlog)
import Paths_hablo (getDataDir)
import Pretty ((.$))
import SJW (compile, source, sourceCode)
import SJW (compile, source)
import System.Directory (createDirectoryIfMissing)
import System.Exit (die)
import System.FilePath ((</>))
@ -45,10 +45,11 @@ generateConfig destinationDir = do
generateMain :: FilePath -> IO ()
generateMain destinationDir = do
habloSources <- (</> "js") <$> getDataDir
result <- compile $ source [destinationDir, "unitJS", habloSources]
maybe (die "JS compilation failed\n") output =<< sourceCode result
compile (source [destinationDir, "unitJS", habloSources])
>>= either abort (output . fst)
where
output = writeFile (destinationDir </> "hablo.js") . fromStrict . encodeUtf8
abort = die . (<> "JS compilation failed\n")
generate :: ReaderT Blog IO ()
generate = do