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 |
62 changed files with 1145 additions and 326 deletions
22
README.md
22
README.md
|
@ -20,6 +20,16 @@ cabal new-install hablo
|
||||||
|
|
||||||
Alternatively, if you prefer to do things yourself you can do a
|
Alternatively, if you prefer to do things yourself you can do a
|
||||||
|
|
||||||
|
#### Simple install with nix
|
||||||
|
|
||||||
|
Want to give hablo a quick try using nix ?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix-env -f 'https://git.marvid.fr/Tissevert/mynixpkgs/archive/main.tar.gz' -i hablo
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit my [Nix packages](https://git.marvid.fr/Tissevert/mynixpkgs) for a more declarative setup.
|
||||||
|
|
||||||
#### Manual install from this repository
|
#### Manual install from this repository
|
||||||
|
|
||||||
Get a copy of this repository
|
Get a copy of this repository
|
||||||
|
@ -40,6 +50,18 @@ Install the result
|
||||||
cabal new-install hablo
|
cabal new-install hablo
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
Hablo requires [UnitJS](https://git.marvid.fr/Tissevert/UnitJS) which is handled by [SJW](https://git.marvid.fr/Tissevert/SJW). Make sure you have installed it regularly with `SJW`. If it isn't yet, the following commands should help you:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /tmp
|
||||||
|
git clone https://git.marvid.fr/Tissevert/UnitJS.git
|
||||||
|
cd UnitJS
|
||||||
|
mkdir -p ~/.sjw
|
||||||
|
cp -r src/ ~/.sjw/unitJS
|
||||||
|
```
|
||||||
|
|
||||||
### Using hablo (tutorials)
|
### Using hablo (tutorials)
|
||||||
|
|
||||||
Wanna give it a try ? Start by [generating your blog](https://git.marvid.fr/Tissevert/hablo/wiki/Generating%20your%20blog)
|
Wanna give it a try ? Start by [generating your blog](https://git.marvid.fr/Tissevert/hablo/wiki/Generating%20your%20blog)
|
||||||
|
|
|
@ -32,6 +32,8 @@ hablo --articles turtles /path/to/your/blog
|
||||||
|
|
||||||
See ? It was still `turtles` and not ~~`/path/to/your/blog/turtles`~~.
|
See ? It was still `turtles` and not ~~`/path/to/your/blog/turtles`~~.
|
||||||
|
|
||||||
|
Also note that articles are partly optional : you can use hablo to generate a website with a fix content and no articles. In that case, just make sure no directory named `articles/` exists at the root of your website (see [pages](#pages-path)) and keep in mind that it should have static pages (hablo, just like other famous entities should not be invoked in vain and will exit in error suspecting something went wrong when invoked on an empty website with no articles and no pages, which to it means nothing to do).
|
||||||
|
|
||||||
## Banner
|
## Banner
|
||||||
|
|
||||||
`-b, --banner`
|
`-b, --banner`
|
||||||
|
@ -116,11 +118,25 @@ hablo --name "Turtles/Paradize"
|
||||||
|
|
||||||
Enables Open Graph cards in pages to display a pretty preview of them instead of the raw URL in links posted to social media. Note that this feature requires setting your site URL with [`--site-url`](#site-url).
|
Enables Open Graph cards in pages to display a pretty preview of them instead of the raw URL in links posted to social media. Note that this feature requires setting your site URL with [`--site-url`](#site-url).
|
||||||
|
|
||||||
## Pages
|
## Pages path
|
||||||
|
|
||||||
`-p, --pages`
|
`-p, --pages`
|
||||||
|
|
||||||
This option doesn't work yet but hablo will support static pages in addition to articles in a future release. Like [articles](#article-path), they will be expected to be located in a sub-directory called `pages/` but this option will allow you to use an arbitrary path within your blog's structure.
|
In addition to «dynamic» lists of articles that grow over time, hablo supports «static» pages to allow you to publish relatively constant information related to your blog. Pages are expected to be located in a sub-directory called `pages/` but this option will allow you to use an arbitrary path within your blog's structure.
|
||||||
|
|
||||||
|
So if for instance your blog is for a community of authors and a presentation of each of them is all you want to publish as «static» content, you could have this directory called «authors» and run `hablo` like this :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hablo --pages authors
|
||||||
|
```
|
||||||
|
|
||||||
|
This option is very similar to the one for [articles](#articles-path). Like the articles path, the pages path is of course relative to the blog's root. Pages are also partly optional : you don't have to have static pages in your blog in which case you should just make sure no directory named `pages/` exists at the root of your website and you have articles (because like we said above hablo is highly suspicious of being invoked to perform no work and will suspect this is a mistake and report it as an error).
|
||||||
|
|
||||||
|
Final tip : if you're using hablo to edit a static website with no articles, then you probably don't want to put your pages in a sub-directory but have them at the root of your website instead. This is possible, just remember that the current directory is called `.` in UNIX and run :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hablo -p .
|
||||||
|
```
|
||||||
|
|
||||||
## Number of articles previewed
|
## Number of articles previewed
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Deployment
|
# Deployment
|
||||||
|
|
||||||
Since hablo generates static blogs, deployment is a fairly easy step. The only detail to pay attention to is the handling of dependencies.
|
Since hablo generates static blogs, deployment is a fairly easy step. The only detail to pay attention to is the handling of JS dependencies.
|
||||||
|
|
||||||
We show here a simple local deployment of your blog assuming you use NGinx but this is fairly easy to transpose to your favourite web server. First let's create an NGinx configuration file for your blog. Let's put the following basic configuration
|
We show here a simple local deployment of your blog assuming you use NGinx but this is fairly easy to transpose to your favourite web server. First let's create an NGinx configuration file for your blog. Let's put the following basic configuration
|
||||||
|
|
||||||
|
@ -29,30 +29,11 @@ sudo nginx -s reload
|
||||||
|
|
||||||
Now let's install the dependencies.
|
Now let's install the dependencies.
|
||||||
|
|
||||||
## UnitJS
|
|
||||||
|
|
||||||
Hablo requires [UnitJS](https://git.marvid.fr/Tissevert/UnitJS). Go to some temporary work directory, clone it and generate the packed JS module.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /tmp
|
|
||||||
git clone https://git.marvid.fr/Tissevert/UnitJS.git
|
|
||||||
cd UnitJS
|
|
||||||
make
|
|
||||||
```
|
|
||||||
|
|
||||||
It's in `dist/unit.js`. Let's go back to your blog's directory and copy it.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd "/path/to/My perfect life is better than yours"
|
|
||||||
mkdir -p js
|
|
||||||
cp /tmp/UnitJS/dist/unit.js js
|
|
||||||
```
|
|
||||||
|
|
||||||
## Remarkable
|
## Remarkable
|
||||||
|
|
||||||
The markdown is converted to HTML in the client browser with the JS library [remarkable](https://github.com/jonschlinkert/remarkable).
|
The markdown is converted to HTML in the client browser with the JS library [remarkable](https://github.com/jonschlinkert/remarkable).
|
||||||
|
|
||||||
We can simply download it in your `js` directory.
|
We can simply download it in the `js` subdirectory of your blog hablo created when you first invoked it.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wget 'https://cdnjs.cloudflare.com/ajax/libs/remarkable/1.7.1/remarkable.min.js' -O js/remarkable.min.js
|
wget 'https://cdnjs.cloudflare.com/ajax/libs/remarkable/1.7.1/remarkable.min.js' -O js/remarkable.min.js
|
||||||
|
|
|
@ -25,7 +25,7 @@ EOF
|
||||||
|
|
||||||
Ok, ok, not everyone uses heredocs to write their articles. Personally I don't. You're writing a blog so you probably already have a favourite text editor; use it. The only thing I care about is, at this point, that you've created the file `Olive\ ridley\ sea\ turtle.md` in the `articles` directory with some markdown content in it.
|
Ok, ok, not everyone uses heredocs to write their articles. Personally I don't. You're writing a blog so you probably already have a favourite text editor; use it. The only thing I care about is, at this point, that you've created the file `Olive\ ridley\ sea\ turtle.md` in the `articles` directory with some markdown content in it.
|
||||||
|
|
||||||
Ready ? Good news, we're almost done. The only thing left is to tag your first article. With hablo articles don't have to be put in a single category but they can be tagged this and that to indicate that they are somehow linked to one topic or another (they don't have to, you can perfectly leave an article untagged but the tags directory itself must exist). Tags live in a subdirectory of `articles`.
|
Ready ? Good news, we're almost done. The only thing left is to tag your first article. With hablo articles don't have to be put in a single category but they can be tagged this and that to indicate that they are somehow linked to one topic or another (they don't have to, you can perfectly leave an article untagged). Tags live in a subdirectory of `articles`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p articles/tags/Sea\ turtles
|
mkdir -p articles/tags/Sea\ turtles
|
||||||
|
|
61
hablo.cabal
61
hablo.cabal
|
@ -27,9 +27,8 @@ data-dir: share
|
||||||
data-files: js/*.js
|
data-files: js/*.js
|
||||||
defaultWording.conf
|
defaultWording.conf
|
||||||
|
|
||||||
executable hablo
|
library
|
||||||
main-is: Main.hs
|
exposed-modules: Arguments
|
||||||
other-modules: Arguments
|
|
||||||
, Article
|
, Article
|
||||||
, ArticlesList
|
, ArticlesList
|
||||||
, Blog
|
, Blog
|
||||||
|
@ -45,25 +44,69 @@ executable hablo
|
||||||
, HTML
|
, HTML
|
||||||
, JS
|
, JS
|
||||||
, JSON
|
, JSON
|
||||||
|
, Markdown
|
||||||
|
, Page
|
||||||
, Paths_hablo
|
, Paths_hablo
|
||||||
, Pretty
|
, Pretty
|
||||||
, RSS
|
, RSS
|
||||||
-- other-extensions:
|
-- other-extensions:
|
||||||
build-depends: aeson >= 1.4.0 && < 1.5
|
build-depends: aeson >= 1.4.0 && < 1.6
|
||||||
, base >= 4.9.1 && < 4.14
|
, base >= 4.9.1 && < 4.15
|
||||||
, bytestring >= 0.10.8 && < 0.11
|
, 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.9.11 && < 2.10
|
, lucid >= 2.9.11 && < 2.10
|
||||||
, mtl >= 2.2.2 && < 2.3
|
, mtl >= 2.2.2 && < 2.3
|
||||||
, optparse-applicative >= 0.14.3 && < 0.16
|
, 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
|
||||||
, time >= 1.8.0 && < 1.10
|
, time >= 1.8.0 && < 1.12
|
||||||
, SJW >= 0.1.2 && < 0.2
|
, SJW >= 0.1.2 && < 0.2
|
||||||
, unix >= 2.7.2 && < 2.8
|
, unix >= 2.7.2 && < 2.8
|
||||||
ghc-options: -Wall -dynamic
|
ghc-options: -Wall
|
||||||
hs-source-dirs: src
|
hs-source-dirs: src
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
|
||||||
|
executable hablo
|
||||||
|
main-is: src/Main.hs
|
||||||
|
other-modules: Paths_hablo
|
||||||
|
-- other-extensions:
|
||||||
|
build-depends: base >= 4.9.1 && < 4.15
|
||||||
|
, hablo
|
||||||
|
, mtl >= 2.2.2 && < 2.3
|
||||||
|
ghc-options: -Wall
|
||||||
|
default-language: Haskell2010
|
||||||
|
|
||||||
|
test-suite tests
|
||||||
|
type: detailed-0.9
|
||||||
|
test-module: Tests
|
||||||
|
other-modules: Mock.Arguments
|
||||||
|
, Mock.Article
|
||||||
|
, Mock.ArticlesList
|
||||||
|
, Mock.Blog
|
||||||
|
, Mock.Blog.Path
|
||||||
|
, Mock.Blog.Skin
|
||||||
|
, Mock.Blog.Template
|
||||||
|
, Mock.Blog.URL
|
||||||
|
, Mock.Blog.Wording
|
||||||
|
, Mock.Collection
|
||||||
|
, Mock.Markdown
|
||||||
|
, Structure
|
||||||
|
, Utils
|
||||||
|
, XML.Card
|
||||||
|
, XML.Card.Component
|
||||||
|
, XML.Card.Output
|
||||||
|
build-depends: base
|
||||||
|
, Cabal
|
||||||
|
, containers
|
||||||
|
, directory
|
||||||
|
, filepath
|
||||||
|
, hablo
|
||||||
|
, lucid
|
||||||
|
, mtl
|
||||||
|
, text
|
||||||
|
hs-source-dirs: test
|
||||||
|
ghc-options: -Wall
|
||||||
|
default-language: Haskell2010
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
allLink = See all
|
allLink = See all
|
||||||
allPage = All articles{? tagged ${tag}?}
|
allPage = All articles{? tagged ${tag}?}
|
||||||
|
articleDescription = A new article on ${name}
|
||||||
commentsLink = Comment on the fediverse
|
commentsLink = Comment on the fediverse
|
||||||
commentsSection = Comments
|
commentsSection = Comments
|
||||||
dateFormat = en-US
|
dateFormat = en-US
|
||||||
latestLink = See only latest
|
latestLink = See only latest
|
||||||
latestPage = Latest articles{? tagged ${tag}?}
|
latestPage = Latest articles{? tagged ${tag}?}
|
||||||
metadata = {?by ${author} ?}on ${date}{? tagged ${tags}?}
|
metadata = {?by ${author} ?}on ${date}{? tagged ${tags}?}
|
||||||
|
pageDescription = Read on ${name}
|
||||||
|
pagesList = Pages
|
||||||
rssLink = Subscribe
|
rssLink = Subscribe
|
||||||
rssTitle = Follow all articles{? tagged ${tag}?}
|
rssTitle = Follow all articles{? tagged ${tag}?}
|
||||||
tagsList = Tags
|
tagsList = Tags
|
||||||
|
|
|
@ -6,20 +6,21 @@ import * as Dom from UnitJS.Dom;
|
||||||
import {defined} from UnitJS.Fun;
|
import {defined} from UnitJS.Fun;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
article: article,
|
|
||||||
articlesList: articlesList,
|
articlesList: articlesList,
|
||||||
|
render: render,
|
||||||
replaceMarkdown: replaceMarkdown
|
replaceMarkdown: replaceMarkdown
|
||||||
};
|
};
|
||||||
|
|
||||||
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') {
|
||||||
convertArticle(div.children[0], true);
|
var contentType = window.location.pathname.slice(1).replace(/\/.*/, '');
|
||||||
|
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) {
|
||||||
for(var i = 0; i < articles.children.length; i++) {
|
for(var i = 0; i < articles.children.length; i++) {
|
||||||
convertArticle(articles.children[i]);
|
convertContent('article', articles.children[i]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('No articles found for this page');
|
console.log('No articles found for this page');
|
||||||
|
@ -27,13 +28,15 @@ function replaceMarkdown() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertArticle(article, comments) {
|
function convertContent(contentType, article, comments) {
|
||||||
var header = article.getElementsByTagName('header')[0];
|
var header = article.getElementsByTagName('header')[0];
|
||||||
|
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(comments) {
|
if(contentType == 'article' && comments) {
|
||||||
Metadata.getComments(article.id)
|
Metadata.getComments(article.id)
|
||||||
.forEach(article.appendChild.bind(article));
|
.forEach(article.appendChild.bind(article));
|
||||||
}
|
}
|
||||||
|
@ -56,19 +59,33 @@ function getDiv(markdown) {
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
function article(key, markdown, limit) {
|
function contentUrl(contentType, key, limit) {
|
||||||
var url = ["", blog.path.articlesPath, key + (limit != undefined ? '.html' : '.md')].join('/');
|
var directory = blog.path[contentType + 'sPath'];
|
||||||
var lines = markdown.split(/\n/).slice(blog.articles[key].bodyOffset);
|
var extension = limit != undefined ? '.html' : '.md';
|
||||||
|
return ["", directory, key + extension].join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function commentsSection(contentType, key, limit) {
|
||||||
|
if(contentType != 'article' || limit != undefined) {
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
return Metadata.getComments(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);
|
||||||
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('a', {href: url}, [
|
Dom.make('h1', {}, [
|
||||||
Dom.make('h1', {innerText: blog.articles[key].title})
|
Dom.make('a', {href: url, innerText: resource.title})
|
||||||
]),
|
])].concat(contentType == 'article' ? Metadata.get(key) : [])
|
||||||
Metadata.get(key)
|
),
|
||||||
]),
|
|
||||||
div
|
div
|
||||||
].concat(limit != undefined ? [] : Metadata.getComments(key)));
|
].concat(commentsSection(contentType, key, limit)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function pageTitle(tag, all) {
|
function pageTitle(tag, all) {
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import {article, articlesList} 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;
|
||||||
import * as Dom from UnitJS.Dom;
|
import * as Dom from UnitJS.Dom;
|
||||||
import * as Fun from UnitJS.Fun;
|
import * as Fun from UnitJS.Fun;
|
||||||
|
|
||||||
var articles = Cache.make(function(key) {
|
var cache = {};
|
||||||
var url = ["", blog.path.articlesPath, key + '.md'].join('/');
|
['article', 'page'].forEach(function(contentType) {
|
||||||
|
cache[contentType] = Cache.make(function(key) {
|
||||||
|
var url = ["", blog.path[contentType + 'sPath'], key + '.md'].join('/');
|
||||||
return Async.bind(
|
return Async.bind(
|
||||||
Async.http({method: 'GET', url: url}),
|
Async.http({method: 'GET', url: url}),
|
||||||
function(queryResult) {
|
function(queryResult) {
|
||||||
|
@ -14,12 +16,14 @@ var articles = Cache.make(function(key) {
|
||||||
return Async.wrap(queryResult.responseText);
|
return Async.wrap(queryResult.responseText);
|
||||||
} else {
|
} else {
|
||||||
return Async.fail(
|
return Async.fail(
|
||||||
"Could not load article " + url + " (" + queryResult.status + " " + queryResult.statusText + ")"
|
"Could not load " + contentType + " " + url + " (" + queryResult.status + " " + queryResult.statusText + ")"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener('popstate', function(e) {
|
window.addEventListener('popstate', function(e) {
|
||||||
if(e.state != undefined) {
|
if(e.state != undefined) {
|
||||||
navigate(e.state.url);
|
navigate(e.state.url);
|
||||||
|
@ -60,27 +64,29 @@ function navigate(url) {
|
||||||
if(blog.tags[path[0]] != undefined) {
|
if(blog.tags[path[0]] != undefined) {
|
||||||
show(getArticlesList(path[0], path[1] == "all.html"));
|
show(getArticlesList(path[0], path[1] == "all.html"));
|
||||||
} else if(path[0] == blog.path.articlesPath) {
|
} else if(path[0] == blog.path.articlesPath) {
|
||||||
show(getArticle(path[1].replace(/\.html$/, '')));
|
show(getResource('article', path[1].replace(/\.html$/, '')));
|
||||||
|
} else if(path[0] == blog.path.pagesPath) {
|
||||||
|
show(getResource('page', path[1].replace(/\.html$/, '')));
|
||||||
} else {
|
} else {
|
||||||
show(getArticlesList(null, path[0] == "all.html"));
|
show(getArticlesList(null, path[0] == "all.html"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArticle(key) {
|
function getResource(contentType, key) {
|
||||||
return Async.bind(
|
return Async.bind(
|
||||||
articles.get(key),
|
cache[contentType].get(key),
|
||||||
Async.map(
|
Async.map(
|
||||||
function(contents) {return [article(key, contents)];}
|
function(contents) {return [render(contentType, key, contents)];}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function preview(key) {
|
function preview(key) {
|
||||||
return Async.bind(
|
return Async.bind(
|
||||||
articles.get(key),
|
cache.article.get(key),
|
||||||
function(contents) {
|
function(contents) {
|
||||||
return Async.wrap(
|
return Async.wrap(
|
||||||
article(key, contents, blog.skin.previewLinesCount)
|
render('article', key, contents, blog.skin.previewLinesCount)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
|
{-# LANGUAGE CPP #-}
|
||||||
module Arguments (
|
module Arguments (
|
||||||
Arguments(..)
|
Arguments(..)
|
||||||
, get
|
, get
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
#if !MIN_VERSION_base(4,11,0)
|
||||||
import Data.Monoid ((<>))
|
import Data.Monoid ((<>))
|
||||||
|
#endif
|
||||||
import Data.Version (showVersion)
|
import Data.Version (showVersion)
|
||||||
import Control.Applicative ((<|>), (<**>), optional)
|
import Control.Applicative ((<|>), (<**>), optional)
|
||||||
import Options.Applicative (
|
import Options.Applicative (
|
||||||
|
@ -16,7 +19,7 @@ import System.FilePath (dropTrailingPathSeparator, isValid)
|
||||||
|
|
||||||
data Arguments = BlogConfig {
|
data Arguments = BlogConfig {
|
||||||
sourceDir :: FilePath
|
sourceDir :: FilePath
|
||||||
, articlesPath :: FilePath
|
, articlesPath :: Maybe FilePath
|
||||||
, bannerPath :: Maybe FilePath
|
, bannerPath :: Maybe FilePath
|
||||||
, cardImage :: Maybe FilePath
|
, cardImage :: Maybe FilePath
|
||||||
, commentsURL :: Maybe String
|
, commentsURL :: Maybe String
|
||||||
|
@ -47,13 +50,8 @@ option readM aShort aLong aMetavar aHelpMessage =
|
||||||
blogConfig :: Parser Arguments
|
blogConfig :: Parser Arguments
|
||||||
blogConfig = BlogConfig
|
blogConfig = BlogConfig
|
||||||
<$> argument filePath (value "." <> metavar "INPUT_DIR")
|
<$> argument filePath (value "." <> metavar "INPUT_DIR")
|
||||||
<*> Optparse.option filePath (
|
<*> option filePath 'a' "articles" "DIRECTORY"
|
||||||
metavar "DIRECTORY"
|
"relative path to the directory containing the articles within INPUT_DIR"
|
||||||
<> value "articles"
|
|
||||||
<> short 'a'
|
|
||||||
<> long "articles"
|
|
||||||
<> help "relative path to the directory containing the articles within INPUT_DIR"
|
|
||||||
)
|
|
||||||
<*> option filePath 'b' "banner" "FILE" "path to the file to use for the blog's banner"
|
<*> option filePath 'b' "banner" "FILE" "path to the file to use for the blog's banner"
|
||||||
<*> option filePath 'c' "card-image" "FILE" "relative path to the image to use for the blog's card"
|
<*> option filePath 'c' "card-image" "FILE" "relative path to the image to use for the blog's card"
|
||||||
<*> option filePath 'C' "comments-url" "URL" "URL of the instance where comments are stored"
|
<*> option filePath 'C' "comments-url" "URL" "URL of the instance where comments are stored"
|
||||||
|
@ -61,8 +59,8 @@ blogConfig = BlogConfig
|
||||||
<*> option filePath 'H' "head" "FILE" "path to the file to add in the blog's head"
|
<*> option filePath 'H' "head" "FILE" "path to the file to add in the blog's head"
|
||||||
<*> option str 'n' "name" "BLOG_NAME" "name of the blog"
|
<*> option str 'n' "name" "BLOG_NAME" "name of the blog"
|
||||||
<*> switch (short 'O' <> long "open-graph-cards" <> help "enable Open Graph cards")
|
<*> switch (short 'O' <> long "open-graph-cards" <> help "enable Open Graph cards")
|
||||||
<*> option filePath 'p' "pages"
|
<*> option filePath 'p' "pages" "DIRECTORY"
|
||||||
"DIRECTORY" "relative path to the directory containing the pages within INPUT_DIR"
|
"relative path to the directory containing the pages within INPUT_DIR"
|
||||||
<*> Optparse.option auto (
|
<*> Optparse.option auto (
|
||||||
metavar "INTEGER"
|
metavar "INTEGER"
|
||||||
<> value 3
|
<> value 3
|
||||||
|
|
|
@ -1,75 +1,23 @@
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
|
||||||
module Article (
|
module Article (
|
||||||
Article(..)
|
Article(..)
|
||||||
, at
|
, at
|
||||||
, getKey
|
|
||||||
, preview
|
, preview
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Applicative ((<|>))
|
import Control.Applicative ((<|>))
|
||||||
import Data.Map (Map)
|
import qualified Data.Map as Map (alter)
|
||||||
import qualified Data.Map as Map (fromList, alter)
|
|
||||||
import Data.Time (defaultTimeLocale, getCurrentTimeZone, parseTimeM, timeZoneOffsetString)
|
import Data.Time (defaultTimeLocale, getCurrentTimeZone, parseTimeM, timeZoneOffsetString)
|
||||||
import Data.Time.Clock.POSIX (POSIXTime, utcTimeToPOSIXSeconds)
|
import Data.Time.Clock.POSIX (POSIXTime, utcTimeToPOSIXSeconds)
|
||||||
import Foreign.C.Types (CTime)
|
import Foreign.C.Types (CTime)
|
||||||
import System.FilePath (dropExtension, takeFileName)
|
import Markdown (Markdown(..), MarkdownContent(..), Metadata)
|
||||||
|
import qualified Markdown (at)
|
||||||
import System.Posix.Files (getFileStatus, modificationTime)
|
import System.Posix.Files (getFileStatus, modificationTime)
|
||||||
import Text.ParserCombinators.Parsec (
|
import Text.ParserCombinators.Parsec (ParseError)
|
||||||
ParseError
|
|
||||||
, Parser
|
|
||||||
, (<?>)
|
|
||||||
, anyChar, char, count, endBy, eof, getPosition, many, many1, noneOf
|
|
||||||
, oneOf, option, parse, skipMany, sourceLine, string, try
|
|
||||||
)
|
|
||||||
|
|
||||||
type Metadata = Map String String
|
newtype Article = Article Markdown
|
||||||
|
instance MarkdownContent Article where
|
||||||
data Article = Article {
|
getMarkdown (Article markdown) = markdown
|
||||||
key :: String
|
|
||||||
, title :: String
|
|
||||||
, metadata :: Metadata
|
|
||||||
, bodyOffset :: Int
|
|
||||||
, body :: [String]
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoArticle = (String, Metadata, Int, [String])
|
|
||||||
|
|
||||||
articleP :: Parser ProtoArticle
|
|
||||||
articleP =
|
|
||||||
skipMany eol *> headerP <* skipMany eol <*> lineOffset <*> bodyP
|
|
||||||
where
|
|
||||||
headerP =
|
|
||||||
try ((,,,) <$> titleP <* many eol <*> metadataP)
|
|
||||||
<|> flip (,,,) <$> metadataP <* many eol<*> titleP
|
|
||||||
lineOffset = pred . sourceLine <$> getPosition
|
|
||||||
bodyP = lines <$> many anyChar <* eof
|
|
||||||
|
|
||||||
metadataP :: Parser Metadata
|
|
||||||
metadataP = Map.fromList <$> option [] (
|
|
||||||
metaSectionSeparator *> many eol *>
|
|
||||||
(try keyVal) `endBy` (many1 eol)
|
|
||||||
<* metaSectionSeparator
|
|
||||||
) <?> "metadata section"
|
|
||||||
where
|
|
||||||
metaSectionSeparator = count 3 (oneOf "~-") *> eol
|
|
||||||
spaces = skipMany $ char ' '
|
|
||||||
keyVal = (,) <$> (no ": \r\n" <* spaces <* char ':' <* spaces) <*> no "\r\n"
|
|
||||||
|
|
||||||
titleP :: Parser String
|
|
||||||
titleP = try (singleLine <|> underlined)
|
|
||||||
where
|
|
||||||
singleLine = char '#' *> char ' ' *> no "\r\n" <* eol
|
|
||||||
underlined =
|
|
||||||
no "\r\n" <* eol
|
|
||||||
>>= \titleLine -> count (length titleLine) (oneOf "#=") *> eol *> return titleLine
|
|
||||||
<?> "'#' or '=' to underline the title"
|
|
||||||
|
|
||||||
eol :: Parser String
|
|
||||||
eol = try (string "\r\n") <|> string "\r" <|> string "\n" <?> "newline"
|
|
||||||
|
|
||||||
no :: String -> Parser String
|
|
||||||
no = many1 . noneOf
|
|
||||||
|
|
||||||
setDate :: String -> CTime -> Metadata -> Metadata
|
setDate :: String -> CTime -> Metadata -> Metadata
|
||||||
setDate tzOffset defaultDate = Map.alter timeStamp "date"
|
setDate tzOffset defaultDate = Map.alter timeStamp "date"
|
||||||
|
@ -82,27 +30,16 @@ setDate tzOffset defaultDate = Map.alter timeStamp "date"
|
||||||
let parsedTimes = parseTimeM True defaultTimeLocale <$> formats <*> dates in
|
let parsedTimes = parseTimeM True defaultTimeLocale <$> formats <*> dates in
|
||||||
foldr (<|>) (timeStamp Nothing) (fmap epoch <$> parsedTimes)
|
foldr (<|>) (timeStamp Nothing) (fmap epoch <$> parsedTimes)
|
||||||
|
|
||||||
makeArticle :: FilePath -> (Metadata -> Metadata) -> ProtoArticle -> (String, Article)
|
makeArticle :: (Metadata -> Metadata) -> Markdown -> (String, Article)
|
||||||
makeArticle filePath metaFilter (title, metadata, bodyOffset, body) = (
|
makeArticle metaFilter markdown@(Markdown {key, metadata}) =
|
||||||
getKey filePath
|
(key, Article $ markdown {metadata = metaFilter metadata})
|
||||||
, Article {
|
|
||||||
key = getKey filePath
|
|
||||||
, title
|
|
||||||
, metadata = metaFilter metadata
|
|
||||||
, bodyOffset
|
|
||||||
, body
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
at :: FilePath -> IO (Either ParseError (String, Article))
|
at :: FilePath -> IO (Either ParseError (String, Article))
|
||||||
at filePath = do
|
at filePath = do
|
||||||
tzOffset <- timeZoneOffsetString <$> getCurrentTimeZone
|
tzOffset <- timeZoneOffsetString <$> getCurrentTimeZone
|
||||||
fileDate <- modificationTime <$> getFileStatus filePath
|
fileDate <- modificationTime <$> getFileStatus filePath
|
||||||
let build = makeArticle filePath (setDate tzOffset fileDate)
|
fmap (makeArticle (setDate tzOffset fileDate)) <$> Markdown.at filePath
|
||||||
fmap build . parse articleP filePath <$> readFile filePath
|
|
||||||
|
|
||||||
getKey :: FilePath -> String
|
preview :: Int -> Article -> Markdown
|
||||||
getKey = dropExtension . takeFileName
|
preview linesCount (Article markdown@(Markdown {body})) =
|
||||||
|
markdown {body = take linesCount $ body}
|
||||||
preview :: Int -> Article -> Article
|
|
||||||
preview linesCount article = article {body = take linesCount $ body article}
|
|
||||||
|
|
52
src/Blog.hs
52
src/Blog.hs
|
@ -15,7 +15,7 @@ module Blog (
|
||||||
import Arguments (Arguments)
|
import Arguments (Arguments)
|
||||||
import qualified Arguments (name, sourceDir)
|
import qualified Arguments (name, sourceDir)
|
||||||
import Article (Article)
|
import Article (Article)
|
||||||
import qualified Article (at, getKey)
|
import qualified Article (at)
|
||||||
import Blog.Path (Path(..))
|
import Blog.Path (Path(..))
|
||||||
import qualified Blog.Path as Path (build)
|
import qualified Blog.Path as Path (build)
|
||||||
import Blog.Template (Environment, Templates, render)
|
import Blog.Template (Environment, Templates, render)
|
||||||
|
@ -34,22 +34,28 @@ import qualified Data.Map as Map (empty, fromList)
|
||||||
import Data.Set (Set)
|
import Data.Set (Set)
|
||||||
import qualified Data.Set as Set (empty, null, singleton, union)
|
import qualified Data.Set as Set (empty, null, singleton, union)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Files (File(..), absolute)
|
import Files (File(..), filePath)
|
||||||
import qualified Files (find)
|
import qualified Files (find)
|
||||||
|
import Markdown (getKey)
|
||||||
|
import Page (Page)
|
||||||
|
import qualified Page (at)
|
||||||
import Prelude hiding (lookup)
|
import Prelude hiding (lookup)
|
||||||
import System.Directory (doesFileExist, withCurrentDirectory)
|
import Pretty (assertRight, onRight)
|
||||||
|
import System.Directory (doesFileExist, makeAbsolute, withCurrentDirectory)
|
||||||
import System.FilePath ((</>), dropTrailingPathSeparator, takeExtension, takeFileName)
|
import System.FilePath ((</>), dropTrailingPathSeparator, takeExtension, takeFileName)
|
||||||
import Text.Parsec (ParseError)
|
import Text.Parsec (ParseError)
|
||||||
|
|
||||||
type Collection = Map String Article
|
type Collection = Map String
|
||||||
|
type Parsed a = Either ParseError (String, a)
|
||||||
|
|
||||||
data Blog = Blog {
|
data Blog = Blog {
|
||||||
articles :: Collection
|
articles :: Collection Article
|
||||||
, hasRSS :: Bool
|
, hasRSS :: Bool
|
||||||
, name :: String
|
, name :: String
|
||||||
|
, pages :: Collection Page
|
||||||
, path :: Path
|
, path :: Path
|
||||||
, skin :: Skin
|
, skin :: Skin
|
||||||
, tags :: Map String (Set String)
|
, tags :: Collection (Set String)
|
||||||
, templates :: Templates
|
, templates :: Templates
|
||||||
, urls :: URL
|
, urls :: URL
|
||||||
, wording :: Wording
|
, wording :: Wording
|
||||||
|
@ -60,40 +66,46 @@ type Renderer m = (MonadIO m, MonadReader Blog m)
|
||||||
template :: Renderer m => String -> Environment -> m Text
|
template :: Renderer m => String -> Environment -> m Text
|
||||||
template key environment = asks templates >>= render key environment
|
template key environment = asks templates >>= render key environment
|
||||||
|
|
||||||
keepOrWarn :: Map String Article -> Either ParseError (String, Article) -> IO (Map String Article)
|
keepOrWarn :: Collection a -> Parsed a -> IO (Collection a)
|
||||||
keepOrWarn accumulator (Left parseErrors) =
|
keepOrWarn accumulator (Left parseErrors) =
|
||||||
forM [show parseErrors, "=> Ignoring this article"] putStrLn
|
forM [show parseErrors, "=> Ignoring this text"] putStrLn
|
||||||
>> return accumulator
|
>> return accumulator
|
||||||
keepOrWarn accumulator (Right (key, article)) =
|
keepOrWarn accumulator (Right (key, article)) =
|
||||||
return $ insert key article accumulator
|
return $ insert key article accumulator
|
||||||
|
|
||||||
findArticles :: FilePath -> IO (Map String Article)
|
find :: (FilePath -> IO (Parsed a)) -> FilePath -> IO (Collection a)
|
||||||
findArticles =
|
find parser =
|
||||||
Files.find
|
Files.find
|
||||||
>=> filterM isMarkDownFile
|
>=> filterM isMarkDownFile
|
||||||
>=> mapM Article.at
|
>=> mapM parser
|
||||||
>=> foldM keepOrWarn Map.empty
|
>=> foldM keepOrWarn Map.empty
|
||||||
where
|
where
|
||||||
isMarkDownFile path = do
|
isMarkDownFile path = do
|
||||||
let correctExtension = takeExtension path == ".md"
|
let correctExtension = takeExtension path == ".md"
|
||||||
(correctExtension &&) <$> doesFileExist path
|
(correctExtension &&) <$> doesFileExist path
|
||||||
|
|
||||||
tagged :: Collection -> FilePath -> IO (String, Set String)
|
tagged :: Collection Article -> FilePath -> IO (String, Set String)
|
||||||
tagged collection path = do
|
tagged collection path = do
|
||||||
links <- Files.find path
|
links <- Files.find path
|
||||||
keys <- forM links $ \link -> do
|
keys <- forM links $ \link -> do
|
||||||
fileExists <- doesFileExist link
|
fileExists <- doesFileExist link
|
||||||
return $ if fileExists
|
return $ if fileExists
|
||||||
then let articleKey = Article.getKey link in
|
then let articleKey = getKey link in
|
||||||
maybe Set.empty (\_ -> Set.singleton articleKey) (lookup articleKey collection)
|
maybe Set.empty (\_ -> Set.singleton articleKey) (lookup articleKey collection)
|
||||||
else Set.empty
|
else Set.empty
|
||||||
return (takeFileName path, foldl Set.union Set.empty keys)
|
return (takeFileName path, foldl Set.union Set.empty keys)
|
||||||
|
|
||||||
discover :: Path -> IO (Collection, Map String (Set String))
|
discover :: Path -> IO (Collection Article, Collection Page, Collection (Set String))
|
||||||
discover path = do
|
discover path = do
|
||||||
articles <- findArticles $ articlesPath path
|
(articles, tags) <- discoverArticles $ articlesPath path
|
||||||
|
pages <- maybe (return Map.empty) (find Page.at) $ pagesPath path
|
||||||
|
return (articles, pages, tags)
|
||||||
|
where
|
||||||
|
discoverArticles Nothing = return (Map.empty, Map.empty)
|
||||||
|
discoverArticles (Just somePath) = do
|
||||||
|
articles <- find Article.at somePath
|
||||||
tags <- Map.fromList . filter (not . Set.null . snd)
|
tags <- Map.fromList . filter (not . Set.null . snd)
|
||||||
<$> (Files.find (articlesPath path </> "tags") >>= mapM (articles `tagged`))
|
<$> (Files.find (somePath </> "tags") >>= mapM (articles `tagged`))
|
||||||
return (articles, tags)
|
return (articles, tags)
|
||||||
|
|
||||||
build :: Arguments -> IO Blog
|
build :: Arguments -> IO Blog
|
||||||
|
@ -102,13 +114,13 @@ build arguments = do
|
||||||
let hasRSS = maybe False (\_-> True) $ rss urls
|
let hasRSS = maybe False (\_-> True) $ rss urls
|
||||||
wording <- Wording.build arguments
|
wording <- Wording.build arguments
|
||||||
templates <- Template.build wording
|
templates <- Template.build wording
|
||||||
root <- Files.absolute . Dir $ Arguments.sourceDir arguments
|
root <- onRight makeAbsolute =<< filePath (Dir $ Arguments.sourceDir arguments)
|
||||||
withCurrentDirectory root $ do
|
withCurrentDirectory root $ do
|
||||||
path <- Path.build root arguments
|
path <- assertRight =<< Path.build root arguments
|
||||||
let name = maybe (takeFileName $ dropTrailingPathSeparator root) id
|
let name = maybe (takeFileName $ dropTrailingPathSeparator root) id
|
||||||
$ Arguments.name arguments
|
$ Arguments.name arguments
|
||||||
skin <- Skin.build name arguments
|
skin <- Skin.build name arguments
|
||||||
(articles, tags) <- discover path
|
(articles, pages, tags) <- discover path
|
||||||
return $ Blog {
|
return $ Blog {
|
||||||
articles, hasRSS, name, path, skin, tags, templates, urls, wording
|
articles, hasRSS, name, pages, path, skin, tags, templates, urls, wording
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{-# LANGUAGE DeriveGeneric #-}
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE CPP #-}
|
||||||
module Blog.Path (
|
module Blog.Path (
|
||||||
Path(..)
|
Path(..)
|
||||||
, build
|
, build
|
||||||
|
@ -8,17 +9,21 @@ module Blog.Path (
|
||||||
|
|
||||||
import Arguments (Arguments)
|
import Arguments (Arguments)
|
||||||
import qualified Arguments as Arguments (Arguments(..))
|
import qualified Arguments as Arguments (Arguments(..))
|
||||||
|
import Control.Monad (join)
|
||||||
|
import Control.Monad.Except (MonadError(..), ExceptT(..), runExceptT)
|
||||||
import Data.Aeson (ToJSON(..), (.=), pairs)
|
import Data.Aeson (ToJSON(..), (.=), pairs)
|
||||||
|
#if !MIN_VERSION_base(4,11,0)
|
||||||
import Data.Monoid ((<>))
|
import Data.Monoid ((<>))
|
||||||
|
#endif
|
||||||
import Files (File(..), filePath)
|
import Files (File(..), filePath)
|
||||||
import GHC.Generics (Generic)
|
import GHC.Generics (Generic)
|
||||||
|
|
||||||
data Path = Path {
|
data Path = Path {
|
||||||
articlesPath :: FilePath
|
articlesPath :: Maybe FilePath
|
||||||
, pagesPath :: Maybe FilePath
|
, pagesPath :: Maybe FilePath
|
||||||
, remarkableConfig :: Maybe FilePath
|
, remarkableConfig :: Maybe FilePath
|
||||||
, root :: FilePath
|
, root :: FilePath
|
||||||
} deriving Generic
|
} deriving (Eq, Generic, Show)
|
||||||
|
|
||||||
instance ToJSON Path where
|
instance ToJSON Path where
|
||||||
toEncoding (Path {articlesPath, pagesPath}) = pairs (
|
toEncoding (Path {articlesPath, pagesPath}) = pairs (
|
||||||
|
@ -26,9 +31,22 @@ instance ToJSON Path where
|
||||||
<> "pagesPath" .= pagesPath
|
<> "pagesPath" .= pagesPath
|
||||||
)
|
)
|
||||||
|
|
||||||
build :: FilePath -> Arguments -> IO Path
|
checkFor :: (FilePath -> File) -> FilePath -> ExceptT String IO (Maybe FilePath)
|
||||||
build root arguments = do
|
checkFor fileOrDir = ExceptT . fmap (Just <$>) . filePath . fileOrDir
|
||||||
articlesPath <- filePath . Dir $ Arguments.articlesPath arguments
|
|
||||||
pagesPath <- mapM (filePath . Dir) $ Arguments.pagesPath arguments
|
getMarkdownPath :: FilePath -> Maybe FilePath -> ExceptT String IO (Maybe FilePath)
|
||||||
remarkableConfig <- mapM (filePath . File) $ Arguments.remarkableConfig arguments
|
getMarkdownPath defaultPath Nothing =
|
||||||
|
ExceptT . (Right . either (\_ -> Nothing) Just <$>) . filePath $ Dir defaultPath
|
||||||
|
getMarkdownPath _ (Just customPath) = checkFor Dir customPath
|
||||||
|
|
||||||
|
build :: FilePath -> Arguments -> IO (Either String Path)
|
||||||
|
build root arguments = runExceptT . join $ pack
|
||||||
|
<$> getMarkdownPath "articles" (Arguments.articlesPath arguments)
|
||||||
|
<*> getMarkdownPath "pages" (Arguments.pagesPath arguments)
|
||||||
|
<*> maybe ignore (checkFor File) (Arguments.remarkableConfig arguments)
|
||||||
|
where
|
||||||
|
pack Nothing Nothing _ =
|
||||||
|
throwError "No articles ? No pages ? Why did you wake me up ? I'm going back to sleep"
|
||||||
|
pack articlesPath pagesPath remarkableConfig =
|
||||||
return $ Path {articlesPath, pagesPath, remarkableConfig, root}
|
return $ Path {articlesPath, pagesPath, remarkableConfig, root}
|
||||||
|
ignore = return Nothing
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{-# LANGUAGE DeriveGeneric #-}
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE CPP #-}
|
||||||
module Blog.Skin (
|
module Blog.Skin (
|
||||||
Skin(..)
|
Skin(..)
|
||||||
, build
|
, build
|
||||||
|
@ -11,7 +12,9 @@ import qualified Arguments (bannerPath, favicon, cardImage, headPath, previewArt
|
||||||
import Control.Monad (filterM)
|
import Control.Monad (filterM)
|
||||||
import Data.Aeson (ToJSON(..), (.=), pairs)
|
import Data.Aeson (ToJSON(..), (.=), pairs)
|
||||||
import Data.Maybe (listToMaybe)
|
import Data.Maybe (listToMaybe)
|
||||||
|
#if !MIN_VERSION_base(4,11,0)
|
||||||
import Data.Monoid ((<>))
|
import Data.Monoid ((<>))
|
||||||
|
#endif
|
||||||
import Files (absoluteLink)
|
import Files (absoluteLink)
|
||||||
import GHC.Generics (Generic)
|
import GHC.Generics (Generic)
|
||||||
import Prelude hiding (head)
|
import Prelude hiding (head)
|
||||||
|
@ -36,7 +39,7 @@ instance ToJSON Skin where
|
||||||
findImage :: String -> Maybe FilePath -> IO (Maybe FilePath)
|
findImage :: String -> Maybe FilePath -> IO (Maybe FilePath)
|
||||||
findImage _ (Just path) = return . Just $ absoluteLink path
|
findImage _ (Just path) = return . Just $ absoluteLink path
|
||||||
findImage name Nothing =
|
findImage name Nothing =
|
||||||
fmap absoluteLink . listToMaybe <$> filterM doesFileExist pathsToCheck
|
listToMaybe <$> filterM doesFileExist pathsToCheck
|
||||||
where
|
where
|
||||||
directories = [".", "image", "images", "pictures", "skin", "static"]
|
directories = [".", "image", "images", "pictures", "skin", "static"]
|
||||||
extensions = ["ico", "gif", "jpeg", "jpg", "png", "svg"]
|
extensions = ["ico", "gif", "jpeg", "jpg", "png", "svg"]
|
||||||
|
|
|
@ -26,12 +26,15 @@ variables :: Map String [Text]
|
||||||
variables = Map.fromList [
|
variables = Map.fromList [
|
||||||
("allLink", [])
|
("allLink", [])
|
||||||
, ("allPage", ["tag"])
|
, ("allPage", ["tag"])
|
||||||
|
, ("articleDescription", ["name"])
|
||||||
, ("commentsLink", [])
|
, ("commentsLink", [])
|
||||||
, ("commentsSection", [])
|
, ("commentsSection", [])
|
||||||
, ("dateFormat", [])
|
, ("dateFormat", [])
|
||||||
, ("latestLink", [])
|
, ("latestLink", [])
|
||||||
, ("latestPage", ["tag"])
|
, ("latestPage", ["tag"])
|
||||||
, ("metadata", ["author", "date", "tags"])
|
, ("metadata", ["author", "date", "tags"])
|
||||||
|
, ("pageDescription", ["name"])
|
||||||
|
, ("pagesList", [])
|
||||||
, ("rssLink", [])
|
, ("rssLink", [])
|
||||||
, ("rssTitle", ["tag"])
|
, ("rssTitle", ["tag"])
|
||||||
, ("tagsList", [])
|
, ("tagsList", [])
|
||||||
|
|
|
@ -6,7 +6,7 @@ module Collection (
|
||||||
, title
|
, title
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Article(Article(metadata))
|
import Article(Article)
|
||||||
import Blog (Blog(..), Path(..))
|
import Blog (Blog(..), Path(..))
|
||||||
import Control.Monad.IO.Class (MonadIO(..))
|
import Control.Monad.IO.Class (MonadIO(..))
|
||||||
import Control.Monad.Reader (MonadReader(..), ReaderT, asks)
|
import Control.Monad.Reader (MonadReader(..), ReaderT, asks)
|
||||||
|
@ -15,6 +15,7 @@ import Data.Map ((!))
|
||||||
import qualified Data.Map as Map (elems, filterWithKey, toList)
|
import qualified Data.Map as Map (elems, filterWithKey, toList)
|
||||||
import Data.Ord (Down(..))
|
import Data.Ord (Down(..))
|
||||||
import qualified Data.Set as Set (member)
|
import qualified Data.Set as Set (member)
|
||||||
|
import Markdown (Markdown(metadata), MarkdownContent(..))
|
||||||
import Pretty ((.$))
|
import Pretty ((.$))
|
||||||
import System.Directory (createDirectoryIfMissing)
|
import System.Directory (createDirectoryIfMissing)
|
||||||
import System.FilePath ((</>))
|
import System.FilePath ((</>))
|
||||||
|
@ -34,7 +35,7 @@ build featured tag = do
|
||||||
featured = sortByDate featured, basePath, tag
|
featured = sortByDate featured, basePath, tag
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
sortByDate = sortOn (Down . (! "date") . metadata)
|
sortByDate = sortOn (Down . (! "date") . metadata . getMarkdown)
|
||||||
|
|
||||||
getAll :: ReaderT Blog IO [Collection]
|
getAll :: ReaderT Blog IO [Collection]
|
||||||
getAll = do
|
getAll = do
|
||||||
|
|
79
src/DOM.hs
79
src/DOM.hs
|
@ -1,18 +1,19 @@
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
module DOM (
|
module DOM (
|
||||||
page
|
HasContent(..)
|
||||||
|
, htmlDocument
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Article (Article(..))
|
import Article (Article)
|
||||||
import qualified Article (preview)
|
import qualified Article (preview)
|
||||||
import ArticlesList (
|
import ArticlesList (
|
||||||
ArticlesList(..), description, getArticles, otherURL, rssLinkTexts
|
ArticlesList(..), description, getArticles, otherURL, rssLinkTexts
|
||||||
)
|
)
|
||||||
import Blog (Blog(..), Path(..), Skin(..), URL(..), template)
|
import Blog (Blog(..), Skin(..), URL(..), template)
|
||||||
import Control.Monad.Reader (ReaderT, asks)
|
import Control.Monad.Reader (ReaderT, asks)
|
||||||
import qualified Data.Map as Map (keys)
|
import Data.Map as Map (Map, toList)
|
||||||
import Data.Text (pack, empty)
|
import Data.Text (Text, pack, empty)
|
||||||
import DOM.Card (HasCard)
|
import DOM.Card (HasCard)
|
||||||
import qualified DOM.Card as Card (make)
|
import qualified DOM.Card as Card (make)
|
||||||
import Files (absoluteLink)
|
import Files (absoluteLink)
|
||||||
|
@ -21,19 +22,24 @@ import Lucid (
|
||||||
, head_, header_, href_, li_, link_, id_, meta_, pre_, rel_, script_, src_
|
, head_, header_, href_, li_, link_, id_, meta_, pre_, rel_, script_, src_
|
||||||
, title_, toHtml, toHtmlRaw, type_, ul_
|
, title_, toHtml, toHtmlRaw, type_, ul_
|
||||||
)
|
)
|
||||||
|
import Markdown (Markdown(..), MarkdownContent(..))
|
||||||
|
import Page (Page)
|
||||||
import Prelude hiding (head, lookup)
|
import Prelude hiding (head, lookup)
|
||||||
import Pretty ((.$))
|
import Pretty ((.$))
|
||||||
import System.FilePath.Posix ((</>), (<.>))
|
import System.FilePath.Posix ((<.>))
|
||||||
|
|
||||||
type HtmlGenerator = HtmlT (ReaderT Blog IO)
|
type HtmlGenerator = HtmlT (ReaderT Blog IO)
|
||||||
|
|
||||||
class HasCard a => Page a where
|
class HasCard a => HasContent a where
|
||||||
content :: a -> HtmlGenerator ()
|
content :: a -> HtmlGenerator ()
|
||||||
|
|
||||||
instance Page Article where
|
instance HasContent Article where
|
||||||
content = article True
|
content = mDContent True . getMarkdown
|
||||||
|
|
||||||
instance Page ArticlesList where
|
instance HasContent Page where
|
||||||
|
content = mDContent True . getMarkdown
|
||||||
|
|
||||||
|
instance HasContent ArticlesList where
|
||||||
content al@(ArticlesList {full}) = do
|
content al@(ArticlesList {full}) = do
|
||||||
preview <- Article.preview <$> (asks $skin.$previewLinesCount)
|
preview <- Article.preview <$> (asks $skin.$previewLinesCount)
|
||||||
h2_ . toHtml =<< description al
|
h2_ . toHtml =<< description al
|
||||||
|
@ -41,7 +47,7 @@ instance Page ArticlesList where
|
||||||
asks hasRSS >>= rssLink
|
asks hasRSS >>= rssLink
|
||||||
li_ . a_ [href_ . pack $ otherURL al, class_ "other"] =<< otherLink
|
li_ . a_ [href_ . pack $ otherURL al, class_ "other"] =<< otherLink
|
||||||
div_ [class_ "articles"] (
|
div_ [class_ "articles"] (
|
||||||
mapM_ (article False . preview) =<< getArticles al
|
mapM_ (mDContent False . preview) =<< getArticles al
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
otherLink =
|
otherLink =
|
||||||
|
@ -52,24 +58,25 @@ instance Page ArticlesList where
|
||||||
li_ . a_ [href_ "rss.xml", class_ "RSS", title_ title] $ toHtml text
|
li_ . a_ [href_ "rss.xml", class_ "RSS", title_ title] $ toHtml text
|
||||||
rssLink False = return ()
|
rssLink False = return ()
|
||||||
|
|
||||||
article :: Bool -> Article -> HtmlGenerator ()
|
mDContent :: Bool -> Markdown -> HtmlGenerator ()
|
||||||
article raw (Article {key, body, Article.title}) = do
|
mDContent raw markdown@(Markdown {key, body}) =
|
||||||
url <- absoluteLink . (</> key <.> extension) <$> (asks $path.$articlesPath)
|
|
||||||
article_ [id_ $ pack key] (do
|
article_ [id_ $ pack key] (do
|
||||||
header_ (do
|
header_ . h1_ $ mDLink raw markdown
|
||||||
a_ [href_ $ pack url] . h1_ $ toHtml title
|
|
||||||
)
|
|
||||||
pre_ . toHtml $ unlines body
|
pre_ . toHtml $ unlines body
|
||||||
)
|
)
|
||||||
where extension = if raw then "md" else "html"
|
|
||||||
|
mDLink :: Bool -> Markdown -> HtmlGenerator ()
|
||||||
|
mDLink raw (Markdown {Markdown.path, title}) =
|
||||||
|
a_ [href_ $ pack url] $ toHtml title
|
||||||
|
where
|
||||||
|
url = absoluteLink $ path <.> (if raw then "md" else "html")
|
||||||
|
|
||||||
tag :: String -> HtmlGenerator ()
|
tag :: String -> HtmlGenerator ()
|
||||||
tag name = li_ (
|
tag name =
|
||||||
a_ [href_ . pack $ absoluteLink name ++ "/", class_ "tag"] $ toHtml name
|
a_ [href_ . pack $ absoluteLink name ++ "/", class_ "tag"] $ toHtml name
|
||||||
)
|
|
||||||
|
|
||||||
defaultBanner :: HtmlGenerator ()
|
defaultBanner :: HtmlGenerator ()
|
||||||
defaultBanner = do
|
defaultBanner =
|
||||||
div_ [id_ "header"] (
|
div_ [id_ "header"] (
|
||||||
a_ [href_ "/"] (
|
a_ [href_ "/"] (
|
||||||
h1_ . toHtml =<< asks name
|
h1_ . toHtml =<< asks name
|
||||||
|
@ -77,13 +84,25 @@ defaultBanner = do
|
||||||
)
|
)
|
||||||
|
|
||||||
faviconLink :: FilePath -> HtmlGenerator ()
|
faviconLink :: FilePath -> HtmlGenerator ()
|
||||||
faviconLink url = link_ [rel_ "shortcut icon", href_ $ pack url, type_ "image/x-icon"]
|
faviconLink url = link_ [
|
||||||
|
rel_ "shortcut icon", href_ . pack $ absoluteLink url, type_ "image/x-icon"
|
||||||
|
]
|
||||||
|
|
||||||
optional :: (a -> HtmlGenerator ()) -> Maybe a -> HtmlGenerator ()
|
optional :: (a -> HtmlGenerator ()) -> Maybe a -> HtmlGenerator ()
|
||||||
optional = maybe (return ())
|
optional = maybe (return ())
|
||||||
|
|
||||||
page :: Page a => a -> HtmlGenerator ()
|
navigationSection ::
|
||||||
page aPage =
|
Text -> String -> ((String, a) -> HtmlGenerator ()) -> Map String a -> HtmlGenerator ()
|
||||||
|
navigationSection sectionId templateKey generator collection
|
||||||
|
| null collection = return ()
|
||||||
|
| otherwise =
|
||||||
|
div_ [id_ sectionId, class_ "navigator"] (do
|
||||||
|
h2_ . toHtml =<< template templateKey []
|
||||||
|
ul_ . mapM_ (li_ . generator) $ Map.toList collection
|
||||||
|
)
|
||||||
|
|
||||||
|
htmlDocument :: HasContent a => a -> HtmlGenerator ()
|
||||||
|
htmlDocument someContent =
|
||||||
doctypehtml_ (do
|
doctypehtml_ (do
|
||||||
head_ (do
|
head_ (do
|
||||||
meta_ [charset_ "utf-8"]
|
meta_ [charset_ "utf-8"]
|
||||||
|
@ -91,15 +110,15 @@ page aPage =
|
||||||
script_ [src_ "/js/remarkable.min.js"] empty
|
script_ [src_ "/js/remarkable.min.js"] empty
|
||||||
script_ [src_ "/js/hablo.js"] empty
|
script_ [src_ "/js/hablo.js"] empty
|
||||||
optional faviconLink =<< (asks $skin.$favicon)
|
optional faviconLink =<< (asks $skin.$favicon)
|
||||||
optional (Card.make aPage) =<< (asks $urls.$cards)
|
optional (Card.make someContent) =<< (asks $urls.$cards)
|
||||||
optional toHtmlRaw =<< (asks $skin.$head)
|
optional toHtmlRaw =<< (asks $skin.$head)
|
||||||
)
|
)
|
||||||
body_ (do
|
body_ (do
|
||||||
maybe defaultBanner toHtmlRaw =<< (asks $skin.$banner)
|
maybe defaultBanner toHtmlRaw =<< (asks $skin.$banner)
|
||||||
div_ [id_ "navigator"] (do
|
asks tags >>= navigationSection "tags" "tagsList"
|
||||||
h2_ . toHtml =<< template "tagsList" []
|
(\(key, _) -> tag key)
|
||||||
ul_ . mapM_ tag . Map.keys =<< asks tags
|
asks pages >>= navigationSection "pages" "pagesList"
|
||||||
)
|
(\(_, page) -> mDLink False $ getMarkdown page)
|
||||||
div_ [id_ "contents"] $ content aPage
|
div_ [id_ "contents"] $ content someContent
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,15 +2,14 @@
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
module DOM.Card (
|
module DOM.Card (
|
||||||
Card(..)
|
HasCard(..)
|
||||||
, HasCard(..)
|
|
||||||
, make
|
, make
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Article (Article(..))
|
import Article (Article(..))
|
||||||
import ArticlesList (ArticlesList(..))
|
import ArticlesList (ArticlesList(..))
|
||||||
import qualified ArticlesList (description)
|
import qualified ArticlesList (description)
|
||||||
import Blog (Blog(..), Renderer, Skin(..))
|
import Blog (Blog(..), Renderer, Skin(..), template)
|
||||||
import Collection (Collection(..))
|
import Collection (Collection(..))
|
||||||
import qualified Collection (title)
|
import qualified Collection (title)
|
||||||
import Control.Applicative ((<|>))
|
import Control.Applicative ((<|>))
|
||||||
|
@ -19,18 +18,18 @@ import qualified Data.Map as Map (lookup)
|
||||||
import Data.Text (Text, pack)
|
import Data.Text (Text, pack)
|
||||||
import Lucid (HtmlT, content_, meta_)
|
import Lucid (HtmlT, content_, meta_)
|
||||||
import Lucid.Base (makeAttribute)
|
import Lucid.Base (makeAttribute)
|
||||||
|
import Markdown (MarkdownContent(..), metadata)
|
||||||
|
import qualified Markdown (Markdown(..))
|
||||||
|
import Page (Page(..))
|
||||||
import Pretty ((.$))
|
import Pretty ((.$))
|
||||||
|
import System.FilePath.Posix ((</>), (<.>))
|
||||||
data Card = Card {
|
|
||||||
cardType :: Text
|
|
||||||
, description :: Text
|
|
||||||
, image :: Maybe String
|
|
||||||
, title :: String
|
|
||||||
, urlPath :: String
|
|
||||||
}
|
|
||||||
|
|
||||||
class HasCard a where
|
class HasCard a where
|
||||||
getCard :: Renderer m => a -> m Card
|
cardType :: Renderer m => a -> m Text
|
||||||
|
description :: Renderer m => a -> m Text
|
||||||
|
image :: Renderer m => a -> m (Maybe String)
|
||||||
|
title :: Renderer m => a -> m String
|
||||||
|
urlPath :: Renderer m => a -> m String
|
||||||
|
|
||||||
og :: Applicative m => Text -> Text -> HtmlT m ()
|
og :: Applicative m => Text -> Text -> HtmlT m ()
|
||||||
og attribute value =
|
og attribute value =
|
||||||
|
@ -41,39 +40,52 @@ og attribute value =
|
||||||
|
|
||||||
make :: (HasCard a, Renderer m) => a -> String -> HtmlT m ()
|
make :: (HasCard a, Renderer m) => a -> String -> HtmlT m ()
|
||||||
make element siteURL = do
|
make element siteURL = do
|
||||||
Card {cardType, description, image, title, urlPath} <- getCard element
|
og "url" . sitePrefix =<< urlPath element
|
||||||
og "url" . pack $ siteURL ++ urlPath
|
og "type" =<< cardType element
|
||||||
og "type" cardType
|
og "title" . pack =<< title element
|
||||||
og "title" $ pack title
|
og "description" =<< description element
|
||||||
og "description" description
|
maybeImage =<< ((<|>) <$> image element <*> (asks $skin.$cardImage))
|
||||||
maybeImage =<< ((image <|>) <$> (asks $skin.$cardImage))
|
|
||||||
og "site_name" =<< (asks $name.$pack)
|
og "site_name" =<< (asks $name.$pack)
|
||||||
where
|
where
|
||||||
maybeImage = maybe (return ()) (og "image" . pack . (siteURL++))
|
maybeImage = maybe (return ()) (og "image" . sitePrefix)
|
||||||
|
sitePrefix = pack . (siteURL </>)
|
||||||
|
|
||||||
instance HasCard Article.Article where
|
mDImage :: (Renderer m, MarkdownContent a ) => a -> m (Maybe String)
|
||||||
getCard (Article.Article {Article.title, Article.metadata}) = do
|
mDImage = return . Map.lookup "featuredImage" . metadata . getMarkdown
|
||||||
description <- pack <$> getDescription (Map.lookup "summary" metadata)
|
|
||||||
return $ Card {
|
mDTitle :: (Renderer m, MarkdownContent a) => a -> m String
|
||||||
cardType = "article"
|
mDTitle = return . Markdown.title . getMarkdown
|
||||||
, description
|
|
||||||
, image = (Map.lookup "featuredImage" metadata)
|
mDUrlPath :: (Renderer m, MarkdownContent a) => a -> m String
|
||||||
, DOM.Card.title
|
mDUrlPath a = return $ Markdown.path (getMarkdown a) <.> "html"
|
||||||
, urlPath = "/articles/" ++ title ++ ".html"
|
|
||||||
}
|
mDDescription :: (Renderer m, MarkdownContent a) => String -> a -> m Text
|
||||||
|
mDDescription key =
|
||||||
|
getDescription . Map.lookup "summary" . metadata . getMarkdown
|
||||||
where
|
where
|
||||||
getDescription = maybe (asks $name.$("A new article on " <>)) return
|
getDescription = maybe defaultDescription (return . pack)
|
||||||
|
defaultDescription = asks name >>= template key . \v -> [("name", pack v)]
|
||||||
|
|
||||||
|
instance HasCard Article where
|
||||||
|
cardType _ = return "article"
|
||||||
|
description = mDDescription "articleDescription"
|
||||||
|
image = mDImage
|
||||||
|
title = mDTitle
|
||||||
|
urlPath = mDUrlPath
|
||||||
|
|
||||||
|
instance HasCard Page where
|
||||||
|
cardType _ = return "website"
|
||||||
|
description = mDDescription "pageDescription"
|
||||||
|
image = mDImage
|
||||||
|
title = mDTitle
|
||||||
|
urlPath = mDUrlPath
|
||||||
|
|
||||||
instance HasCard ArticlesList where
|
instance HasCard ArticlesList where
|
||||||
getCard al@(ArticlesList {collection}) = do
|
cardType _ = return "website"
|
||||||
cardTitle <- Collection.title collection
|
description = ArticlesList.description
|
||||||
description <- ArticlesList.description al
|
image _ = return Nothing
|
||||||
return $ Card {
|
title (ArticlesList {collection}) = Collection.title collection
|
||||||
cardType = "website"
|
urlPath al@(ArticlesList {collection}) =
|
||||||
, description
|
return $ maybe "" id (tag collection) </> file
|
||||||
, image = Nothing
|
|
||||||
, DOM.Card.title = cardTitle
|
|
||||||
, urlPath = maybe "" ('/':) (tag collection) ++ file
|
|
||||||
}
|
|
||||||
where
|
where
|
||||||
file = '/' : (if full al then "all" else "index") ++ ".html"
|
file = (if full al then "all" else "index") <.> ".html"
|
||||||
|
|
31
src/Files.hs
31
src/Files.hs
|
@ -1,35 +1,32 @@
|
||||||
module Files (
|
module Files (
|
||||||
File(..)
|
File(..)
|
||||||
, absolute
|
|
||||||
, absoluteLink
|
, absoluteLink
|
||||||
, filePath
|
, filePath
|
||||||
, find
|
, find
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import System.Exit (die)
|
import System.Directory (doesDirectoryExist, doesFileExist, listDirectory)
|
||||||
import System.Directory (doesDirectoryExist, doesFileExist, listDirectory, makeAbsolute)
|
|
||||||
import System.FilePath ((</>))
|
import System.FilePath ((</>))
|
||||||
|
|
||||||
data File = File FilePath | Dir FilePath
|
data File = File FilePath | Dir FilePath
|
||||||
|
|
||||||
absolute :: File -> IO (FilePath)
|
|
||||||
absolute file = filePath file >>= makeAbsolute
|
|
||||||
|
|
||||||
absoluteLink :: FilePath -> FilePath
|
absoluteLink :: FilePath -> FilePath
|
||||||
absoluteLink ('.':path) = path
|
absoluteLink ('.':path) = path
|
||||||
absoluteLink path = "/" </> path
|
absoluteLink path = "/" </> path
|
||||||
|
|
||||||
filePath :: File -> IO FilePath
|
filePath :: File -> IO (Either String FilePath)
|
||||||
filePath file = do
|
filePath = filePathAux
|
||||||
let (thePath, test, errorMessage) =
|
where
|
||||||
case file of
|
filePathAux (File path) = ifIO doesFileExist path Right (notExist . File)
|
||||||
File path -> (path, doesFileExist, (++ ": no such file"))
|
filePathAux (Dir path) = ifIO doesDirectoryExist path Right (notExist . Dir)
|
||||||
Dir path -> (path, doesDirectoryExist, (++ ": no such directory"))
|
ifIO predicate value whenTrue whenFalse = do
|
||||||
bool <- test thePath
|
result <- predicate value
|
||||||
if bool
|
return $ if result then whenTrue value else whenFalse value
|
||||||
then return thePath
|
notExist (File path) = Left $ path ++ ": no such file"
|
||||||
else die $ errorMessage thePath
|
notExist (Dir path) = Left $ path ++ ": no such directory"
|
||||||
|
|
||||||
find :: FilePath -> IO [FilePath]
|
find :: FilePath -> IO [FilePath]
|
||||||
find path =
|
find path =
|
||||||
fmap (path </>) <$> listDirectory path
|
filePath (Dir path) >>= emptyIfMissing (fmap ((path </>) <$>) . listDirectory)
|
||||||
|
where
|
||||||
|
emptyIfMissing = either (\_ -> return [])
|
||||||
|
|
20
src/HTML.hs
20
src/HTML.hs
|
@ -4,7 +4,6 @@ module HTML (
|
||||||
generate
|
generate
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Article(Article(..))
|
|
||||||
import ArticlesList (ArticlesList(..))
|
import ArticlesList (ArticlesList(..))
|
||||||
import Blog (Blog(..), Path(..))
|
import Blog (Blog(..), Path(..))
|
||||||
import Collection (Collection(..))
|
import Collection (Collection(..))
|
||||||
|
@ -13,8 +12,9 @@ import Control.Monad.IO.Class (MonadIO(..))
|
||||||
import Control.Monad.Reader (ReaderT, asks)
|
import Control.Monad.Reader (ReaderT, asks)
|
||||||
import qualified Data.Map as Map (elems)
|
import qualified Data.Map as Map (elems)
|
||||||
import qualified Data.Text.Lazy.IO as TextIO (writeFile)
|
import qualified Data.Text.Lazy.IO as TextIO (writeFile)
|
||||||
import DOM (page)
|
import DOM (HasContent, htmlDocument)
|
||||||
import Lucid (renderTextT)
|
import Lucid (renderTextT)
|
||||||
|
import Markdown (Markdown(..), MarkdownContent(..))
|
||||||
import Pretty ((.$))
|
import Pretty ((.$))
|
||||||
import System.FilePath.Posix ((</>), (<.>))
|
import System.FilePath.Posix ((</>), (<.>))
|
||||||
|
|
||||||
|
@ -26,19 +26,21 @@ articlesLists collection@(Collection {basePath}) = [
|
||||||
file bool = if bool then "all" else "index"
|
file bool = if bool then "all" else "index"
|
||||||
path bool = basePath </> file bool <.> "html"
|
path bool = basePath </> file bool <.> "html"
|
||||||
|
|
||||||
generateArticles :: [Article] -> ReaderT Blog IO ()
|
generateMarkdown :: (HasContent a, MarkdownContent a) => [a] -> ReaderT Blog IO ()
|
||||||
generateArticles = mapM_ $ \article -> do
|
generateMarkdown = mapM_ $ \content -> do
|
||||||
baseDir <- (</>) <$> (asks $path.$root) <*> (asks $path.$articlesPath)
|
let relativePath = Markdown.path (getMarkdown content) <.> "html"
|
||||||
(renderTextT $ page article)
|
filePath <- (</> relativePath) <$> (asks $Blog.path.$root)
|
||||||
>>= liftIO . TextIO.writeFile (baseDir </> key article <.> "html")
|
(renderTextT $ htmlDocument content) >>= liftIO . TextIO.writeFile filePath
|
||||||
|
|
||||||
generateCollection :: Collection -> ReaderT Blog IO ()
|
generateCollection :: Collection -> ReaderT Blog IO ()
|
||||||
generateCollection (Collection {featured = []}) = return ()
|
generateCollection (Collection {featured = []}) = return ()
|
||||||
generateCollection collection =
|
generateCollection collection =
|
||||||
flip mapM_ (articlesLists collection) $ \(filePath, articlesList) ->
|
flip mapM_ (articlesLists collection) $ \(filePath, articlesList) ->
|
||||||
(renderTextT $ page articlesList) >>= liftIO . TextIO.writeFile filePath
|
(renderTextT $ htmlDocument articlesList)
|
||||||
|
>>= liftIO . TextIO.writeFile filePath
|
||||||
|
|
||||||
generate :: ReaderT Blog IO ()
|
generate :: ReaderT Blog IO ()
|
||||||
generate = do
|
generate = do
|
||||||
asks articles >>= generateArticles . Map.elems
|
asks articles >>= generateMarkdown . Map.elems
|
||||||
Collection.getAll >>= mapM_ generateCollection
|
Collection.getAll >>= mapM_ generateCollection
|
||||||
|
asks pages >>= generateMarkdown . Map.elems
|
||||||
|
|
|
@ -3,6 +3,8 @@ module JS (
|
||||||
generate
|
generate
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import Data.Aeson (encode)
|
||||||
|
|
||||||
import Blog (Blog(..), Path(..))
|
import Blog (Blog(..), Path(..))
|
||||||
import Control.Monad.IO.Class (MonadIO(..))
|
import Control.Monad.IO.Class (MonadIO(..))
|
||||||
import Control.Monad.Reader (ReaderT, asks)
|
import Control.Monad.Reader (ReaderT, asks)
|
||||||
|
@ -31,7 +33,7 @@ var (varName, content) = concat ["\t", pack varName, " : ", content]
|
||||||
|
|
||||||
generateConfig :: FilePath -> ReaderT Blog IO ()
|
generateConfig :: FilePath -> ReaderT Blog IO ()
|
||||||
generateConfig destinationDir = do
|
generateConfig destinationDir = do
|
||||||
blogJSON <- exportBlog
|
blogJSON <- asks (encode . exportBlog)
|
||||||
remarkablePath <- asks $path.$remarkableConfig
|
remarkablePath <- asks $path.$remarkableConfig
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
remarkableJSON <- maybe (return "{html: true}") readFile remarkablePath
|
remarkableJSON <- maybe (return "{html: true}") readFile remarkablePath
|
||||||
|
|
54
src/JSON.hs
54
src/JSON.hs
|
@ -4,58 +4,60 @@ module JSON (
|
||||||
exportBlog
|
exportBlog
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Article (Article)
|
|
||||||
import qualified Article (Article(..))
|
|
||||||
import Blog (Blog, Path, Skin, URL, Wording)
|
import Blog (Blog, Path, Skin, URL, Wording)
|
||||||
import qualified Blog (Blog(..))
|
import qualified Blog (Blog(..))
|
||||||
import Control.Monad.Reader (ReaderT, ask)
|
import Data.Aeson (Options(..), ToJSON(..), genericToEncoding, defaultOptions)
|
||||||
import Data.Aeson (ToJSON(..), genericToEncoding, defaultOptions, encode)
|
|
||||||
import Data.ByteString.Lazy (ByteString)
|
|
||||||
import Data.Map (Map, mapWithKey)
|
import Data.Map (Map, mapWithKey)
|
||||||
import qualified Data.Map as Map (filter, keys)
|
import qualified Data.Map as Map (filter, keys)
|
||||||
import qualified Data.Set as Set (elems, member)
|
import qualified Data.Set as Set (elems, member)
|
||||||
import GHC.Generics
|
import GHC.Generics
|
||||||
|
import Markdown (Markdown, MarkdownContent(..))
|
||||||
|
import qualified Markdown (Markdown(..))
|
||||||
|
|
||||||
data ArticleExport = ArticleExport {
|
data MarkdownExport = MarkdownExport {
|
||||||
title :: String
|
title :: String
|
||||||
, bodyOffset :: Int
|
|
||||||
, metadata :: Map String String
|
, metadata :: Map String String
|
||||||
, tagged :: [String]
|
, bodyOffset :: Int
|
||||||
|
, tagged :: Maybe [String]
|
||||||
} deriving (Generic)
|
} deriving (Generic)
|
||||||
|
|
||||||
instance ToJSON ArticleExport where
|
instance ToJSON MarkdownExport where
|
||||||
toEncoding = genericToEncoding defaultOptions
|
toEncoding = genericToEncoding (defaultOptions {omitNothingFields = True})
|
||||||
|
|
||||||
data BlogDB = BlogDB {
|
exportMarkdown :: Maybe [String] -> Markdown -> MarkdownExport
|
||||||
articles :: Map String ArticleExport
|
exportMarkdown tagged markdown = MarkdownExport {
|
||||||
|
title = Markdown.title markdown
|
||||||
|
, metadata = Markdown.metadata markdown
|
||||||
|
, bodyOffset = Markdown.bodyOffset markdown
|
||||||
|
, tagged
|
||||||
|
}
|
||||||
|
|
||||||
|
data BlogExport = BlogExport {
|
||||||
|
articles :: Map String MarkdownExport
|
||||||
, hasRSS :: Bool
|
, hasRSS :: Bool
|
||||||
, path :: Path
|
, path :: Path
|
||||||
|
, pages :: Map String MarkdownExport
|
||||||
, skin :: Skin
|
, skin :: Skin
|
||||||
, tags :: Map String [String]
|
, tags :: Map String [String]
|
||||||
, urls :: URL
|
, urls :: URL
|
||||||
, wording :: Wording
|
, wording :: Wording
|
||||||
} deriving (Generic)
|
} deriving (Generic)
|
||||||
|
|
||||||
instance ToJSON BlogDB where
|
instance ToJSON BlogExport where
|
||||||
toEncoding = genericToEncoding defaultOptions
|
toEncoding = genericToEncoding defaultOptions
|
||||||
|
|
||||||
exportArticle :: Blog -> String -> Article -> ArticleExport
|
exportBlog :: Blog -> BlogExport
|
||||||
exportArticle blog key article = ArticleExport {
|
exportBlog blog = BlogExport {
|
||||||
title = Article.title article
|
articles = getArticles $ getMarkdown <$> Blog.articles blog
|
||||||
, bodyOffset = Article.bodyOffset article
|
|
||||||
, metadata = Article.metadata article
|
|
||||||
, tagged = Map.keys . Map.filter (Set.member key) $ Blog.tags blog
|
|
||||||
}
|
|
||||||
|
|
||||||
exportBlog :: ReaderT Blog IO ByteString
|
|
||||||
exportBlog = do
|
|
||||||
blog <- ask
|
|
||||||
return . encode $ BlogDB {
|
|
||||||
articles = mapWithKey (exportArticle blog) $ Blog.articles blog
|
|
||||||
, hasRSS = Blog.hasRSS blog
|
, hasRSS = Blog.hasRSS blog
|
||||||
|
, pages = getPages $ getMarkdown <$> Blog.pages blog
|
||||||
, path = Blog.path blog
|
, path = Blog.path blog
|
||||||
, skin = Blog.skin blog
|
, skin = Blog.skin blog
|
||||||
, tags = Set.elems <$> Blog.tags blog
|
, tags = Set.elems <$> Blog.tags blog
|
||||||
, urls = Blog.urls blog
|
, urls = Blog.urls blog
|
||||||
, wording = Blog.wording blog
|
, wording = Blog.wording blog
|
||||||
}
|
}
|
||||||
|
where
|
||||||
|
tag key = Just . Map.keys . Map.filter (Set.member key) $ Blog.tags blog
|
||||||
|
getArticles = mapWithKey (exportMarkdown . tag)
|
||||||
|
getPages = mapWithKey (\_-> exportMarkdown Nothing)
|
||||||
|
|
76
src/Markdown.hs
Normal file
76
src/Markdown.hs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
module Markdown (
|
||||||
|
Markdown(..)
|
||||||
|
, MarkdownContent(..)
|
||||||
|
, Metadata
|
||||||
|
, at
|
||||||
|
, getKey
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Applicative ((<|>))
|
||||||
|
import Data.Map (Map)
|
||||||
|
import qualified Data.Map as Map (fromList)
|
||||||
|
import System.FilePath (dropExtension, takeFileName)
|
||||||
|
import Text.ParserCombinators.Parsec (
|
||||||
|
ParseError, Parser
|
||||||
|
, (<?>)
|
||||||
|
, anyChar, char, count, endBy, eof, getPosition, many, many1, noneOf
|
||||||
|
, oneOf, option, parse, skipMany, sourceLine, sourceName, string, try
|
||||||
|
)
|
||||||
|
|
||||||
|
type Metadata = Map String String
|
||||||
|
data Markdown = Markdown {
|
||||||
|
key :: String
|
||||||
|
, path :: String
|
||||||
|
, title :: String
|
||||||
|
, metadata :: Metadata
|
||||||
|
, bodyOffset :: Int
|
||||||
|
, body :: [String]
|
||||||
|
}
|
||||||
|
|
||||||
|
class MarkdownContent a where
|
||||||
|
getMarkdown :: a -> Markdown
|
||||||
|
|
||||||
|
parser :: Parser Markdown
|
||||||
|
parser = do
|
||||||
|
(title, metadata) <- skipMany eol *> (headerP <|> reverseHeaderP)
|
||||||
|
bodyOffset <- skipMany eol *> (pred . sourceLine <$> getPosition)
|
||||||
|
body <- lines <$> many anyChar <* eof
|
||||||
|
inputFile <- sourceName <$> getPosition
|
||||||
|
let (key, path) = (getKey inputFile, dropExtension inputFile)
|
||||||
|
return $ Markdown {key, path, title, metadata, bodyOffset, body}
|
||||||
|
where
|
||||||
|
headerP = (,) <$> titleP <* many eol <*> metadataP
|
||||||
|
reverseHeaderP = flip (,) <$> metadataP <* many eol<*> titleP
|
||||||
|
|
||||||
|
metadataP :: Parser Metadata
|
||||||
|
metadataP = Map.fromList <$> option [] (
|
||||||
|
metaSectionSeparator *> many eol *>
|
||||||
|
(try keyVal) `endBy` (many1 eol)
|
||||||
|
<* metaSectionSeparator
|
||||||
|
) <?> "metadata section"
|
||||||
|
where
|
||||||
|
metaSectionSeparator = count 3 (oneOf "~-") *> eol
|
||||||
|
spaces = skipMany $ char ' '
|
||||||
|
keyVal = (,) <$> (no ": \r\n" <* spaces <* char ':' <* spaces) <*> no "\r\n"
|
||||||
|
|
||||||
|
titleP :: Parser String
|
||||||
|
titleP = try (singleLine <|> underlined)
|
||||||
|
where
|
||||||
|
singleLine = char '#' *> char ' ' *> no "\r\n" <* eol
|
||||||
|
underlined =
|
||||||
|
no "\r\n" <* eol
|
||||||
|
>>= \titleLine -> count (length titleLine) (oneOf "#=") *> eol *> return titleLine
|
||||||
|
<?> "'#' or '=' to underline the title"
|
||||||
|
|
||||||
|
eol :: Parser String
|
||||||
|
eol = try (string "\r\n") <|> string "\r" <|> string "\n" <?> "newline"
|
||||||
|
|
||||||
|
no :: String -> Parser String
|
||||||
|
no = many1 . noneOf
|
||||||
|
|
||||||
|
getKey :: FilePath -> String
|
||||||
|
getKey = dropExtension . takeFileName
|
||||||
|
|
||||||
|
at :: FilePath -> IO (Either ParseError Markdown)
|
||||||
|
at filePath = parse parser filePath <$> readFile filePath
|
17
src/Page.hs
Normal file
17
src/Page.hs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
module Page (
|
||||||
|
Page(..)
|
||||||
|
, at
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Markdown (Markdown(..), MarkdownContent(..))
|
||||||
|
import qualified Markdown as Markdown (at)
|
||||||
|
import Text.ParserCombinators.Parsec (ParseError)
|
||||||
|
|
||||||
|
newtype Page = Page Markdown
|
||||||
|
instance MarkdownContent Page where
|
||||||
|
getMarkdown (Page markdown) = markdown
|
||||||
|
|
||||||
|
at :: FilePath -> IO (Either ParseError (String, Page))
|
||||||
|
at filePath = fmap makePage <$> Markdown.at filePath
|
||||||
|
where
|
||||||
|
makePage markdown = (key markdown, Page markdown)
|
|
@ -1,6 +1,16 @@
|
||||||
module Pretty (
|
module Pretty (
|
||||||
(.$)
|
(.$)
|
||||||
|
, assertRight
|
||||||
|
, onRight
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import System.Exit (die)
|
||||||
|
|
||||||
(.$) :: (a -> b) -> (b -> c) -> (a -> c)
|
(.$) :: (a -> b) -> (b -> c) -> (a -> c)
|
||||||
(.$) f g = g . f
|
(.$) f g = g . f
|
||||||
|
|
||||||
|
onRight :: (a -> IO b) -> Either String a -> IO b
|
||||||
|
onRight = either die
|
||||||
|
|
||||||
|
assertRight :: Either String a -> IO a
|
||||||
|
assertRight = onRight return
|
||||||
|
|
|
@ -8,7 +8,7 @@ module RSS (
|
||||||
import Article (Article(..))
|
import Article (Article(..))
|
||||||
import ArticlesList (ArticlesList(..), getArticles)
|
import ArticlesList (ArticlesList(..), getArticles)
|
||||||
import qualified ArticlesList (description)
|
import qualified ArticlesList (description)
|
||||||
import Blog (Blog(..), Path(..), Renderer, URL(..))
|
import Blog (Blog(urls), Renderer, URL(..))
|
||||||
import Collection (Collection(..), getAll)
|
import Collection (Collection(..), getAll)
|
||||||
import qualified Collection (title)
|
import qualified Collection (title)
|
||||||
import Control.Monad.IO.Class (MonadIO(..))
|
import Control.Monad.IO.Class (MonadIO(..))
|
||||||
|
@ -20,6 +20,7 @@ import Data.Time (defaultTimeLocale, formatTime, rfc822DateFormat)
|
||||||
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
|
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
|
||||||
import Lucid (Attribute, HtmlT, Term, ToHtml(..), term, renderTextT)
|
import Lucid (Attribute, HtmlT, Term, ToHtml(..), term, renderTextT)
|
||||||
import Lucid.Base (makeAttribute)
|
import Lucid.Base (makeAttribute)
|
||||||
|
import Markdown (Markdown(..))
|
||||||
import Pretty ((.$))
|
import Pretty ((.$))
|
||||||
import System.FilePath.Posix ((</>), (<.>))
|
import System.FilePath.Posix ((</>), (<.>))
|
||||||
|
|
||||||
|
@ -57,13 +58,12 @@ pubDate_ :: Term arg result => arg -> result
|
||||||
pubDate_ = term "pubDate"
|
pubDate_ = term "pubDate"
|
||||||
|
|
||||||
articleItem :: MonadReader Blog m => String -> Article -> HtmlT m ()
|
articleItem :: MonadReader Blog m => String -> Article -> HtmlT m ()
|
||||||
articleItem siteURL (Article {key, metadata, title}) =
|
articleItem siteURL (Article (Markdown {path, metadata, title})) =
|
||||||
item_ $ do
|
item_ $ do
|
||||||
title_ $ toHtml title
|
title_ $ toHtml title
|
||||||
link_ . toHtml =<< link <$> (asks $path.$articlesPath)
|
link_ $ toHtml (siteURL </> path <.> "html")
|
||||||
pubDate_ . toHtml . rfc822Date $ metadata ! "date"
|
pubDate_ . toHtml . rfc822Date $ metadata ! "date"
|
||||||
where
|
where
|
||||||
link path = siteURL </> path </> key <.> "html"
|
|
||||||
rfc822Date =
|
rfc822Date =
|
||||||
formatTime defaultTimeLocale rfc822DateFormat
|
formatTime defaultTimeLocale rfc822DateFormat
|
||||||
. posixSecondsToUTCTime . fromIntegral . (read :: String -> Int)
|
. posixSecondsToUTCTime . fromIntegral . (read :: String -> Int)
|
||||||
|
|
86
test/Mock/Arguments.hs
Normal file
86
test/Mock/Arguments.hs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
module Mock.Arguments (
|
||||||
|
badCustomArticles
|
||||||
|
, badCustomPages
|
||||||
|
, bothCustom
|
||||||
|
, bothDefault
|
||||||
|
, customArticles
|
||||||
|
, customArticlesDefaultPages
|
||||||
|
, customPages
|
||||||
|
, customPagesDefaultArticles
|
||||||
|
, defaultArticles
|
||||||
|
, defaultPages
|
||||||
|
, emptyBlog
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Arguments (Arguments(..))
|
||||||
|
import Utils (testDataPath)
|
||||||
|
|
||||||
|
defaultArticles :: Arguments
|
||||||
|
defaultArticles = BlogConfig {
|
||||||
|
sourceDir = testDataPath "Structure/defaultArticles"
|
||||||
|
, articlesPath = Nothing
|
||||||
|
, bannerPath = Nothing
|
||||||
|
, cardImage = Nothing
|
||||||
|
, commentsURL = Nothing
|
||||||
|
, favicon = Nothing
|
||||||
|
, headPath = Nothing
|
||||||
|
, name = Nothing
|
||||||
|
, openGraphCards = False
|
||||||
|
, pagesPath = Nothing
|
||||||
|
, previewArticlesCount = 3
|
||||||
|
, previewLinesCount = 10
|
||||||
|
, remarkableConfig = Nothing
|
||||||
|
, rss = False
|
||||||
|
, siteURL = Nothing
|
||||||
|
, wording = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultPages :: Arguments
|
||||||
|
defaultPages = defaultArticles {
|
||||||
|
sourceDir = testDataPath "Structure/defaultPages"
|
||||||
|
}
|
||||||
|
|
||||||
|
bothDefault :: Arguments
|
||||||
|
bothDefault = defaultArticles {
|
||||||
|
sourceDir = testDataPath "Structure/both"
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyBlog :: Arguments
|
||||||
|
emptyBlog = defaultArticles {
|
||||||
|
sourceDir = testDataPath "Structure/custom"
|
||||||
|
}
|
||||||
|
|
||||||
|
customArticles :: Arguments
|
||||||
|
customArticles = emptyBlog {
|
||||||
|
articlesPath = Just "customArticles"
|
||||||
|
}
|
||||||
|
|
||||||
|
customArticlesDefaultPages :: Arguments
|
||||||
|
customArticlesDefaultPages = bothDefault {
|
||||||
|
articlesPath = Just "customArticles"
|
||||||
|
}
|
||||||
|
|
||||||
|
customPages :: Arguments
|
||||||
|
customPages = emptyBlog {
|
||||||
|
pagesPath = Just "customPages"
|
||||||
|
}
|
||||||
|
|
||||||
|
customPagesDefaultArticles :: Arguments
|
||||||
|
customPagesDefaultArticles = bothDefault {
|
||||||
|
pagesPath = Just "customPages"
|
||||||
|
}
|
||||||
|
|
||||||
|
bothCustom :: Arguments
|
||||||
|
bothCustom = customArticles {
|
||||||
|
pagesPath = Just "customPages"
|
||||||
|
}
|
||||||
|
|
||||||
|
badCustomArticles :: Arguments
|
||||||
|
badCustomArticles = bothDefault {
|
||||||
|
articlesPath = Just "missingDirectory"
|
||||||
|
}
|
||||||
|
|
||||||
|
badCustomPages :: Arguments
|
||||||
|
badCustomPages = bothDefault {
|
||||||
|
pagesPath = Just "missingDirectory"
|
||||||
|
}
|
23
test/Mock/Article.hs
Normal file
23
test/Mock/Article.hs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
module Mock.Article (
|
||||||
|
noDescription
|
||||||
|
, noImage
|
||||||
|
, noMeta
|
||||||
|
, simple
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Article (Article(..))
|
||||||
|
import qualified Data.Map as Map (fromList)
|
||||||
|
import Markdown (Markdown(..))
|
||||||
|
import Mock.Markdown (article)
|
||||||
|
|
||||||
|
simple :: Article
|
||||||
|
simple = Article article
|
||||||
|
|
||||||
|
noImage :: Article
|
||||||
|
noImage = Article $ article {metadata = Map.fromList [("summary", "It's a test")]}
|
||||||
|
|
||||||
|
noDescription :: Article
|
||||||
|
noDescription = Article $ article {metadata = Map.fromList [("featuredImage", "test.png")]}
|
||||||
|
|
||||||
|
noMeta :: Article
|
||||||
|
noMeta = Article $ article {metadata = Map.fromList []}
|
22
test/Mock/ArticlesList.hs
Normal file
22
test/Mock/ArticlesList.hs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
module Mock.ArticlesList (
|
||||||
|
longMain
|
||||||
|
, longTesting
|
||||||
|
, shortMain
|
||||||
|
, shortTesting
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ArticlesList (ArticlesList(..))
|
||||||
|
import Mock.Collection (main, testing)
|
||||||
|
import Prelude hiding (all)
|
||||||
|
|
||||||
|
shortMain :: IO ArticlesList
|
||||||
|
shortMain = ArticlesList False <$> main
|
||||||
|
|
||||||
|
shortTesting :: IO ArticlesList
|
||||||
|
shortTesting = ArticlesList False <$> testing
|
||||||
|
|
||||||
|
longMain :: IO ArticlesList
|
||||||
|
longMain = ArticlesList True <$> main
|
||||||
|
|
||||||
|
longTesting :: IO ArticlesList
|
||||||
|
longTesting = ArticlesList True <$> testing
|
39
test/Mock/Blog.hs
Normal file
39
test/Mock/Blog.hs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
module Mock.Blog (
|
||||||
|
noCards
|
||||||
|
, noRSS
|
||||||
|
, simple
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Blog (Blog(..))
|
||||||
|
import qualified Data.Map as Map (fromList)
|
||||||
|
import qualified Data.Set as Set (fromList)
|
||||||
|
import qualified Mock.Article (simple)
|
||||||
|
import qualified Mock.Blog.Path (defaultArticles)
|
||||||
|
import qualified Mock.Blog.Skin (simple)
|
||||||
|
import qualified Mock.Blog.Template (simple)
|
||||||
|
import qualified Mock.Blog.URL (simple, noCards)
|
||||||
|
import qualified Mock.Blog.Wording (defaultWording)
|
||||||
|
|
||||||
|
simple :: IO Blog
|
||||||
|
simple =
|
||||||
|
let wording = Mock.Blog.Wording.defaultWording in do
|
||||||
|
templates <- Mock.Blog.Template.simple
|
||||||
|
return $ Blog {
|
||||||
|
articles = Map.fromList [("test", Mock.Article.simple)]
|
||||||
|
, hasRSS = True
|
||||||
|
, name = "The Test Blog"
|
||||||
|
, pages = Map.fromList []
|
||||||
|
, path = Mock.Blog.Path.defaultArticles
|
||||||
|
, skin = Mock.Blog.Skin.simple
|
||||||
|
, tags = Map.fromList [("testing", Set.fromList ["test"])]
|
||||||
|
, templates
|
||||||
|
, urls = Mock.Blog.URL.simple
|
||||||
|
, wording
|
||||||
|
}
|
||||||
|
|
||||||
|
noCards :: IO Blog
|
||||||
|
noCards = (\b -> b {urls = Mock.Blog.URL.noCards}) <$> simple
|
||||||
|
|
||||||
|
noRSS :: IO Blog
|
||||||
|
noRSS = (\b -> b {hasRSS = False}) <$> simple
|
66
test/Mock/Blog/Path.hs
Normal file
66
test/Mock/Blog/Path.hs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
module Mock.Blog.Path (
|
||||||
|
bothCustom
|
||||||
|
, bothDefault
|
||||||
|
, customArticles
|
||||||
|
, customArticlesDefaultPages
|
||||||
|
, customPages
|
||||||
|
, customPagesDefaultArticles
|
||||||
|
, defaultArticles
|
||||||
|
, defaultPages
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Blog.Path (Path(..))
|
||||||
|
|
||||||
|
defaultArticles :: Path
|
||||||
|
defaultArticles = Path {
|
||||||
|
articlesPath = Just "articles"
|
||||||
|
, pagesPath = Nothing
|
||||||
|
, remarkableConfig = Nothing
|
||||||
|
, root = "test/Structure/defaultArticles"
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultPages :: Path
|
||||||
|
defaultPages = Path {
|
||||||
|
articlesPath = Nothing
|
||||||
|
, pagesPath = Just "pages"
|
||||||
|
, remarkableConfig = Nothing
|
||||||
|
, root = "test/Structure/defaultPages"
|
||||||
|
}
|
||||||
|
|
||||||
|
bothDefault :: Path
|
||||||
|
bothDefault = Path {
|
||||||
|
articlesPath = Just "articles"
|
||||||
|
, pagesPath = Just "pages"
|
||||||
|
, remarkableConfig = Nothing
|
||||||
|
, root = "test/Structure/both"
|
||||||
|
}
|
||||||
|
|
||||||
|
customArticles :: Path
|
||||||
|
customArticles = Path {
|
||||||
|
articlesPath = Just "customArticles"
|
||||||
|
, pagesPath = Nothing
|
||||||
|
, remarkableConfig = Nothing
|
||||||
|
, root = "test/Structure/custom"
|
||||||
|
}
|
||||||
|
|
||||||
|
bothCustom :: Path
|
||||||
|
bothCustom = customArticles {
|
||||||
|
pagesPath = Just "customPages"
|
||||||
|
}
|
||||||
|
|
||||||
|
customPages :: Path
|
||||||
|
customPages = bothCustom {
|
||||||
|
articlesPath = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
customArticlesDefaultPages :: Path
|
||||||
|
customArticlesDefaultPages = bothDefault {
|
||||||
|
articlesPath = Just "customArticles"
|
||||||
|
, pagesPath = Just "pages"
|
||||||
|
}
|
||||||
|
|
||||||
|
customPagesDefaultArticles :: Path
|
||||||
|
customPagesDefaultArticles = customArticlesDefaultPages {
|
||||||
|
articlesPath = Just "articles"
|
||||||
|
, pagesPath = Just "customPages"
|
||||||
|
}
|
16
test/Mock/Blog/Skin.hs
Normal file
16
test/Mock/Blog/Skin.hs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
module Mock.Blog.Skin (
|
||||||
|
simple
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Blog.Skin (Skin(..))
|
||||||
|
import Prelude hiding (head)
|
||||||
|
|
||||||
|
simple :: Skin
|
||||||
|
simple = Skin {
|
||||||
|
banner = Nothing
|
||||||
|
, cardImage = Nothing
|
||||||
|
, favicon = Nothing
|
||||||
|
, head = Nothing
|
||||||
|
, previewArticlesCount = 3
|
||||||
|
, previewLinesCount = 10
|
||||||
|
}
|
9
test/Mock/Blog/Template.hs
Normal file
9
test/Mock/Blog/Template.hs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
module Mock.Blog.Template (
|
||||||
|
simple
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Blog.Template (Templates, build)
|
||||||
|
import Mock.Blog.Wording (defaultWording)
|
||||||
|
|
||||||
|
simple :: IO Templates
|
||||||
|
simple = build Mock.Blog.Wording.defaultWording
|
16
test/Mock/Blog/URL.hs
Normal file
16
test/Mock/Blog/URL.hs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
module Mock.Blog.URL (
|
||||||
|
noCards
|
||||||
|
, simple
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Blog.URL (URL(..))
|
||||||
|
|
||||||
|
simple :: URL
|
||||||
|
simple = URL {
|
||||||
|
cards = Just "https://test.net"
|
||||||
|
, comments = Nothing
|
||||||
|
, rss = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
noCards :: URL
|
||||||
|
noCards = simple {cards = Nothing}
|
25
test/Mock/Blog/Wording.hs
Normal file
25
test/Mock/Blog/Wording.hs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
module Mock.Blog.Wording (
|
||||||
|
defaultWording
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Blog.Wording (Wording(..))
|
||||||
|
import qualified Data.Map as Map (fromList)
|
||||||
|
|
||||||
|
defaultWording :: Wording
|
||||||
|
defaultWording = Wording $ Map.fromList [
|
||||||
|
("allLink", "See all")
|
||||||
|
, ("allPage", "All articles{? tagged ${tag}?}")
|
||||||
|
, ("articleDescription", "A new article on ${name}")
|
||||||
|
, ("commentsLink", "Comment on the fediverse")
|
||||||
|
, ("commentsSection", "Comments")
|
||||||
|
, ("dateFormat", "en-US")
|
||||||
|
, ("latestLink", "See only latest")
|
||||||
|
, ("latestPage", "Latest articles{? tagged ${tag}?}")
|
||||||
|
, ("metadata", "{?by ${author} ?}on ${date}{? tagged ${tags}?}")
|
||||||
|
, ("pageDescription", "Read on ${name}")
|
||||||
|
, ("pagesList", "Pages")
|
||||||
|
, ("rssLink", "Subscribe")
|
||||||
|
, ("rssTitle", "Follow all articles{? tagged ${tag}?}")
|
||||||
|
, ("tagsList", "Tags")
|
||||||
|
]
|
28
test/Mock/Collection.hs
Normal file
28
test/Mock/Collection.hs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
module Mock.Collection (
|
||||||
|
main
|
||||||
|
, testing
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Blog (Blog(..), Path(..))
|
||||||
|
import Collection (Collection(..))
|
||||||
|
import Data.Map as Map (elems)
|
||||||
|
import qualified Mock.Blog (simple)
|
||||||
|
import System.FilePath ((</>))
|
||||||
|
|
||||||
|
main :: IO Collection
|
||||||
|
main = do
|
||||||
|
blog <- Mock.Blog.simple
|
||||||
|
return $ Collection {
|
||||||
|
featured = Map.elems $ articles blog
|
||||||
|
, basePath = root $ path blog
|
||||||
|
, tag = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
testing :: IO Collection
|
||||||
|
testing = do
|
||||||
|
blog <- Mock.Blog.simple
|
||||||
|
return $ Collection {
|
||||||
|
featured = Map.elems $ articles blog
|
||||||
|
, basePath = root (path blog) </> "testing"
|
||||||
|
, tag = Just "testing"
|
||||||
|
}
|
33
test/Mock/Markdown.hs
Normal file
33
test/Mock/Markdown.hs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
module Mock.Markdown (
|
||||||
|
article
|
||||||
|
, page
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Map as Map (fromList)
|
||||||
|
import Markdown (Markdown(..))
|
||||||
|
|
||||||
|
article :: Markdown
|
||||||
|
article = Markdown {
|
||||||
|
key = "test"
|
||||||
|
, path = "articles/test"
|
||||||
|
, Markdown.title = "Some test"
|
||||||
|
, metadata = Map.fromList [
|
||||||
|
("summary", "It's a test")
|
||||||
|
, ("featuredImage", "test.png")
|
||||||
|
]
|
||||||
|
, bodyOffset = 3
|
||||||
|
, body = []
|
||||||
|
}
|
||||||
|
|
||||||
|
page :: Markdown
|
||||||
|
page = Markdown {
|
||||||
|
key = "test"
|
||||||
|
, path = "pages/test"
|
||||||
|
, Markdown.title = "A test page"
|
||||||
|
, metadata = Map.fromList [
|
||||||
|
("summary", "Tests are useful")
|
||||||
|
, ("featuredImage", "test.png")
|
||||||
|
]
|
||||||
|
, bodyOffset = 3
|
||||||
|
, body = []
|
||||||
|
}
|
40
test/Structure.hs
Normal file
40
test/Structure.hs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
module Structure (
|
||||||
|
test
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Arguments (Arguments(..))
|
||||||
|
import Blog (Path)
|
||||||
|
import qualified Blog.Path as Path (build)
|
||||||
|
import Distribution.TestSuite
|
||||||
|
import qualified Mock.Arguments as Arguments
|
||||||
|
import qualified Mock.Blog.Path as Path
|
||||||
|
import System.Directory (withCurrentDirectory)
|
||||||
|
import Utils (simpleTest, tag)
|
||||||
|
|
||||||
|
checkPath :: Arguments -> Maybe Path -> IO Progress
|
||||||
|
checkPath input expected = do
|
||||||
|
withCurrentDirectory root $ do
|
||||||
|
actual <- either (\_ -> Nothing) Just <$> Path.build root input
|
||||||
|
return . Finished $
|
||||||
|
if actual == expected
|
||||||
|
then Pass
|
||||||
|
else Fail $ "Expected " ++ show expected ++ " but got " ++ show actual
|
||||||
|
where
|
||||||
|
root = sourceDir input
|
||||||
|
|
||||||
|
test :: Test
|
||||||
|
test = tag "structure" . testGroup "Blog structure" $ simpleTest <$> [
|
||||||
|
("empty structure", checkPath Arguments.emptyBlog Nothing)
|
||||||
|
, ("default articles", checkPath Arguments.defaultArticles $ Just Path.defaultArticles)
|
||||||
|
, ("default pages", checkPath Arguments.defaultPages $ Just Path.defaultPages)
|
||||||
|
, ("both default", checkPath Arguments.bothDefault $ Just Path.bothDefault)
|
||||||
|
, ("custom articles", checkPath Arguments.customArticles $ Just Path.customArticles)
|
||||||
|
, ("custom pages", checkPath Arguments.customPages $ Just Path.customPages)
|
||||||
|
, ("both custom", checkPath Arguments.bothCustom $ Just Path.bothCustom)
|
||||||
|
, ("custom articles, default pages"
|
||||||
|
, checkPath Arguments.customArticlesDefaultPages $ Just Path.customArticlesDefaultPages)
|
||||||
|
, ("custom pages, default articles"
|
||||||
|
, checkPath Arguments.customPagesDefaultArticles $ Just Path.customPagesDefaultArticles)
|
||||||
|
, ("bad custom articles", checkPath Arguments.badCustomArticles $ Nothing)
|
||||||
|
, ("bad custom pages", checkPath Arguments.badCustomPages $ Nothing)
|
||||||
|
]
|
0
test/Structure/.placeHolder
Normal file
0
test/Structure/.placeHolder
Normal file
0
test/Structure/both/.placeHolder
Normal file
0
test/Structure/both/.placeHolder
Normal file
0
test/Structure/both/articles/.placeHolder
Normal file
0
test/Structure/both/articles/.placeHolder
Normal file
0
test/Structure/both/customArticles/.placeHolder
Normal file
0
test/Structure/both/customArticles/.placeHolder
Normal file
0
test/Structure/both/customPages/.placeHolder
Normal file
0
test/Structure/both/customPages/.placeHolder
Normal file
0
test/Structure/both/pages/.placeHolder
Normal file
0
test/Structure/both/pages/.placeHolder
Normal file
0
test/Structure/custom/.placeHolder
Normal file
0
test/Structure/custom/.placeHolder
Normal file
0
test/Structure/custom/customArticles/.placeHolder
Normal file
0
test/Structure/custom/customArticles/.placeHolder
Normal file
0
test/Structure/custom/customPages/.placeHolder
Normal file
0
test/Structure/custom/customPages/.placeHolder
Normal file
0
test/Structure/defaultArticles/.placeHolder
Normal file
0
test/Structure/defaultArticles/.placeHolder
Normal file
0
test/Structure/defaultArticles/articles/.placeHolder
Normal file
0
test/Structure/defaultArticles/articles/.placeHolder
Normal file
0
test/Structure/defaultPages/.placeHolder
Normal file
0
test/Structure/defaultPages/.placeHolder
Normal file
0
test/Structure/defaultPages/pages/.placeHolder
Normal file
0
test/Structure/defaultPages/pages/.placeHolder
Normal file
14
test/Tests.hs
Normal file
14
test/Tests.hs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
module Tests (
|
||||||
|
tests
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Distribution.TestSuite
|
||||||
|
import qualified Structure (test)
|
||||||
|
import Utils (tag)
|
||||||
|
import qualified XML.Card (test)
|
||||||
|
|
||||||
|
tests :: IO [Test]
|
||||||
|
tests = return $ tag "xml" <$> [
|
||||||
|
XML.Card.test
|
||||||
|
, Structure.test
|
||||||
|
]
|
49
test/Utils.hs
Normal file
49
test/Utils.hs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
module Utils (
|
||||||
|
assertAll
|
||||||
|
, assertEqual
|
||||||
|
, simpleTest
|
||||||
|
, tag
|
||||||
|
, testDataPath
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Distribution.TestSuite
|
||||||
|
import System.FilePath ((</>))
|
||||||
|
import Text.Printf (printf)
|
||||||
|
|
||||||
|
tagInstance :: String -> TestInstance -> TestInstance
|
||||||
|
tagInstance tagName testInstance = testInstance {
|
||||||
|
tags = tagName : (tags testInstance)
|
||||||
|
}
|
||||||
|
|
||||||
|
tag :: String -> Test -> Test
|
||||||
|
tag tagName (Test testInstance) = Test (tagInstance tagName testInstance)
|
||||||
|
tag tagName group = group {groupTests = tag tagName <$> groupTests group}
|
||||||
|
|
||||||
|
simpleTest :: (String, IO Progress) -> Test
|
||||||
|
simpleTest (name, run) = Test testInstance
|
||||||
|
where
|
||||||
|
testInstance = TestInstance {
|
||||||
|
run
|
||||||
|
, name
|
||||||
|
, tags = []
|
||||||
|
, options = []
|
||||||
|
, setOption = \_ _ -> Right testInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
wrong :: Show a => String -> a -> a -> IO Progress
|
||||||
|
wrong message expected actual = return . Finished . Fail $
|
||||||
|
printf "%s: %s vs. %s" message (show expected) (show actual)
|
||||||
|
|
||||||
|
assertAll :: [(Bool, IO Progress, String)] -> IO Progress
|
||||||
|
assertAll = foldr assert (return $ Finished Pass)
|
||||||
|
where
|
||||||
|
assert (bool, badIssue, checkMessage) next =
|
||||||
|
if bool then return $ Progress checkMessage next else badIssue
|
||||||
|
|
||||||
|
assertEqual :: (Show a, Eq a) => String -> a -> a -> (Bool, IO Progress, String)
|
||||||
|
assertEqual what a b =
|
||||||
|
(a == b, wrong (what ++ " do not match !") a b, what ++ " ok")
|
||||||
|
|
||||||
|
testDataPath :: FilePath -> FilePath
|
||||||
|
testDataPath = ("test" </>)
|
11
test/XML/Card.hs
Normal file
11
test/XML/Card.hs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
module XML.Card (
|
||||||
|
test
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Distribution.TestSuite
|
||||||
|
import Utils (tag)
|
||||||
|
import qualified XML.Card.Component as Component (test)
|
||||||
|
import qualified XML.Card.Output as Output (test)
|
||||||
|
|
||||||
|
test :: Test
|
||||||
|
test = tag "card" $ testGroup "Cards" [Component.test, Output.test]
|
89
test/XML/Card/Component.hs
Normal file
89
test/XML/Card/Component.hs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
module XML.Card.Component (
|
||||||
|
test
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Blog (Blog)
|
||||||
|
import Control.Monad.IO.Class (MonadIO(..))
|
||||||
|
import Control.Monad.Reader (runReaderT)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Distribution.TestSuite
|
||||||
|
import DOM.Card (HasCard(..))
|
||||||
|
import Mock.Blog as Blog (simple)
|
||||||
|
import Mock.Article as Article (noDescription, noImage, simple)
|
||||||
|
import Mock.ArticlesList as ArticlesList (
|
||||||
|
longMain, longTesting, shortMain, shortTesting
|
||||||
|
)
|
||||||
|
import Utils (assertAll, assertEqual, simpleTest, tag)
|
||||||
|
|
||||||
|
check :: HasCard a => IO Blog -> a -> (Text, Text, Maybe String, String, String) -> IO Progress
|
||||||
|
check getBlog input (expectedCT, expectedD, expectedI, expectedT, expectedU) =
|
||||||
|
getBlog >>= runReaderT (
|
||||||
|
sequence [
|
||||||
|
assertEqual "card types" expectedCT <$> cardType input
|
||||||
|
, assertEqual "descriptions" expectedD <$> description input
|
||||||
|
, assertEqual "images" expectedI <$> image input
|
||||||
|
, assertEqual "titles" expectedT <$> title input
|
||||||
|
, assertEqual "urls" expectedU <$> urlPath input
|
||||||
|
] >>= liftIO . assertAll
|
||||||
|
)
|
||||||
|
|
||||||
|
articleCard :: Test
|
||||||
|
articleCard = tag "article" . testGroup "Article cards" $ simpleTest <$> [
|
||||||
|
("simple article components", check Blog.simple Article.simple (
|
||||||
|
"article"
|
||||||
|
, "It's a test"
|
||||||
|
, Just "test.png"
|
||||||
|
, "Some test"
|
||||||
|
, "articles/test.html"
|
||||||
|
))
|
||||||
|
, ("article components without description", check Blog.simple Article.noDescription (
|
||||||
|
"article"
|
||||||
|
, "A new article on The Test Blog"
|
||||||
|
, Just "test.png"
|
||||||
|
, "Some test"
|
||||||
|
, "articles/test.html"
|
||||||
|
))
|
||||||
|
, ("article components without image", check Blog.simple Article.noImage (
|
||||||
|
"article"
|
||||||
|
, "It's a test"
|
||||||
|
, Nothing
|
||||||
|
, "Some test"
|
||||||
|
, "articles/test.html"
|
||||||
|
))
|
||||||
|
]
|
||||||
|
|
||||||
|
articlesListCard :: Test
|
||||||
|
articlesListCard = tag "articlesList" . testGroup "Articles list cards" $ simpleTest <$> [
|
||||||
|
("short untagged page component", ArticlesList.shortMain >>= (flip (check Blog.simple) (
|
||||||
|
"website"
|
||||||
|
, "Latest articles"
|
||||||
|
, Nothing
|
||||||
|
, "The Test Blog"
|
||||||
|
, "index.html"
|
||||||
|
)))
|
||||||
|
, ("long untagged page component", ArticlesList.longMain >>= (flip (check Blog.simple) (
|
||||||
|
"website"
|
||||||
|
, "All articles"
|
||||||
|
, Nothing
|
||||||
|
, "The Test Blog"
|
||||||
|
, "all.html"
|
||||||
|
)))
|
||||||
|
, ("short tagged page component", ArticlesList.shortTesting >>= (flip (check Blog.simple) (
|
||||||
|
"website"
|
||||||
|
, "Latest articles tagged testing"
|
||||||
|
, Nothing
|
||||||
|
, "The Test Blog - testing"
|
||||||
|
, "testing/index.html"
|
||||||
|
)))
|
||||||
|
, ("long tagged page component", ArticlesList.longTesting >>= (flip (check Blog.simple) (
|
||||||
|
"website"
|
||||||
|
, "All articles tagged testing"
|
||||||
|
, Nothing
|
||||||
|
, "The Test Blog - testing"
|
||||||
|
, "testing/all.html"
|
||||||
|
)))
|
||||||
|
]
|
||||||
|
|
||||||
|
test :: Test
|
||||||
|
test = tag "component" $ testGroup "Cards components" [articleCard, articlesListCard]
|
49
test/XML/Card/Output.hs
Normal file
49
test/XML/Card/Output.hs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
module XML.Card.Output (
|
||||||
|
test
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Blog (Blog(..), URL(..))
|
||||||
|
import Control.Monad.IO.Class (MonadIO(..))
|
||||||
|
import Control.Monad.Reader (asks, runReaderT)
|
||||||
|
import qualified Data.Text.Lazy.IO as Lazy (readFile)
|
||||||
|
import Distribution.TestSuite
|
||||||
|
import DOM.Card (HasCard(..), make)
|
||||||
|
import Lucid (renderTextT)
|
||||||
|
import Mock.Blog as Blog (noCards, simple)
|
||||||
|
import Mock.Article as Article (noDescription, noImage, simple)
|
||||||
|
import Mock.ArticlesList as ArticlesList (
|
||||||
|
longMain, longTesting, shortMain, shortTesting
|
||||||
|
)
|
||||||
|
import Pretty ((.$))
|
||||||
|
import System.FilePath ((</>))
|
||||||
|
import Utils (assertAll, assertEqual, simpleTest, tag, testDataPath)
|
||||||
|
|
||||||
|
check :: HasCard a => IO Blog -> a -> FilePath -> IO Progress
|
||||||
|
check getBlog input expectedFile =
|
||||||
|
getBlog >>= runReaderT (do
|
||||||
|
actual <- renderTextT $ maybe (return ()) (DOM.Card.make input) =<< (asks $urls.$cards)
|
||||||
|
expected <- liftIO . Lazy.readFile $ testDataPath "XML/Card/Output" </> expectedFile
|
||||||
|
liftIO $ assertAll [
|
||||||
|
assertEqual "card HTML output" expected actual
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
articleCard :: Test
|
||||||
|
articleCard = tag "article" . testGroup "Article cards" $ simpleTest <$> [
|
||||||
|
("simple article output", check Blog.simple Article.simple "simple.html")
|
||||||
|
, ("article output without description", check Blog.simple Article.noDescription "noDescription.html")
|
||||||
|
, ("article output without image", check Blog.simple Article.noImage "noImage.html")
|
||||||
|
, ("no card article output", check Blog.noCards Article.simple "/dev/null")
|
||||||
|
]
|
||||||
|
|
||||||
|
articlesListCard :: Test
|
||||||
|
articlesListCard = tag "article" . testGroup "Article cards" $ simpleTest <$> [
|
||||||
|
("short untagged page output", ArticlesList.shortMain >>= flip (check Blog.simple) "shortMain.html")
|
||||||
|
, ("long untagged page output", ArticlesList.longMain >>= flip (check Blog.simple) "longMain.html")
|
||||||
|
, ("short tagged page output", ArticlesList.shortTesting >>= flip (check Blog.simple) "shortTesting.html")
|
||||||
|
, ("long tagged page output", ArticlesList.longTesting >>= flip (check Blog.simple) "longTesting.html")
|
||||||
|
, ("no card articlesList output", ArticlesList.shortMain >>= flip (check Blog.noCards) "/dev/null")
|
||||||
|
]
|
||||||
|
|
||||||
|
test :: Test
|
||||||
|
test = tag "output" $ testGroup "Cards outputs" [articleCard, articlesListCard]
|
1
test/XML/Card/Output/longMain.html
Normal file
1
test/XML/Card/Output/longMain.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<meta property="og:url" content="https://test.net/all.html"><meta property="og:type" content="website"><meta property="og:title" content="The Test Blog"><meta property="og:description" content="All articles"><meta property="og:site_name" content="The Test Blog">
|
1
test/XML/Card/Output/longTesting.html
Normal file
1
test/XML/Card/Output/longTesting.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<meta property="og:url" content="https://test.net/testing/all.html"><meta property="og:type" content="website"><meta property="og:title" content="The Test Blog - testing"><meta property="og:description" content="All articles tagged testing"><meta property="og:site_name" content="The Test Blog">
|
1
test/XML/Card/Output/noDescription.html
Normal file
1
test/XML/Card/Output/noDescription.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<meta property="og:url" content="https://test.net/articles/test.html"><meta property="og:type" content="article"><meta property="og:title" content="Some test"><meta property="og:description" content="A new article on The Test Blog"><meta property="og:image" content="https://test.net/test.png"><meta property="og:site_name" content="The Test Blog">
|
1
test/XML/Card/Output/noImage.html
Normal file
1
test/XML/Card/Output/noImage.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<meta property="og:url" content="https://test.net/articles/test.html"><meta property="og:type" content="article"><meta property="og:title" content="Some test"><meta property="og:description" content="It's a test"><meta property="og:site_name" content="The Test Blog">
|
1
test/XML/Card/Output/shortMain.html
Normal file
1
test/XML/Card/Output/shortMain.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<meta property="og:url" content="https://test.net/index.html"><meta property="og:type" content="website"><meta property="og:title" content="The Test Blog"><meta property="og:description" content="Latest articles"><meta property="og:site_name" content="The Test Blog">
|
1
test/XML/Card/Output/shortTesting.html
Normal file
1
test/XML/Card/Output/shortTesting.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<meta property="og:url" content="https://test.net/testing/index.html"><meta property="og:type" content="website"><meta property="og:title" content="The Test Blog - testing"><meta property="og:description" content="Latest articles tagged testing"><meta property="og:site_name" content="The Test Blog">
|
1
test/XML/Card/Output/simple.html
Normal file
1
test/XML/Card/Output/simple.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<meta property="og:url" content="https://test.net/articles/test.html"><meta property="og:type" content="article"><meta property="og:title" content="Some test"><meta property="og:description" content="It's a test"><meta property="og:image" content="https://test.net/test.png"><meta property="og:site_name" content="The Test Blog">
|
Loading…
Reference in a new issue