Compare commits

..

1 commit

Author SHA1 Message Date
Chris Josten f405f99729 WIP 2021-11-18 19:28:24 +01:00
33 changed files with 545 additions and 429 deletions

1
.gitignore vendored
View file

@ -16,4 +16,3 @@ mijnblog-test-*
/articles/
/pages/
/projects/
/result

View file

@ -14,14 +14,6 @@ function get_default_code_class(meta)
end
end
function make_image_url_absolute (img)
if img.src:sub(1,1) == '/' then
img.src = os.getenv 'WEBROOT' .. img.src
end
return img
end
return {{Meta = get_default_code_class},
{Code = add_default_code_class},
{CodeBlock = add_default_code_class},
{Image = make_image_url_absolute}}
{CodeBlock = add_default_code_class}}

View file

@ -5,12 +5,16 @@
"copyright": "Copyright © 2019, Chris Josten",
"dependencies": {
"dyaml": "~>0.8.0",
"htmld": "~>0.3.7",
"vibe-d": "~>0.9.0"
},
"description": "A blog based on Markdown and JSON",
"license": "AGPLv3",
"mainSourceFile": "source/nl/netsoj/chris/blog/main.d",
"name": "mijnblog",
"targetType": "executable",
"stringImportPaths": ["views", "translations"],
"versions": ["DeimosOpenSSL_3_0"]
}
"stringImportPaths": [
"views",
"translations"
],
"targetType": "executable"
}

View file

@ -3,21 +3,21 @@
"versions": {
"botan": "1.12.19",
"botan-math": "1.0.3",
"diet-ng": "1.8.1",
"dyaml": "0.8.6",
"eventcore": "0.9.26",
"diet-ng": "1.7.5",
"dyaml": "0.8.3",
"eventcore": "0.9.13",
"fswatch": "0.5.0",
"htmld": "0.3.7",
"libasync": "0.8.6",
"libdominator": "1.1.7",
"libevent": "2.0.2+2.0.16",
"memutils": "1.0.9",
"memutils": "1.0.4",
"mir-linux-kernel": "1.0.1",
"openssl": "3.3.3",
"openssl-static": "1.0.2+3.0.8",
"openssl": "1.1.6+1.0.1g",
"stdx-allocator": "2.77.5",
"taggedalgebraic": "0.11.22",
"taggedalgebraic": "0.11.19",
"tinyendian": "0.2.0",
"vibe-container": "1.0.1",
"vibe-core": "2.5.1",
"vibe-d": "0.9.7"
"vibe-core": "1.13.0",
"vibe-d": "0.9.3"
}
}

View file

@ -51,61 +51,49 @@
fetch = {
type = "git";
url = "https://github.com/etcimon/memutils.git";
rev = "v1.0.9";
sha256 = "08j9grn3l6gr275m392l0dfsx5ma6hs08gjfhh48hpy3v0bvb0ny";
rev = "v1.0.4";
sha256 = "1m65iy03yl5km61ijk5ysapxc5mks9b15hmaqxpn111981qrqx9z";
fetchSubmodules = false;
date = "2023-03-02T15:19:28-05:00";
date = "2020-02-02T20:53:12-05:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/glnf0ljd84mv6gzc9wgs1qq0vqqxvmzh-memutils";
path = "/nix/store/3pmdm9szxsqfvi2dv8b58vk7856g3gd8-memutils";
};
} {
fetch = {
type = "git";
url = "https://github.com/s-ludwig/taggedalgebraic.git";
rev = "v0.11.22";
sha256 = "02iy90nwy0zzy25hwdqbcgd0w0lwzramcvi3pgyljhq0w0vl5hkq";
rev = "v0.11.19";
sha256 = "1mb4l9hhkzhwwj2v3m9l4g59q66msy15ky762wk5dv11viyfwrqb";
fetchSubmodules = false;
date = "2021-05-20T21:00:02+02:00";
date = "2021-01-13T16:58:20+01:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/p8id0qb13j8pjdczflj2x35w6v63q4cx-taggedalgebraic";
};
} {
fetch = {
type = "git";
url = "https://github.com/vibe-d/vibe-container.git";
rev = "v1.0.1";
sha256 = "0miynjmfzz340z3qw9mclaj6cyirzrpk528idr5yfmvl3jsi6wh8";
fetchSubmodules = false;
date = "2023-11-27T09:12:47+01:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/2vnn20q8k335h5k078yk4lrxkp4y63pd-vibe-container";
};
} {
fetch = {
type = "git";
url = "https://github.com/vibe-d/vibe.d.git";
rev = "v0.9.7";
sha256 = "1q4yvcaf36lmf29izg01x888v40snpwiph310nrlpr4gyhd834av";
fetchSubmodules = false;
date = "2023-08-29T13:24:09+02:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/7fq089i7zib7m9hxyl75mlfy264d9gnx-vibe.d";
path = "/nix/store/qlw3bv0xqd0dl01hd9lcyk9cx7v9qhvi-taggedalgebraic";
};
} {
fetch = {
type = "git";
url = "https://github.com/vibe-d/vibe-core.git";
rev = "v2.5.1";
sha256 = "1g66vyn9hivy8rmsdl1xg1vz41csq5k91c04yvp4wqbil9cwgqdr";
rev = "v1.13.0";
sha256 = "1zbx861dwmkp14pbzr4qyq71xsnlksx248x0a1q6afjmvvxiys8w";
fetchSubmodules = false;
date = "2023-11-24T17:25:20+01:00";
date = "2021-01-15T21:35:13+01:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/iddq1amvzic8fn4xa2317ykmkrkdr51y-vibe-core";
path = "/nix/store/zngy3z8hmdgjg8wl31d2qy194mhz0i09-vibe-core";
};
} {
fetch = {
type = "git";
url = "https://github.com/vibe-d/vibe.d.git";
rev = "v0.9.3";
sha256 = "10a5njn2nq1z6gknmkg6m6wrbndzj19mpg5860ky6v12b9ks76z3";
fetchSubmodules = false;
date = "2021-01-29T11:37:00+01:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/8r3h7npkhj9wq9lg64i3fih19hibpmg5-vibe.d";
};
} {
fetch = {
@ -135,13 +123,13 @@
fetch = {
type = "git";
url = "https://github.com/rejectedsoftware/diet-ng.git";
rev = "v1.8.1";
sha256 = "11hrbvsxhipvcz9m1qlq92iaw0h35pzdy5rf7z8c4vx3hwjrnhni";
rev = "v1.7.5";
sha256 = "1cymg3v924d499sbjagjf5dqv1pj196c55a2282knxgq18a3gynf";
fetchSubmodules = false;
date = "2022-04-22T11:38:43+02:00";
date = "2021-02-07T14:20:30+01:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/xziliy9ah3s3ah8mslnxsw47n4w6wkca-diet-ng";
path = "/nix/store/5r6v9f7y9kwkb90ihgimqqryf6ls3kqq-diet-ng";
};
} {
fetch = {
@ -159,37 +147,37 @@
fetch = {
type = "git";
url = "https://github.com/D-Programming-Deimos/openssl.git";
rev = "v3.3.3";
sha256 = "1634j4psp3qwgwhk2sa3jj6gvwv3i96hpg2wrdy9ihjhabnszn0f";
rev = "v1.1.6+1.0.1g";
sha256 = "0ramqjyq4v7xpqwf4nf4ddmsg6yk2fbzn2d1yj4b6dla3q5lv9i3";
fetchSubmodules = false;
date = "2023-09-14T12:05:32+00:00";
date = "2017-11-05T20:15:26+01:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/i3pgh154r80xvh4vsnl6srg6khmk5dg7-openssl";
path = "/nix/store/jbfb5in6gzqs2byn1hdc1625yh8sx6j2-openssl";
};
} {
fetch = {
type = "git";
url = "https://github.com/vibe-d/eventcore.git";
rev = "v0.9.26";
sha256 = "13bjs5v5l1387vi2ss4gvqlslhq33v9sn4pg7nis4r0wdw0zmlk1";
rev = "v0.9.13";
sha256 = "0frxifhjwzyi35cv4pvv8k11a966fg76gqxdpmx9bsqbx750lrvz";
fetchSubmodules = false;
date = "2023-09-16T09:46:45+02:00";
date = "2021-01-12T19:20:28+01:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/zy7ydasg4fwnim2m1mi4gzdg9l087wpb-eventcore";
path = "/nix/store/yikgbvj61xhk6r3x8pc8lb7sp5smcn5q-eventcore";
};
} {
fetch = {
type = "git";
url = "https://github.com/kiith-sa/D-YAML.git";
rev = "v0.8.6";
sha256 = "1bvidcxp1n65r4wmiqakyl8vjvhqh3gln9wbsmbxrx9mf6k0zv8h";
rev = "v0.8.3";
sha256 = "13wy304xjbwkpgg7ilql1lkxkm83s87jm59ffnrg26slp7cx149q";
fetchSubmodules = false;
date = "2022-05-15T16:18:02-03:00";
date = "2020-09-19T23:46:57+02:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/zd0yayd1j11809j95sjs4gdqcngvkhbg-D-YAML";
path = "/nix/store/3i8i56lkmw2xq3lxr5h66v909waq2mqg-D-YAML";
};
} {
fetch = {
@ -203,16 +191,4 @@
leaveDotGit = false;
path = "/nix/store/6l82qf5nav5pkbvnrhcs5v9vwj5xycq3-libasync";
};
} {
fetch = {
type = "git";
url = "https://github.com/bildhuus/deimos-openssl-static.git";
rev = "v1.0.2+3.0.8";
sha256 = "00wllmfrjpq5ln3zs9qcqf4kq2i6bqbjxwj0jnxwwz125kfm55sy";
fetchSubmodules = false;
date = "2023-02-24T13:23:37+01:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/g9shdkm450yg8c15rvqms8v09vql8z0l-deimos-openssl-static";
};
} ]

View file

@ -6,11 +6,7 @@ mkDubDerivation {
src = ./.;
dubJSON = ./dub.json;
selections = ./dub.selections.nix;
version = "0.0.3";
version = "0.0.1";
buildInputs = [ pkgs.openssl ];
propagatedBuildInputs = [ pkgs.nix-prefetch-git ];
extraDubFlags = "--override-config openssl/library-manual-version";
preBuild = ''
export DC=${pkgs.dmd}/bin/dmd
'';
}

View file

@ -1,9 +1,7 @@
{ pkgs ? import <nixpkgs> {},
stdenv ? pkgs.stdenv,
lib ? pkgs.lib,
dtools ? pkgs.dtools or pkgs.rdmd,
rdmd ? pkgs.rdmd,
dmd ? pkgs.dmd,
dcompiler ? dmd,
dub ? pkgs.dub }:
with stdenv;
@ -22,7 +20,7 @@ let
fromDub = dubDep: mkDerivation rec {
name = "${src.name}-${version}";
version = rev-to-version dubDep.fetch.rev;
nativeBuildInputs = [ dcompiler dtools dub ];
nativeBuildInputs = [ rdmd dmd dub ];
src = dep2src dubDep;
buildPhase = ''
@ -48,18 +46,10 @@ let
targetOf = package: "${package.targetPath or "."}/${package.targetName or package.name}";
# Remove reference to build tools and library sources
disallowedReferences = deps: [ dcompiler dtools dub ] ++ builtins.map dep2src deps;
disallowedReferences = deps: [ dmd rdmd dub ] ++ builtins.map dep2src deps;
removeExpr = refs: ''remove-references-to ${lib.concatMapStrings (ref: " -t ${ref}") refs}'';
# Like split, but only keep the matches
matches = regex: str: builtins.filter lib.isList (builtins.split regex str);
# Very primitive parsing of SDL files, but suffices for name, description, homepage, etc.
importSDL = path: builtins.foldl' (a: l: a // {"${lib.elemAt l 1}"=lib.elemAt l 2;}) {} (matches "(^|\n)([a-z]+) \"([^\"]+)\"" (builtins.readFile path));
importPackage = sdl: json: if builtins.pathExists sdl then importSDL sdl else lib.importJSON json;
in {
inherit fromDub;
@ -67,23 +57,20 @@ in {
src,
nativeBuildInputs ? [],
dubJSON ? src + "/dub.json",
dubSDL ? src + "/dub.sdl",
buildType ? "release",
extraDubFlags ? "",
selections ? src + "/dub.selections.nix",
deps ? import selections,
package ? importPackage dubSDL dubJSON,
passthru ? {},
package ? lib.importJSON dubJSON,
...
} @ attrs: stdenv.mkDerivation ((removeAttrs attrs ["package" "deps" "selections" "dubJSON" "dubSDL"]) // {
} @ attrs: stdenv.mkDerivation (attrs // {
pname = package.name;
nativeBuildInputs = [ dcompiler dtools dub pkgs.removeReferencesTo ] ++ nativeBuildInputs;
nativeBuildInputs = [ rdmd dmd dub pkgs.removeReferencesTo ] ++ nativeBuildInputs;
disallowedReferences = disallowedReferences deps;
passthru = passthru // {
inherit dub dcompiler dtools pkgs;
inherit dub dmd rdmd pkgs;
};
src = lib.cleanSourceWith {
@ -100,7 +87,7 @@ in {
export HOME=$PWD
${lib.concatMapStringsSep "\n" dub-add-local deps}
dub build -b ${buildType} --combined --skip-registry=all ${extraDubFlags}
dub build -b release --combined --skip-registry=all
runHook postBuild
'';
@ -110,7 +97,7 @@ in {
export HOME=$PWD
${lib.concatMapStringsSep "\n" dub-add-local deps}
dub test --combined --skip-registry=all ${extraDubFlags}
dub test --combined --skip-registry=all
runHook postCheck
'';
@ -126,8 +113,6 @@ in {
meta = lib.optionalAttrs (package ? description) {
description = package.description;
} // lib.optionalAttrs (package ? homepage) {
homepage = package.homepage;
} // attrs.meta or {};
} // lib.optionalAttrs (!(attrs ? version)) {
# Use name from dub.json, unless pname and version are specified

View file

@ -1,3 +1,32 @@
@media (max-width: 850px) {
body {
flex-wrap: wrap;
}
body > main {
box-sizing: border-box;
min-width: 100%;
}
body > section.header-navigation, body > footer {
width: 100%;
flex-grow: 1 !important;
}
.hide-small {
display: none;
}
.hide-big {
display: block !important;
}
}
.hide-big {
display: none;
}
:root {
font-family: "sans-serif";
--colour-bg: #f0f0f0;
@ -33,54 +62,48 @@ body {
background-color: #f0f0f0;
background-color: var(--colour-bg);
margin: 0;
display: grid;
grid-template-columns: 2em 200px 600px min-content;
grid-column-gap: 2em;
grid-template-rows: 2em min-content min-content 1fr;
grid-template-areas:
". . main . "
". header main sidebar"
". menu main sidebar"
". footer main sidebar";
display: flex;
justify-content: center;
min-height: 100vh;
color: #000000;
color: var(--colour-fg);
}
body > nav {
grid-area: menu;
body > section.header-navigation {
flex: 0 0 200px;
padding: 2em;
}
@media (pointer: coarse) {
body > section.header-navigation li {
padding: 0.25em 0;
}
}
body > footer {
grid-area: footer;
margin-top: 0;
flex: 0 0 200px;
padding: 2em;
border: none;
}
body > header {
grid-area: header;
body > section.header-navigation > header {
text-align: center;
margin-bottom: 0;
}
body > header > img {
body > section.header-navigation > header > img {
width: 100%;
max-width: 160px;
}
body > section.main-sidebar {
grid-area: sidebar;
}
body > main {
grid-area: main;
flex: 1 1;
padding: 2em;
/* width: 600px; */
border-left: #7f0602 dotted 1px;
border-right: #7f0602 dotted 1px;
border-left: var(--colour-fg-highlight) dotted 1px;
border-right: var(--colour-fg-highlight) dotted 1px;
max-width: calc(600px);
max-width: 600px;
background-color: #ddd;
background-color: var(--colour-bg-main);
}
@ -251,65 +274,11 @@ a {
text-decoration: underline dotted;
}
a:hover, a:focus {
a:hover {
color: var(--colour-fg);
text-decoration: underline solid;
}
@media (max-width: calc(800px + 8em)) {
body {
grid-template-columns: 100%;
grid-column-gap: 2em;
grid-template-rows: repeat(4, min-content);
grid-template-areas:
"header"
"menu"
"main"
"footer";
}
body > .main-sidebar {
display: none;
}
body > main {
box-sizing: border-box;
min-width: 100%;
border: none;
border-top: #7f0602 dotted 1px;
border-bottom: #7f0602 dotted 1px;
border-top: var(--colour-fg-highlight) dotted 1px;
border-bottom: var(--colour-fg-highlight) dotted 1px;
}
body > nav, body > footer {
width: calc(100% - 4em);
padding: 2em;
}
}
@media (pointer: coarse) {
body > nav li a {
display: inline-block;
padding: 1em 2em;
width: calc(100% - 4em);
}
body > nav > ul {
width: 100%;
}
body > nav li:not(:last-child) {
border-bottom: 1px dotted #000000;
border-bottom: 1px dotted var(--colour-fg);
}
body > nav {
padding: 1em 0em;
width: 100%;
}
}
@media print {
body {
background-color: initial;

View file

@ -2,17 +2,6 @@ body {
background-image: url(old/chip.jpg);
cursor: url(old/cursor.gif), auto;
justify-content: flex-start;
grid-column-gap: 0em;
grid-template-columns: 200px 600px;
grid-template-rows: min-content min-content 1fr;
grid-template-areas:
"header main"
"menu main"
"footer main";
}
.main-sidebar {
display: none;
}
header {
@ -27,15 +16,14 @@ footer {
a:hover {
cursor: url(old/cursor-over.gif), auto;
color: red;
}
body > nav, body > header, body > footer {
.header-navigation {
background-color: cyan;
color: black;
}
body > nav ul {
.header-navigation> nav ul {
padding-left: 2em;
list-style-image: url(old/bullet.gif);
}

View file

@ -1,11 +1,15 @@
/**
* Implements and holds caches for several pages.
*/
module nl.netsoj.chris.blog.cache;
import std.experimental.logger;
import std.traits;
import article;
import page;
import project;
import nl.netsoj.chris.blog.model.article;
import nl.netsoj.chris.blog.model.page;
import nl.netsoj.chris.blog.model.project;
@safe:
/**
* Default ordering and list with pointers to ordered articles.
@ -22,7 +26,7 @@ GenericCache!(Project, "a.title < b.title") projects;
* again if needed.
*/
struct GenericCache(T, string sortOrder)
if (isImplicitlyConvertible!(T, Page)) {
if (is(T : Page)) {
public:
void addItem(T item) {

View file

@ -1,10 +1,14 @@
module nl.netsoj.chris.blog.constants;
/**
* Constants which are passed to templates while rendering.
*/
struct Constants {
class Constants {
public static immutable string SITE_NAME = "Chris Josten's site";
public static immutable string SITE_HOST = "chris.netsoj.nl";
public static immutable string SITE_URL = "https://chris.netsoj.nl";
public static immutable string SITE_AUTHOR = "Chris Josten";
debug {
public static immutable string SITE_URL = "https://kortstondig.chris.netsoj.nl";
} else {
public static immutable string SITE_URL = "https://chris.netsoj.nl";
}
public static immutable string COPYRIGHT = "&copy; Chris Josten, 2020";
}

View file

@ -1,14 +1,15 @@
import std.algorithm : map, maxElement;
import std.format : format;
import std.string: join;
module nl.netsoj.chris.blog.interfaces.http;
import std.experimental.logger;
import vibe.d;
import cache;
import constants;
import article;
import page;
import project;
import nl.netsoj.chris.blog.interfaces.indieauth;
import nl.netsoj.chris.blog.model.article;
import nl.netsoj.chris.blog.model.page;
import nl.netsoj.chris.blog.model.project;
import nl.netsoj.chris.blog.cache;
import nl.netsoj.chris.blog.constants;
/**
* Output types for the content.
@ -63,7 +64,7 @@ struct TranslateContext {
mixin translationModule!"mijnblog";
static string determineLanguage(scope HTTPServerRequest req) {
if ("lang" !in req.query) return req.determineLanguageByHeader(languages); // default behaviour using "Accept-Language" header
return determineLanguageByHeader(req.query.get("lang", "en_GB"), languages);
return req.query.get("lang", "en_GB");
}
}
@ -74,29 +75,41 @@ struct TranslateContext {
* templateName = The name of the template to render.
*/
string singleResponseMixin(string arrayName, string templateName) {
return `string slug = req.params["slug"];
OutputType outputType = getOutputType(slug);
return `string slug = req.params["slug"];
OutputType outputType = getOutputType(slug);
enforceHTTP(slug in ` ~ arrayName ~ `, HTTPStatus.notFound, "Page not found");
auto content = ` ~ arrayName ~ `[slug];
switch(outputType) with (OutputType) {
case MARKDOWN:
res.writeBody(content.contentSource, MIME_MARKDOWN);
break;
default:
case HTML:
res.render!("` ~ templateName ~ `", content);
break;
}`;
}
enforceHTTP(slug in ` ~ arrayName ~ `, HTTPStatus.notFound, "Page not found");
auto content = ` ~ arrayName ~ `[slug];
switch(outputType) with (OutputType) {
case MARKDOWN:
res.writeBody(content.contentSource, MIME_MARKDOWN);
break;
default:
case HTML:
render!("` ~ templateName ~ `", content);
break;
}`;
}
@translationContext!TranslateContext
class MijnBlog {
public:
this() {
m_indieAuth = new IndieAuth();
}
/**
* IndieAuth subinterface
*/
@safe
@path("/indieweb")
@property IndieAuth indieweb() { return m_indieAuth; };
/**
* Generates response for /posts/:slug and /palen/:slug.
*/
* Generates response for /posts/:slug and /palen/:slug.
*/
@path("/posts/:slug")
void getArticleSingle(string _slug, HTTPServerRequest req, HTTPServerResponse res) {
//getSingle!(Article, "pages/article.dt")(articles, req, res);
@ -145,62 +158,13 @@ public:
addCachingHeader(res);
// If no slug is supplied, it will be adjusted to "index"
req.params.addField("slug", "index");
res.headers.addField("Link", "<" ~ Constants.SITE_URL ~ "/indieweb/auth>; rel=\"authorization_endpoint\"");
res.headers.addField("Link", "<" ~ Constants.SITE_URL ~ "/indieweb/token>; rel=\"token_endpoint\"");
res.headers.addField("Link", "<" ~ Constants.SITE_URL ~ "/indieweb/micropub>; rel=\"micropub\"");
mixin(singleResponseMixin("pages", "pages/page.dt"));
}
@path("/feeds/posts.atom")
void getPostFeed(HTTPServerRequest req, HTTPServerResponse res) {
Article[] articleList = articles.sortedList;
DateTime lastUpdated = articleList.length > 0
? articleList.map!"a.firstPublished()".maxElement
: DateTime(2019, 06, 30);
string response = q"EOS
<?xml version="1.0" encoding="utf-8" ?>
<feed xmlns="http://www.w3.org/2005/Atom">
<updated>%s</updated>
<title>%s</title>
<icon>%s</icon>
<link href="%s/feeds/posts.atom" rel="self" />
<link href="%s" />
<id>urn:uuid:036f1087-7fcd-466d-866d-78a0c60038cd</id>
<author><name>%s</name></author>
%s
</feed>
EOS"
.format(
lastUpdated.toISOExtString() ~ "Z",
trWeb("template.feed.title").format(trWeb("template.feed.posts.title")),
Constants.SITE_URL ~ "/static/img/logo.png",
Constants.SITE_URL,
Constants.SITE_URL,
Constants.SITE_AUTHOR,
articleList.map!((article) {
return q"EOS
<entry>
<title>%s</title>
<link href="%s" />
<id>%s</id>
<published>%s</published>
<updated>%s</updated>
<summary>%s</summary>
<content type="html">%s</content>
</entry>
EOS"
.format(article.title,
Constants.SITE_URL ~ "/posts/" ~ article.slug,
"tag:" ~ Constants.SITE_HOST ~ ",2024:blog:posts:" ~ article.slug,
article.firstPublished().toISOExtString() ~ "Z",
article.updated().toISOExtString() ~ "Z",
article.excerpt(),
htmlEscape(article.content())
);
}).join()
);
res.writeBody(response, "application/atom+xml");
}
private:
IndieAuth m_indieAuth;
}
/**
@ -208,7 +172,9 @@ EOS"
*/
@safe
void errorPage(HTTPServerRequest req, HTTPServerResponse res, HTTPServerErrorInfo error) {
render!("pages/error.dt", error)(res);
//render!("pages/error.dt", error)(res);
import std.conv;
res.writeBody(text("Error ", error.code, ": ", error.message, "\n\n", error.debugMessage), "text/plain");
}
@trusted
@ -230,4 +196,7 @@ void startHTTPServer() {
router.registerWebInterface(new MijnBlog);
listenHTTP(settings, router);
foreach(route; router.getAllRoutes()) {
infof("Path: %s", route);
}
}

View file

@ -0,0 +1,92 @@
module nl.netsoj.chris.blog.interfaces.indieauth;
import nl.netsoj.chris.blog.interfaces.http;
import nl.netsoj.chris.blog.interfaces.micropub;
import nl.netsoj.chris.blog.microformats.parser;
import mfd = nl.netsoj.chris.blog.microformats.definitions;
import vibe.d;
struct App {
string logo;;
string name;
string url;
string clientId;
string redirectUri;
bool richInfo = false;
}
/**
* An implementation of https://www.w3.org/TR/indieauth/
*/
@translationContext!TranslateContext
class IndieAuth {
public:
this() {
m_microPub = new MicroPub();
}
@queryParam("response_type", "response_type")
@queryParam("me", "me")
@queryParam("client_id", "client_id")
@queryParam("redirect_uri", "redirect_uri")
@queryParam("state", "state")
void getAuth(HTTPServerRequest req, HTTPServerResponse res,
string response_type, string me, string client_id, string redirect_uri, string state) {
enforceHTTP(response_type == "code", HTTPStatus.badRequest);
URL source = URL(client_id);
HTTPClientResponse response = requestHTTP(source);
mfd.App[] parsedApps = parsePage!(mfd.App)(response.bodyReader.readAllUTF8, source);
App app;
app.name = client_id;
app.richInfo = false;
if (parsedApps.length > 0) {
auto parsedApp = parsedApps[0];
app.logo = parsedApp.logo;
app.name = parsedApp.name;
app.richInfo = true;
}
app.clientId = client_id;
URL redirectUrl = URL(redirect_uri);
redirectUrl.queryString = redirectUrl.queryString ~ "%scode=%s&state=%s".format(
(redirectUrl.queryString.length == 0 ? "" : "&"),
"123456", state);
app.redirectUri = redirectUrl.toString();
render!("pages/indieauth.dt", app);
}
@safe
@path("/")
void getIndex(HTTPServerResponse res) {
res.writeBody("IndieAuth root", "text/plain");
}
@safe
void postToken(HTTPServerRequest req, HTTPServerResponse res,
string grant_type, string code, string client_id, string redirect_uri, string me) {
struct OkResponse {
string access_token;
string token_type = "Bearer";
string me;
string scope_;
}
OkResponse response;
response.me = me;
response.scope_ = "foo";
response.access_token = "baz";
res.writeJsonBody(response, HTTPStatus.ok);
}
@safe @property
MicroPub micropub() { return m_microPub; };
private:
MicroPub m_microPub;
}

View file

@ -0,0 +1,14 @@
module nl.netsoj.chris.blog.interfaces.micropub;
import vibe.d;
import nl.netsoj.chris.blog.interfaces.http;
@translationContext!TranslateContext
class MicroPub {
@path("/")
void getIndex(HTTPServerRequest req, HTTPServerResponse res) {
res.writeBody("MicroPub endpoint", "text/plain");
}
}

View file

@ -1,23 +1,14 @@
module nl.netsoj.chris.blog.main;
import std.experimental.logger;
import vibe.d;
import article;
import page;
import project;
import cache;
import http;
import watcher;
void watchTask(T, C)(C *cache, string directory) @safe nothrow {
do {
try {
initPages!T(cache, directory);
} catch(Exception e) {
logWarn("Error while watching pages: " ~ e.msg);
}
} while(Task.getThis().running);
}
import nl.netsoj.chris.blog.interfaces.http;
import nl.netsoj.chris.blog.model.article;
import nl.netsoj.chris.blog.model.page;
import nl.netsoj.chris.blog.model.project;
import nl.netsoj.chris.blog.cache;
import nl.netsoj.chris.blog.watcher;
void main() {
@ -25,13 +16,13 @@ void main() {
// Start indexing pages.
runTask({
watchTask!Page(&pages, "pages");
initPages!Page(&pages, "pages");
});
runTask({
watchTask!Article(&articles, "articles");
initPages!Article(&articles, "articles");
});
runTask({
watchTask!Project(&projects, "projects");
initPages!Project(&projects, "projects");
});
runApplication();
}

View file

@ -0,0 +1,12 @@
module nl.netsoj.chris.blog.microformats.definitions;
import nl.netsoj.chris.blog.microformats.parser;
struct App {
@MicroFormat(MicroFormat.Type.Url)
string logo;
@MicroFormat(MicroFormat.Type.PlainText)
string name;
string url;
}

View file

@ -0,0 +1,116 @@
/**
* Parser for microformats.
* Standards: http://microformats.org/wiki/microformats2-parsing
*/
module nl.netsoj.chris.blog.microformats.parser;
import std.exception;
import std.traits;
import vibe.inet.url;
import html;
import nl.netsoj.chris.blog.url;
class MicroFormatParseException : Exception {
mixin basicExceptionCtors;
}
struct MicroFormatProperty {
enum Type {
RootClass,
PlainText,
Url,
DateTime,
EmbeddedMarkup
};
Type type = Type.PlainText;
string getClassPrefix() pure {
final switch(type) {
case Type.RootClass:
return "h-";
case Type.PlainText:
return "p-";
case Type.Url:
return "u-";
case Type.DateTime:
return "dt-";
case Type.EmbeddedMarkup:
return "e-";
}
}
};
/**
* Parses a web page to extract to the given microformat model out of it
*/
T[] parsePage(T)(string source, URL url) {
return parsePage!T(createDocument(source), url);
}
T[] parsePage(T)(Document page, URL url)
if (isAggregateType!T) {
import std.algorithm;
import std.array;
import std.conv;
import std.range;
import std.string;
string rootClass = "h-" ~ T.stringof.toLower;
return page.querySelectorAll(".%s".format(rootClass)).map!((node){
alias PropType = MicroFormatProperty.Type;
T instance = T();
MicroFormatProperty uda;
string propertyClass;
Node propNode;
static foreach(sym; getSymbolsByUDA!(T, MicroFormatProperty)) {
uda = getUDAs!(sym, MicroFormatProperty)[0];
propertyClass = uda.getClassPrefix() ~ sym.stringof.toLower;
propNode = page.querySelector(".%s".format(propertyClass), node);
switch (uda.type) {
case PropType.PlainText:
if (propNode.firstChild && propNode.firstChild.isTextNode()) {
__traits(getMember, instance, sym.stringof) = to!string(propNode.text);
}
break;
case PropType.Url:
if (propNode.tag == "a" && propNode.hasAttr("href")) {
__traits(getMember, instance, sym.stringof) = resolveURL(url, to!string(propNode["href"])).toString;
} else if (propNode.tag == "img" && propNode.hasAttr("src")) {
__traits(getMember, instance, sym.stringof) = resolveURL(url, to!string(propNode["src"])).toString;
}
break;
default:
break;
}
}
return instance;
}).array;
}
unittest {
import std.stdio;
string page = q"eos
<div class="h-app">
<img src="/logo.png" class="u-logo">
<a href="/" class="u-url p-name">Example App</a>
</div>"
eos";
struct App {
@MicroFormatProperty(MicroFormatProperty.Type.Url)
string logo;
@MicroFormatProperty(MicroFormatProperty.Type.PlainText)
string name;
}
auto ts = parsePage!App(page, URL("https://example.com/"));
assert(ts[0] == App ("https://example.com:443/logo.png", "Example App"));
}

View file

@ -1,3 +1,5 @@
module nl.netsoj.chris.blog.model.article;
import std.file;
import std.stdio;
import std.string;
@ -7,10 +9,9 @@ import std.experimental.logger;
import dyaml;
import vibe.d;
import page;
import utils;
import nl.netsoj.chris.blog.model.page;
import nl.netsoj.chris.blog.utils;
@safe:
/**
* Represents an article on the blog
@ -35,9 +36,9 @@ class Article : Page {
// Find the first header and mark everything up to that as
if (m_excerpt is null) {
// an excerpt, used in search results.
const long seperatorIndex = cast(long) lastIndexOf(m_contentSource, "---\n");
const uint seperatorIndex = cast(uint) indexOf(m_contentSource, "---\n");
this.m_excerpt = this.m_contentSource[seperatorIndex + 4..$];
const long firstHeaderIndex = indexOf(this.m_excerpt, '#');
const uint firstHeaderIndex = cast(uint) indexOf(this.m_excerpt, '#');
if (firstHeaderIndex >= 0) {
this.m_excerpt = this.m_excerpt[0..firstHeaderIndex];
}

View file

@ -1,3 +1,5 @@
module nl.netsoj.chris.blog.model.page;
import std.exception;
import std.experimental.logger;
import std.file;
@ -7,10 +9,8 @@ import std.stdio;
import dyaml;
import vibe.vibe;
import constants;
import utils;
import nl.netsoj.chris.blog.utils;
@safe:
/**
* Exception thrown when a page has syntax errors e.g.
@ -53,11 +53,11 @@ class Page {
/**
* Creates a page from a file. This will read from the file and parse it.
*/
this(string file) @safe {
this(string file) {
this.m_name = file;
this.m_contentSource = readText(file);
// Find the seperator and split the string in two
const long seperatorIndex = lastIndexOf(m_contentSource, "\n---\n");
const uint seperatorIndex = cast(uint) lastIndexOf(m_contentSource, "---\n");
enforce!ArticleParseException(seperatorIndex >= 0);
string header = m_contentSource[0..seperatorIndex];
@ -108,12 +108,10 @@ class Page {
"-f", "markdown",
"-t", "html",
"--lua-filter", "defaultClasses.lua"];
string[string] env;
env["WEBROOT"] = Constants.SITE_URL;
if (shiftHeader != 0) args ~= "--shift-heading-level-by=" ~ to!string(shiftHeader);
ProcessPipes pandoc = pipeProcess(args, Redirect.all, env);
ProcessPipes pandoc = pipeProcess(args);
pandoc.stdin.write(source);
pandoc.stdin.writeln();
pandoc.stdin.flush();

View file

@ -1,3 +1,5 @@
module nl.netsoj.chris.blog.model.project;
import std.array;
import std.algorithm;
import std.typecons;
@ -5,11 +7,9 @@ import std.typecons;
import dyaml;
import vibe.vibe;
import page;
import utils;
import staticpaths;
@safe:
import nl.netsoj.chris.blog.model.page;
import nl.netsoj.chris.blog.staticpaths;
import nl.netsoj.chris.blog.utils;
/**
* Represents a project, like an unfinished application

View file

@ -0,0 +1 @@
module nl.netsoj.chris.blog;

View file

@ -1,3 +1,5 @@
module nl.netsoj.chris.blog.staticpaths;
/**
* Paths to static data.
*/

View file

@ -0,0 +1,27 @@
module nl.netsoj.chris.blog.url;
import std.exception;
import vibe.inet.url;
URL resolveURL(URL source, string other) {
URL otherUrl = URL(other);
if (otherUrl.schema.length > 0 || otherUrl.host.length > 0) {
return otherUrl;
}
if (otherUrl.schema.length == 0) {
enforce(source.schema.length > 0, "Source URL must have a scheme to resolve the other URL");
otherUrl.schema = source.schema;
}
if (otherUrl.host.length == 0) {
otherUrl.host = source.host;
otherUrl.port = source.port;
if (!otherUrl.path.absolute) {
otherUrl.path = source.path ~ otherUrl.path;
}
}
return otherUrl;
}

View file

@ -1,3 +1,5 @@
module nl.netsoj.chris.blog.utils;
import std.algorithm;
import std.array;
import std.conv;

View file

@ -1,47 +1,41 @@
module nl.netsoj.chris.blog.watcher;
import std.array;
import std.algorithm;
//import std.experimental.logger;
import std.experimental.logger;
import std.file;
import std.stdio;
import std.traits;
import vibe.d;
import cache;
import page;
import nl.netsoj.chris.blog.cache;
import nl.netsoj.chris.blog.model.page;
/**
* Loads pages into memory and sets up a "watcher" to watch a directory for file changes.
*/
void initPages(T, C)(C *cache, const string directory) @trusted
if (isImplicitlyConvertible!(T, Page)) {
NativePath watchingDir;
try {
watchingDir = getWorkingDirectory() ~ directory;
} catch(PathValidationException) {
logError("Cannot watch path " ~ directory);
return;
}
void initPages(T, C)(C *cache, const string directory)
if (is(T : Page)) {
bool addPage(string path) {
try {
T newPage = new T(path);
logInfo("Added %s", newPage.slug);
logf("Added %s", newPage.slug);
cache.addItem(newPage);
return true;
} catch (page.ArticleParseException e) {
logWarn("Could not parse %s: %s", path, e);
} catch (ArticleParseException e) {
logf("Could not parse %s: %s", path, e);
return false;
} catch (Exception e) {
logWarn("Other exception while parsing %s: %s", path, e);
logf("Other exception while parsing %s: %s", path, e);
return false;
}
}
// Initial scan
void scan(NativePath path, int level = 0) {
logInfo("Scanning %s", path.toString());
logf("Scanning %s", path.toString());
foreach(file; iterateDirectory(path)) {
if (file.isDirectory) {
scan(path ~ file.name, level + 1);
@ -51,11 +45,11 @@ void initPages(T, C)(C *cache, const string directory) @trusted
}
}
if (!existsFile(watchingDir)) {
createDirectory(watchingDir);
if (!existsFile(getWorkingDirectory() ~ directory)) {
createDirectory(getWorkingDirectory() ~ directory);
}
scan(watchingDir);
DirectoryWatcher watcher = watchDirectory(watchingDir, true);
scan(getWorkingDirectory() ~ directory);
DirectoryWatcher watcher = watchDirectory(getWorkingDirectory() ~ directory, true);
bool shouldStop = false;
while (!shouldStop) {
@ -63,16 +57,16 @@ void initPages(T, C)(C *cache, const string directory) @trusted
DirectoryChange[] changes;
shouldStop = !watcher.readChanges(changes);
foreach(change; changes) {
logInfo("=======[New changes]======");
logf("=======[New changes]======");
string[] changeTypes = ["added", "removed", "modified"];
logInfo("Path: %s, type: %s", change.path.toString(), changeTypes[change.type]);
logf("Path: %s, type: %s", change.path.toString(), changeTypes[change.type]);
if (endsWith(change.path.toString(), ".kate-swp")) continue;
switch (change.type) with (DirectoryChangeType){
case added:
try {
addPage(change.path.toString());
} catch(Exception e) {
logWarn("Error while updating %s: %s", change.path.toString(), e.msg);
warningf("Error while updating %s: %s", change.path.toString(), e.msg);
}
break;
case modified:
@ -80,17 +74,17 @@ void initPages(T, C)(C *cache, const string directory) @trusted
try {
newPage = new T(change.path.toString());
cache.changeItem(newPage);
} catch(page.ArticleParseException e) {
logWarn("Could not parse %s", change.path.toString());
} catch(ArticleParseException e) {
warningf("Could not parse %s", change.path.toString());
} catch (Exception e) {
logWarn("Error while updating %s: %s", change.path.toString(), e.msg);
warningf("Error while updating %s: %s", change.path.toString(), e.msg);
}
break;
case removed:
try {
cache.removeItemByName(change.path.toString());
} catch(Exception e) {
logInfo("Error while trying to remove %s: %s", T.stringof, e.msg);
logf("Error while trying to remove %s: %s", T.stringof, e.msg);
}
break;
default: break;

View file

@ -17,12 +17,6 @@ msgid "template.menu.contact"
msgstr "Contact"
msgid "template.page.copyright"
msgstr "&copy; Chris Josten, 2024. If not specified otherwise, all content on this "
msgstr "&copy; Chris Josten, 2021. If not specified otherwise, all content on this"
"website is <a rel=\"license\" href=\"https://creativecommons.org/licenses/by/4.0/\">"
"licensed under the CC-BY 4.0</a>"
msgid "template.feed.title"
msgstr "%s | Chris's website"
msgid "template.feed.posts.title"
msgstr "Posts"

View file

@ -30,11 +30,5 @@ msgid "template.menu.contact"
msgstr "Contact"
msgid "template.page.copyright"
msgstr "&copy; Chris Josten, 2024. Tenzij anders vermeld staat, valt alle inhoud op deze webstek "
msgstr "&copy; Chris Josten, 2021. Tenzij anders vermeld staat, valt alle inhoud op deze webstek"
"<a rel=\"license\" href=\"https://creativecommons.org/licenses/by/4.0/\"> onder de CC-BY 4.0</a>"
msgid "template.feed.title"
msgstr "%s | Chris z'n webstekkie"
msgid "template.feed.posts.title"
msgstr "Berichten"

View file

@ -1,28 +1,2 @@
msgid "template.page.name"
msgstr "Chris's website"
msgid "template.page.html_language"
msgstr "en-GB"
msgid "template.menu.home"
msgstr "Home"
msgid "template.menu.posts"
msgstr "Posts"
msgid "template.menu.projects"
msgstr "Projects"
msgid "template.menu.contact"
msgstr "Contact"
msgid "template.page.copyright"
msgstr "&copy; Chris Josten, 2024. If not specified otherwise, all content on this "
"website is <a rel=\"license\" href=\"https://creativecommons.org/licenses/by/4.0/\">"
"licensed under the CC-BY 4.0</a>"
msgid "template.feed.title"
msgstr "%s | Chris's website"
msgid "template.feed.posts.title"
msgstr "Posts"
msgid "page.header-bottom-text"
msgstr ""

View file

@ -12,7 +12,7 @@ block content
- if (articleList.length == 0)
p No posts found
- else
- import utils;
- import nl.netsoj.chris.blog.utils;
- foreach(article; articleList)
article
header

View file

@ -14,7 +14,7 @@ block extra_meta_data
block content
article(itemscope, itemtype="https://schema.org/BlogPosting", lang="#{content.language}")
- import utils;
- import nl.netsoj.chris.blog.utils;
header
h1.title(itemprop="headline") #{content.title}
p.subtitle

21
views/pages/indieauth.dt Normal file
View file

@ -0,0 +1,21 @@
extends parts/page
block header
title Aanmelden - Netsoj.nl
block sidebar
block content
- if (app.richInfo)
header.project-header
img.project-icon(src="#{app.logo}", alt="Icon of #{app.name}")
div
h1.title Aanmelden met #[a(href="#{app.clientId}") #{app.name}]
span.project-description #{app.clientId}
- else
header
h1.title Aanmelden met #[a(href="#{app.clientId}") #{app.name}]
p #{app.name} wil uw identiteit bevestigen.
p #[a(href="#{app.redirectUri}") Aanvaarden en aanmelden]&nbsp; #[a(href="javascript:history.back();") Weigeren]

View file

@ -1,2 +1,2 @@
p
small !{trWeb("template.page.copyright")}
small& template.page.copyright

View file

@ -1,12 +1,11 @@
doctype html
html(prefix="og: http://ogp.me/ns#")
- import constants;
- import nl.netsoj.chris.blog.constants;
- import vibe.d;
head
//- Kick off loading the css as fast as possible
meta(name="viewport", content="width=device-width; initial-scale=1")
link(rel="stylesheet", href="/static/style/base.css")
link(rel="alternate", href="/feeds/posts.atom", type="application/atom+xml", title=trWeb("template.feed.title").format(trWeb("template.feed.posts.title")))
- string page_image = "/static/img/logo.png";
- string page_title;
@ -44,25 +43,23 @@ html(prefix="og: http://ogp.me/ns#")
li
a(href="#{link}") #{trWeb(text)}
header
img.logo(src="/static/img/logo.png", alt="The logo of the website: the letter C drawn in an unprofessional manner with wobbly eyes on put on top")
p #{trWeb("template.page.name")}
section.main-sidebar
section.header-navigation
header
img.logo(src="/static/img/logo.png", alt="The logo of the website: the letter C drawn in an unprofessional manner with wobbly eyes on put on top")
p& template.page.name
nav
ul
- menuItem("template.menu.home", "/");
- menuItem("template.menu.posts", "/posts/");
- menuItem("template.menu.projects", "/projects/");
- menuItem("template.menu.contact", "/contact");
block sidebar
nav
ul
- menuItem("template.menu.home", "/");
- menuItem("template.menu.posts", "/posts/");
- menuItem("template.menu.projects", "/projects/");
- menuItem("template.menu.contact", "/contact");
footer.hide-small
block footer
include parts/footer
main
block content
footer
footer.hide-big
block footer
include parts/footer