Compare commits

...

3 Commits

Author SHA1 Message Date
Tissevert e7e44f8c63 Add a development package for guix and fix SJW incompatibility since
the 0.1.3.1 bugfix
2023-08-02 23:42:12 +02:00
Tissevert 7a9c1a65c2 Update dependencies versions 2023-07-31 19:32:31 +02:00
Tissevert 7e7d7e0804 Release 1.1.0.1 2021-01-20 13:48:21 +01:00
7 changed files with 151 additions and 61 deletions

View File

@ -1,5 +1,10 @@
# 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

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,7 +3,7 @@ cabal-version: >= 1.10
-- For further documentation, see http://haskell.org/cabal/users-guide/
name: hablo
version: 1.1.0.0
version: 1.1.0.1
synopsis: A minimalist static blog generator
description:
Hablo is a fediverse-oriented static blog generator for articles written
@ -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