Compare commits
33 commits
main
...
implement-
Author | SHA1 | Date | |
---|---|---|---|
bc8db13160 | |||
de4b32c022 | |||
44c4813d6f | |||
d4fb3eb778 | |||
49f819c54c | |||
fc054ee575 | |||
cb2bd54596 | |||
d338e7b5c9 | |||
ded02d4c71 | |||
3fe0dd3c2e | |||
a0dccc770d | |||
f9465d1aa5 | |||
804d3aa644 | |||
e74eadd6ba | |||
4c6ee2d9bc | |||
f7ec6d06c1 | |||
371b9a8098 | |||
e0161173ef | |||
5211379f00 | |||
8382dc11f2 | |||
47f5c70e21 | |||
937a6858e0 | |||
19b3694d06 | |||
ce3003178f | |||
08990e8440 | |||
6002f7c4d6 | |||
fc0ef57b53 | |||
1a2ece9dd9 | |||
1df95d5091 | |||
baa1d0ce09 | |||
fc8e26a983 | |||
46daaa2b7a | |||
b080c32d4c |
7 changed files with 64 additions and 160 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -1,16 +1,5 @@
|
||||||
# Revision history for hablo
|
# 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
|
## 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)
|
* 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)
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
(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
44
guix.scm
|
@ -1,44 +0,0 @@
|
||||||
(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+)))
|
|
18
hablo.cabal
18
hablo.cabal
|
@ -3,16 +3,16 @@ cabal-version: >= 1.10
|
||||||
-- For further documentation, see http://haskell.org/cabal/users-guide/
|
-- For further documentation, see http://haskell.org/cabal/users-guide/
|
||||||
|
|
||||||
name: hablo
|
name: hablo
|
||||||
version: 1.1.0.1
|
version: 1.0.3.0
|
||||||
synopsis: A minimalist static blog generator
|
synopsis: A minimalist static blog generator
|
||||||
description:
|
description:
|
||||||
Hablo is a fediverse-oriented static blog generator for articles written
|
Hablo is a fediverse-oriented static blog generator for articles written
|
||||||
in Markdown. It tries to generate as little HTML as needed and uses
|
in Markdown. It tries to generate as little HTML as needed and uses
|
||||||
Javascript to implement dynamic features in the browser.
|
Javascript to implement dynamic features in the browser.
|
||||||
|
|
||||||
Those features include the handling of comments and a cached navigation to
|
Those features include the handling of comments and a cached navigation
|
||||||
minimize the number of queries to the server. Hablo also generates RSS feeds
|
to minimize the queries to the server. Hablo also generate cards for all
|
||||||
and Open Graph cards for prettier shares on social networks.
|
pages, including articles for prettier shares on social-networks.
|
||||||
homepage: https://git.marvid.fr/Tissevert/hablo
|
homepage: https://git.marvid.fr/Tissevert/hablo
|
||||||
-- bug-reports:
|
-- bug-reports:
|
||||||
license: BSD3
|
license: BSD3
|
||||||
|
@ -50,15 +50,15 @@ library
|
||||||
, Pretty
|
, Pretty
|
||||||
, RSS
|
, RSS
|
||||||
-- other-extensions:
|
-- other-extensions:
|
||||||
build-depends: aeson >= 1.2.0 && < 2.1
|
build-depends: aeson >= 1.4.0 && < 1.6
|
||||||
, base >= 4.9.1 && < 4.17
|
, base >= 4.9.1 && < 4.15
|
||||||
, bytestring >= 0.10.8 && < 0.12
|
, bytestring >= 0.10.8 && < 0.12
|
||||||
, containers >= 0.5.11 && < 0.7
|
, containers >= 0.5.11 && < 0.7
|
||||||
, directory >= 1.3.1 && < 1.4
|
, directory >= 1.3.1 && < 1.4
|
||||||
, filepath >= 1.4.2 && < 1.5
|
, filepath >= 1.4.2 && < 1.5
|
||||||
, lucid >= 2.8.0 && < 2.12
|
, lucid >= 2.9.11 && < 2.10
|
||||||
, mtl >= 2.2.2 && < 2.3
|
, mtl >= 2.2.2 && < 2.3
|
||||||
, optparse-applicative >= 0.14.0 && < 0.18
|
, optparse-applicative >= 0.14.3 && < 0.17
|
||||||
, parsec >= 3.1.13 && < 3.2
|
, parsec >= 3.1.13 && < 3.2
|
||||||
, template >= 0.2.0 && < 0.3
|
, template >= 0.2.0 && < 0.3
|
||||||
, text >= 1.2.3 && < 1.3
|
, text >= 1.2.3 && < 1.3
|
||||||
|
@ -73,7 +73,7 @@ executable hablo
|
||||||
main-is: src/Main.hs
|
main-is: src/Main.hs
|
||||||
other-modules: Paths_hablo
|
other-modules: Paths_hablo
|
||||||
-- other-extensions:
|
-- other-extensions:
|
||||||
build-depends: base
|
build-depends: base >= 4.9.1 && < 4.15
|
||||||
, hablo
|
, hablo
|
||||||
, mtl >= 2.2.2 && < 2.3
|
, mtl >= 2.2.2 && < 2.3
|
||||||
ghc-options: -Wall
|
ghc-options: -Wall
|
||||||
|
|
|
@ -7,37 +7,15 @@ import {defined} from UnitJS.Fun;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
articlesList: articlesList,
|
articlesList: articlesList,
|
||||||
getResource: getResource,
|
|
||||||
render: render,
|
render: render,
|
||||||
replaceMarkdown: replaceMarkdown
|
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() {
|
function replaceMarkdown() {
|
||||||
var div = document.getElementById('contents');
|
var div = document.getElementById('contents');
|
||||||
if(div.children[0] && div.children[0].tagName.toLowerCase() == 'article') {
|
if(div.children[0] && div.children[0].tagName.toLowerCase() == 'article') {
|
||||||
var resourceType = getResource(window.location.pathname).type;
|
var contentType = window.location.pathname.slice(1).replace(/\/.*/, '');
|
||||||
convertContent(resourceType, div.children[0], true);
|
convertContent(contentType, div.children[0], true);
|
||||||
} else {
|
} else {
|
||||||
var articles = div.getElementsByClassName('articles')[0];
|
var articles = div.getElementsByClassName('articles')[0];
|
||||||
if(articles != undefined) {
|
if(articles != undefined) {
|
||||||
|
@ -50,15 +28,15 @@ function replaceMarkdown() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertContent(resourceType, article, comments) {
|
function convertContent(contentType, article, comments) {
|
||||||
var header = article.getElementsByTagName('header')[0];
|
var header = article.getElementsByTagName('header')[0];
|
||||||
if(resourceType == 'article') {
|
if(contentType == 'article') {
|
||||||
header.appendChild(Metadata.get(article.id));
|
header.appendChild(Metadata.get(article.id));
|
||||||
}
|
}
|
||||||
var text = article.getElementsByTagName('pre')[0];
|
var text = article.getElementsByTagName('pre')[0];
|
||||||
if(text != undefined) {
|
if(text != undefined) {
|
||||||
article.replaceChild(getDiv(text.innerText), text);
|
article.replaceChild(getDiv(text.innerText), text);
|
||||||
if(resourceType == 'article' && comments) {
|
if(contentType == 'article' && comments) {
|
||||||
Metadata.getComments(article.id)
|
Metadata.getComments(article.id)
|
||||||
.forEach(article.appendChild.bind(article));
|
.forEach(article.appendChild.bind(article));
|
||||||
}
|
}
|
||||||
|
@ -81,53 +59,59 @@ function getDiv(markdown) {
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
function commentsSection(resource, limit) {
|
function contentUrl(contentType, key, limit) {
|
||||||
if(resource.type != 'article' || limit != undefined) {
|
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) {
|
||||||
return [];
|
return [];
|
||||||
} else {
|
} else {
|
||||||
return Metadata.getComments(resource.key);
|
return Metadata.getComments(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(resource, markdown, limit) {
|
function render(contentType, key, markdown, limit) {
|
||||||
var url = resourceUrl(resource, limit);
|
var url = contentUrl(contentType, key, limit);
|
||||||
var content = blog[resource.type + 's'][resource.key];
|
var resource = blog[contentType + 's'][key];
|
||||||
var lines = markdown.split(/\n/).slice(content.bodyOffset);
|
var lines = markdown.split(/\n/).slice(resource.bodyOffset);
|
||||||
var div = getDiv(lines.slice(0, limit).join('\n'));
|
var div = getDiv(lines.slice(0, limit).join('\n'));
|
||||||
return Dom.make('article', {}, [
|
return Dom.make('article', {}, [
|
||||||
Dom.make('header', {}, [
|
Dom.make('header', {}, [
|
||||||
Dom.make('h1', {}, [
|
Dom.make('h1', {}, [
|
||||||
Dom.make('a', {href: url, innerText: content.title})
|
Dom.make('a', {href: url, innerText: resource.title})
|
||||||
])].concat(resource.type == 'article' ? Metadata.get(resource.key) : [])
|
])].concat(contentType == 'article' ? Metadata.get(key) : [])
|
||||||
),
|
),
|
||||||
div
|
div
|
||||||
].concat(commentsSection(resource, limit)));
|
].concat(commentsSection(contentType, key, limit)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function pageTitle(resource) {
|
function pageTitle(tag, all) {
|
||||||
return Template.render(resource.all ? 'allPage' : 'latestPage', {tag: resource.tag});
|
return Template.render(all ? 'allPage' : 'latestPage', {tag: tag});
|
||||||
}
|
}
|
||||||
|
|
||||||
function otherUrl(resource) {
|
function otherUrl(tag, all) {
|
||||||
var path = [resource.tag, resource.all ? '' : 'all.html'];
|
var path = [tag, all ? '' : 'all.html'];
|
||||||
return '/' + path.filter(defined).join('/');
|
return '/' + path.filter(defined).join('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
function articlesList(resource) {
|
function articlesList(tag, all) {
|
||||||
return function(articlePreviews) {
|
return function(articlePreviews) {
|
||||||
return [
|
return [
|
||||||
Dom.make('h2', {innerText: pageTitle(resource)}),
|
Dom.make('h2', {innerText: pageTitle(tag, all)}),
|
||||||
Dom.make('ul', {}, articlesListLinks(resource)),
|
Dom.make('ul', {}, articlesListLinks(tag, all)),
|
||||||
Dom.make('div', {class: 'articles'}, articlePreviews.filter(defined))
|
Dom.make('div', {class: 'articles'}, articlePreviews.filter(defined))
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function articlesListLinks(resource) {
|
function articlesListLinks(tag, all) {
|
||||||
var links = [
|
var links = [
|
||||||
Dom.make('a', {
|
Dom.make('a', {
|
||||||
innerText: resource.all ? blog.wording.latestLink : blog.wording.allLink,
|
innerText: all ? blog.wording.latestLink : blog.wording.allLink,
|
||||||
href: otherUrl(resource),
|
href: otherUrl(tag, all),
|
||||||
class: 'other'
|
class: 'other'
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
@ -136,7 +120,7 @@ function articlesListLinks(resource) {
|
||||||
innerText: blog.wording.rssLink,
|
innerText: blog.wording.rssLink,
|
||||||
href: 'rss.xml',
|
href: 'rss.xml',
|
||||||
class: 'RSS',
|
class: 'RSS',
|
||||||
title: Template.render('rssTitle', {tag: resource.tag})
|
title: Template.render('rssTitle', {tag: tag})
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return links.map(function(e) {return Dom.make('li', {}, [e]);});
|
return links.map(function(e) {return Dom.make('li', {}, [e]);});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {articlesList, getResource, render} from DomRenderer;
|
import {articlesList, render} from DomRenderer;
|
||||||
import blog from Hablo.Config;
|
import blog from Hablo.Config;
|
||||||
import * as Async from UnitJS.Async;
|
import * as Async from UnitJS.Async;
|
||||||
import * as Cache from UnitJS.Cache;
|
import * as Cache from UnitJS.Cache;
|
||||||
|
@ -60,20 +60,23 @@ function visit(url) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigate(url) {
|
function navigate(url) {
|
||||||
var resource = getResource(url);
|
var path = decodeURI(url).split("/").slice(1);
|
||||||
switch(resource.type) {
|
if(blog.tags[path[0]] != undefined) {
|
||||||
case 'list': show(getArticlesList(resource)); break;
|
show(getArticlesList(path[0], path[1] == "all.html"));
|
||||||
case 'article':
|
} else if(path[0] == blog.path.articlesPath) {
|
||||||
case 'page': show(getCached(resource)); break;
|
show(getResource('article', path[1].replace(/\.html$/, '')));
|
||||||
default: console.log("No idea how to navigate to " + url);
|
} else if(path[0] == blog.path.pagesPath) {
|
||||||
|
show(getResource('page', path[1].replace(/\.html$/, '')));
|
||||||
|
} else {
|
||||||
|
show(getArticlesList(null, path[0] == "all.html"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCached(resource) {
|
function getResource(contentType, key) {
|
||||||
return Async.bind(
|
return Async.bind(
|
||||||
cache[resource.type].get(resource.key),
|
cache[contentType].get(key),
|
||||||
Async.map(
|
Async.map(
|
||||||
function(contents) {return [render(resource, contents)];}
|
function(contents) {return [render(contentType, key, contents)];}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -83,23 +86,23 @@ function preview(key) {
|
||||||
cache.article.get(key),
|
cache.article.get(key),
|
||||||
function(contents) {
|
function(contents) {
|
||||||
return Async.wrap(
|
return Async.wrap(
|
||||||
render({type: 'article', key: key}, contents, blog.skin.previewLinesCount)
|
render('article', key, contents, blog.skin.previewLinesCount)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function articleIds(resource) {
|
function articleIds(tag, all) {
|
||||||
var ids = resource.tag != undefined ? blog.tags[resource.tag] : Object.keys(blog.articles);
|
var ids = tag != undefined ? blog.tags[tag] : Object.keys(blog.articles);
|
||||||
var reverseDate = function (id) {return -blog.articles[id].metadata.date;};
|
var reverseDate = function (id) {return -blog.articles[id].metadata.date;};
|
||||||
ids.sort(Fun.compare(reverseDate));
|
ids.sort(Fun.compare(reverseDate));
|
||||||
return ids.slice(0, resource.all ? undefined : blog.skin.previewArticlesCount);
|
return ids.slice(0, all ? undefined : blog.skin.previewArticlesCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArticlesList(resource) {
|
function getArticlesList(tag, all) {
|
||||||
return Async.bind(
|
return Async.bind(
|
||||||
Async.parallel.apply(null, articleIds(resource).map(preview)),
|
Async.parallel.apply(null, articleIds(tag, all).map(preview)),
|
||||||
Async.map(articlesList(resource))
|
Async.map(articlesList(tag, all))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ import Data.Text.Encoding (encodeUtf8)
|
||||||
import JSON (exportBlog)
|
import JSON (exportBlog)
|
||||||
import Paths_hablo (getDataDir)
|
import Paths_hablo (getDataDir)
|
||||||
import Pretty ((.$))
|
import Pretty ((.$))
|
||||||
import SJW (compile, source)
|
import SJW (compile, source, sourceCode)
|
||||||
import System.Directory (createDirectoryIfMissing)
|
import System.Directory (createDirectoryIfMissing)
|
||||||
import System.Exit (die)
|
import System.Exit (die)
|
||||||
import System.FilePath ((</>))
|
import System.FilePath ((</>))
|
||||||
|
@ -45,11 +45,10 @@ generateConfig destinationDir = do
|
||||||
generateMain :: FilePath -> IO ()
|
generateMain :: FilePath -> IO ()
|
||||||
generateMain destinationDir = do
|
generateMain destinationDir = do
|
||||||
habloSources <- (</> "js") <$> getDataDir
|
habloSources <- (</> "js") <$> getDataDir
|
||||||
compile (source [destinationDir, "unitJS", habloSources])
|
result <- compile $ source [destinationDir, "unitJS", habloSources]
|
||||||
>>= either abort (output . fst)
|
maybe (die "JS compilation failed\n") output =<< sourceCode result
|
||||||
where
|
where
|
||||||
output = writeFile (destinationDir </> "hablo.js") . fromStrict . encodeUtf8
|
output = writeFile (destinationDir </> "hablo.js") . fromStrict . encodeUtf8
|
||||||
abort = die . (<> "JS compilation failed\n")
|
|
||||||
|
|
||||||
generate :: ReaderT Blog IO ()
|
generate :: ReaderT Blog IO ()
|
||||||
generate = do
|
generate = do
|
||||||
|
|
Loading…
Reference in a new issue