Compare commits

..

19 commits

Author SHA1 Message Date
Chris Josten 7bcc91a2fa Use fallback date when there are no visible articles
This fixes a crash
2024-10-12 23:11:20 +02:00
Chris Josten 0e7aa0451f atom: add atom feed to website for posts 2024-10-12 22:31:23 +02:00
Chris Josten 63f177475b old style; update for new grid layout
It still looks terrible, but I suppose that was the entire idea of this
optional style
2024-10-12 20:54:03 +02:00
Chris Josten c68094cd11 gitignore: ignore result
This file is created by running nix build
2024-10-12 20:51:23 +02:00
Chris Josten 6874b14916 style: move to grid layout from flex
This allows to reposition items without hiding/showing classes
2024-10-12 20:37:26 +02:00
Chris Josten c1dfa1f065 Update copyright year 2024-10-12 14:04:51 +02:00
Chris Josten 0ad53b1a24 Update dub selections 2023-12-08 22:59:09 +01:00
Chris Josten 7bb5c65d90 Bump version to 0.0.3 2023-12-08 22:57:15 +01:00
Chris Josten 651668526d Update dmd version + dependencies 2023-12-08 22:52:11 +01:00
Chris Josten bdb7615044 Fix building on NixOS 2022-09-15 20:29:06 +02:00
Chris Josten 9a5aeb1526 Upgrade version number 2022-09-15 17:43:07 +02:00
Chris Josten cc526e88f0 Upgrade dependencies 2022-09-15 17:41:15 +02:00
Chris Josten 40eeb9d25a Fix whitespace 2022-05-30 12:48:57 +02:00
Chris Josten 16f25d8297 Update nix deps 2022-05-25 14:49:54 +02:00
Chris Josten 223ca8bc29 Workaround broken l10n, upgrade deps 2022-05-25 14:46:37 +02:00
Chris Josten 1d0d1a54b1 Fix crash caused by lastIndexOf returning negative numbers
The ArticleParser would crash when a negative number was returned by
lastIndexOf, since it was casted to an unsigned integer. This unsigned
integer then was used to allocate memory, which would allocate way to
much memory, causing a MemoryException and crashing the process.
2021-10-13 14:07:27 +02:00
Chris Josten 48b95c4a13 Article: parse document separators properly
Document separators should start with a newline, followed by three
dashes, followed by a newline again.
2021-10-11 15:15:12 +02:00
Chris Josten 75f5d447d6 Update dependencies Nix 2021-08-17 10:04:36 +02:00
Chris Josten e806dbf1cd Fix NixOS build 2021-06-27 12:50:21 +02:00
33 changed files with 422 additions and 538 deletions

1
.gitignore vendored
View file

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

View file

@ -14,6 +14,14 @@ 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}}
{CodeBlock = add_default_code_class},
{Image = make_image_url_absolute}}

View file

@ -5,16 +5,12 @@
"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",
"stringImportPaths": [
"views",
"translations"
],
"targetType": "executable"
}
"targetType": "executable",
"stringImportPaths": ["views", "translations"],
"versions": ["DeimosOpenSSL_3_0"]
}

View file

@ -3,21 +3,21 @@
"versions": {
"botan": "1.12.19",
"botan-math": "1.0.3",
"diet-ng": "1.7.5",
"dyaml": "0.8.3",
"eventcore": "0.9.13",
"diet-ng": "1.8.1",
"dyaml": "0.8.6",
"eventcore": "0.9.26",
"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.4",
"memutils": "1.0.9",
"mir-linux-kernel": "1.0.1",
"openssl": "1.1.6+1.0.1g",
"openssl": "3.3.3",
"openssl-static": "1.0.2+3.0.8",
"stdx-allocator": "2.77.5",
"taggedalgebraic": "0.11.19",
"taggedalgebraic": "0.11.22",
"tinyendian": "0.2.0",
"vibe-core": "1.13.0",
"vibe-d": "0.9.3"
"vibe-container": "1.0.1",
"vibe-core": "2.5.1",
"vibe-d": "0.9.7"
}
}

View file

@ -51,49 +51,61 @@
fetch = {
type = "git";
url = "https://github.com/etcimon/memutils.git";
rev = "v1.0.4";
sha256 = "1m65iy03yl5km61ijk5ysapxc5mks9b15hmaqxpn111981qrqx9z";
rev = "v1.0.9";
sha256 = "08j9grn3l6gr275m392l0dfsx5ma6hs08gjfhh48hpy3v0bvb0ny";
fetchSubmodules = false;
date = "2020-02-02T20:53:12-05:00";
date = "2023-03-02T15:19:28-05:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/3pmdm9szxsqfvi2dv8b58vk7856g3gd8-memutils";
path = "/nix/store/glnf0ljd84mv6gzc9wgs1qq0vqqxvmzh-memutils";
};
} {
fetch = {
type = "git";
url = "https://github.com/s-ludwig/taggedalgebraic.git";
rev = "v0.11.19";
sha256 = "1mb4l9hhkzhwwj2v3m9l4g59q66msy15ky762wk5dv11viyfwrqb";
rev = "v0.11.22";
sha256 = "02iy90nwy0zzy25hwdqbcgd0w0lwzramcvi3pgyljhq0w0vl5hkq";
fetchSubmodules = false;
date = "2021-01-13T16:58:20+01:00";
date = "2021-05-20T21:00:02+02:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/qlw3bv0xqd0dl01hd9lcyk9cx7v9qhvi-taggedalgebraic";
path = "/nix/store/p8id0qb13j8pjdczflj2x35w6v63q4cx-taggedalgebraic";
};
} {
fetch = {
type = "git";
url = "https://github.com/vibe-d/vibe-core.git";
rev = "v1.13.0";
sha256 = "1zbx861dwmkp14pbzr4qyq71xsnlksx248x0a1q6afjmvvxiys8w";
url = "https://github.com/vibe-d/vibe-container.git";
rev = "v1.0.1";
sha256 = "0miynjmfzz340z3qw9mclaj6cyirzrpk528idr5yfmvl3jsi6wh8";
fetchSubmodules = false;
date = "2021-01-15T21:35:13+01:00";
date = "2023-11-27T09:12:47+01:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/zngy3z8hmdgjg8wl31d2qy194mhz0i09-vibe-core";
path = "/nix/store/2vnn20q8k335h5k078yk4lrxkp4y63pd-vibe-container";
};
} {
fetch = {
type = "git";
url = "https://github.com/vibe-d/vibe.d.git";
rev = "v0.9.3";
sha256 = "10a5njn2nq1z6gknmkg6m6wrbndzj19mpg5860ky6v12b9ks76z3";
rev = "v0.9.7";
sha256 = "1q4yvcaf36lmf29izg01x888v40snpwiph310nrlpr4gyhd834av";
fetchSubmodules = false;
date = "2021-01-29T11:37:00+01:00";
date = "2023-08-29T13:24:09+02:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/8r3h7npkhj9wq9lg64i3fih19hibpmg5-vibe.d";
path = "/nix/store/7fq089i7zib7m9hxyl75mlfy264d9gnx-vibe.d";
};
} {
fetch = {
type = "git";
url = "https://github.com/vibe-d/vibe-core.git";
rev = "v2.5.1";
sha256 = "1g66vyn9hivy8rmsdl1xg1vz41csq5k91c04yvp4wqbil9cwgqdr";
fetchSubmodules = false;
date = "2023-11-24T17:25:20+01:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/iddq1amvzic8fn4xa2317ykmkrkdr51y-vibe-core";
};
} {
fetch = {
@ -123,13 +135,13 @@
fetch = {
type = "git";
url = "https://github.com/rejectedsoftware/diet-ng.git";
rev = "v1.7.5";
sha256 = "1cymg3v924d499sbjagjf5dqv1pj196c55a2282knxgq18a3gynf";
rev = "v1.8.1";
sha256 = "11hrbvsxhipvcz9m1qlq92iaw0h35pzdy5rf7z8c4vx3hwjrnhni";
fetchSubmodules = false;
date = "2021-02-07T14:20:30+01:00";
date = "2022-04-22T11:38:43+02:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/5r6v9f7y9kwkb90ihgimqqryf6ls3kqq-diet-ng";
path = "/nix/store/xziliy9ah3s3ah8mslnxsw47n4w6wkca-diet-ng";
};
} {
fetch = {
@ -147,37 +159,37 @@
fetch = {
type = "git";
url = "https://github.com/D-Programming-Deimos/openssl.git";
rev = "v1.1.6+1.0.1g";
sha256 = "0ramqjyq4v7xpqwf4nf4ddmsg6yk2fbzn2d1yj4b6dla3q5lv9i3";
rev = "v3.3.3";
sha256 = "1634j4psp3qwgwhk2sa3jj6gvwv3i96hpg2wrdy9ihjhabnszn0f";
fetchSubmodules = false;
date = "2017-11-05T20:15:26+01:00";
date = "2023-09-14T12:05:32+00:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/jbfb5in6gzqs2byn1hdc1625yh8sx6j2-openssl";
path = "/nix/store/i3pgh154r80xvh4vsnl6srg6khmk5dg7-openssl";
};
} {
fetch = {
type = "git";
url = "https://github.com/vibe-d/eventcore.git";
rev = "v0.9.13";
sha256 = "0frxifhjwzyi35cv4pvv8k11a966fg76gqxdpmx9bsqbx750lrvz";
rev = "v0.9.26";
sha256 = "13bjs5v5l1387vi2ss4gvqlslhq33v9sn4pg7nis4r0wdw0zmlk1";
fetchSubmodules = false;
date = "2021-01-12T19:20:28+01:00";
date = "2023-09-16T09:46:45+02:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/yikgbvj61xhk6r3x8pc8lb7sp5smcn5q-eventcore";
path = "/nix/store/zy7ydasg4fwnim2m1mi4gzdg9l087wpb-eventcore";
};
} {
fetch = {
type = "git";
url = "https://github.com/kiith-sa/D-YAML.git";
rev = "v0.8.3";
sha256 = "13wy304xjbwkpgg7ilql1lkxkm83s87jm59ffnrg26slp7cx149q";
rev = "v0.8.6";
sha256 = "1bvidcxp1n65r4wmiqakyl8vjvhqh3gln9wbsmbxrx9mf6k0zv8h";
fetchSubmodules = false;
date = "2020-09-19T23:46:57+02:00";
date = "2022-05-15T16:18:02-03:00";
deepClone = false;
leaveDotGit = false;
path = "/nix/store/3i8i56lkmw2xq3lxr5h66v909waq2mqg-D-YAML";
path = "/nix/store/zd0yayd1j11809j95sjs4gdqcngvkhbg-D-YAML";
};
} {
fetch = {
@ -191,4 +203,16 @@
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,7 +6,11 @@ mkDubDerivation {
src = ./.;
dubJSON = ./dub.json;
selections = ./dub.selections.nix;
version = "0.0.1";
version = "0.0.3";
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,7 +1,9 @@
{ pkgs ? import <nixpkgs> {},
stdenv ? pkgs.stdenv,
rdmd ? pkgs.rdmd,
lib ? pkgs.lib,
dtools ? pkgs.dtools or pkgs.rdmd,
dmd ? pkgs.dmd,
dcompiler ? dmd,
dub ? pkgs.dub }:
with stdenv;
@ -20,7 +22,7 @@ let
fromDub = dubDep: mkDerivation rec {
name = "${src.name}-${version}";
version = rev-to-version dubDep.fetch.rev;
nativeBuildInputs = [ rdmd dmd dub ];
nativeBuildInputs = [ dcompiler dtools dub ];
src = dep2src dubDep;
buildPhase = ''
@ -46,10 +48,18 @@ let
targetOf = package: "${package.targetPath or "."}/${package.targetName or package.name}";
# Remove reference to build tools and library sources
disallowedReferences = deps: [ dmd rdmd dub ] ++ builtins.map dep2src deps;
disallowedReferences = deps: [ dcompiler dtools 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;
@ -57,20 +67,23 @@ 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 (attrs // {
} @ attrs: stdenv.mkDerivation ((removeAttrs attrs ["package" "deps" "selections" "dubJSON" "dubSDL"]) // {
pname = package.name;
nativeBuildInputs = [ rdmd dmd dub pkgs.removeReferencesTo ] ++ nativeBuildInputs;
nativeBuildInputs = [ dcompiler dtools dub pkgs.removeReferencesTo ] ++ nativeBuildInputs;
disallowedReferences = disallowedReferences deps;
passthru = passthru // {
inherit dub dmd rdmd pkgs;
inherit dub dcompiler dtools pkgs;
};
src = lib.cleanSourceWith {
@ -87,7 +100,7 @@ in {
export HOME=$PWD
${lib.concatMapStringsSep "\n" dub-add-local deps}
dub build -b release --combined --skip-registry=all
dub build -b ${buildType} --combined --skip-registry=all ${extraDubFlags}
runHook postBuild
'';
@ -97,7 +110,7 @@ in {
export HOME=$PWD
${lib.concatMapStringsSep "\n" dub-add-local deps}
dub test --combined --skip-registry=all
dub test --combined --skip-registry=all ${extraDubFlags}
runHook postCheck
'';
@ -113,6 +126,8 @@ 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,32 +1,3 @@
@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;
@ -62,48 +33,54 @@ body {
background-color: #f0f0f0;
background-color: var(--colour-bg);
margin: 0;
display: flex;
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";
justify-content: center;
min-height: 100vh;
color: #000000;
color: var(--colour-fg);
}
body > section.header-navigation {
flex: 0 0 200px;
padding: 2em;
}
@media (pointer: coarse) {
body > section.header-navigation li {
padding: 0.25em 0;
}
body > nav {
grid-area: menu;
}
body > footer {
flex: 0 0 200px;
padding: 2em;
border: none;
grid-area: footer;
margin-top: 0;
}
body > section.header-navigation > header {
body > header {
grid-area: header;
text-align: center;
margin-bottom: 0;
}
body > section.header-navigation > header > img {
body > header > img {
width: 100%;
max-width: 160px;
}
body > section.main-sidebar {
grid-area: sidebar;
}
body > main {
flex: 1 1;
grid-area: main;
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: 600px;
max-width: calc(600px);
background-color: #ddd;
background-color: var(--colour-bg-main);
}
@ -274,11 +251,65 @@ a {
text-decoration: underline dotted;
}
a:hover {
a:hover, a:focus {
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,6 +2,17 @@ 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 {
@ -16,14 +27,15 @@ footer {
a:hover {
cursor: url(old/cursor-over.gif), auto;
color: red;
}
.header-navigation {
body > nav, body > header, body > footer {
background-color: cyan;
color: black;
}
.header-navigation> nav ul {
body > nav ul {
padding-left: 2em;
list-style-image: url(old/bullet.gif);
}

View file

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

View file

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

View file

@ -1,15 +1,14 @@
module nl.netsoj.chris.blog.interfaces.http;
import std.experimental.logger;
import std.algorithm : map, maxElement;
import std.format : format;
import std.string: join;
import vibe.d;
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;
import cache;
import constants;
import article;
import page;
import project;
/**
* Output types for the content.
@ -64,7 +63,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 req.query.get("lang", "en_GB");
return determineLanguageByHeader(req.query.get("lang", "en_GB"), languages);
}
}
@ -75,41 +74,29 @@ 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:
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:
res.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);
@ -158,13 +145,62 @@ 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"));
}
private:
IndieAuth m_indieAuth;
@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");
}
}
/**
@ -172,9 +208,7 @@ private:
*/
@safe
void errorPage(HTTPServerRequest req, HTTPServerResponse res, HTTPServerErrorInfo error) {
//render!("pages/error.dt", error)(res);
import std.conv;
res.writeBody(text("Error ", error.code, ": ", error.message, "\n\n", error.debugMessage), "text/plain");
render!("pages/error.dt", error)(res);
}
@trusted
@ -196,7 +230,4 @@ void startHTTPServer() {
router.registerWebInterface(new MijnBlog);
listenHTTP(settings, router);
foreach(route; router.getAllRoutes()) {
infof("Path: %s", route);
}
}

View file

@ -1,92 +0,0 @@
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

@ -1,14 +0,0 @@
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,14 +1,23 @@
module nl.netsoj.chris.blog.main;
import std.experimental.logger;
import vibe.d;
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;
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);
}
void main() {
@ -16,13 +25,13 @@ void main() {
// Start indexing pages.
runTask({
initPages!Page(&pages, "pages");
watchTask!Page(&pages, "pages");
});
runTask({
initPages!Article(&articles, "articles");
watchTask!Article(&articles, "articles");
});
runTask({
initPages!Project(&projects, "projects");
watchTask!Project(&projects, "projects");
});
runApplication();
}

View file

@ -1,12 +0,0 @@
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

@ -1,116 +0,0 @@
/**
* 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,5 +1,3 @@
module nl.netsoj.chris.blog.model.article;
import std.file;
import std.stdio;
import std.string;
@ -9,9 +7,10 @@ import std.experimental.logger;
import dyaml;
import vibe.d;
import nl.netsoj.chris.blog.model.page;
import nl.netsoj.chris.blog.utils;
import page;
import utils;
@safe:
/**
* Represents an article on the blog
@ -36,9 +35,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 uint seperatorIndex = cast(uint) indexOf(m_contentSource, "---\n");
const long seperatorIndex = cast(long) lastIndexOf(m_contentSource, "---\n");
this.m_excerpt = this.m_contentSource[seperatorIndex + 4..$];
const uint firstHeaderIndex = cast(uint) indexOf(this.m_excerpt, '#');
const long firstHeaderIndex = indexOf(this.m_excerpt, '#');
if (firstHeaderIndex >= 0) {
this.m_excerpt = this.m_excerpt[0..firstHeaderIndex];
}

View file

@ -1,5 +1,3 @@
module nl.netsoj.chris.blog.model.page;
import std.exception;
import std.experimental.logger;
import std.file;
@ -9,8 +7,10 @@ import std.stdio;
import dyaml;
import vibe.vibe;
import nl.netsoj.chris.blog.utils;
import constants;
import 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) {
this(string file) @safe {
this.m_name = file;
this.m_contentSource = readText(file);
// Find the seperator and split the string in two
const uint seperatorIndex = cast(uint) lastIndexOf(m_contentSource, "---\n");
const long seperatorIndex = lastIndexOf(m_contentSource, "\n---\n");
enforce!ArticleParseException(seperatorIndex >= 0);
string header = m_contentSource[0..seperatorIndex];
@ -108,10 +108,12 @@ 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);
ProcessPipes pandoc = pipeProcess(args, Redirect.all, env);
pandoc.stdin.write(source);
pandoc.stdin.writeln();
pandoc.stdin.flush();

View file

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

View file

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

View file

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

View file

@ -1,27 +0,0 @@
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,5 +1,3 @@
module nl.netsoj.chris.blog.utils;
import std.algorithm;
import std.array;
import std.conv;

View file

@ -1,41 +1,47 @@
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 nl.netsoj.chris.blog.cache;
import nl.netsoj.chris.blog.model.page;
import cache;
import 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)
if (is(T : Page)) {
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;
}
bool addPage(string path) {
try {
T newPage = new T(path);
logf("Added %s", newPage.slug);
logInfo("Added %s", newPage.slug);
cache.addItem(newPage);
return true;
} catch (ArticleParseException e) {
logf("Could not parse %s: %s", path, e);
} catch (page.ArticleParseException e) {
logWarn("Could not parse %s: %s", path, e);
return false;
} catch (Exception e) {
logf("Other exception while parsing %s: %s", path, e);
logWarn("Other exception while parsing %s: %s", path, e);
return false;
}
}
// Initial scan
void scan(NativePath path, int level = 0) {
logf("Scanning %s", path.toString());
logInfo("Scanning %s", path.toString());
foreach(file; iterateDirectory(path)) {
if (file.isDirectory) {
scan(path ~ file.name, level + 1);
@ -45,11 +51,11 @@ void initPages(T, C)(C *cache, const string directory)
}
}
if (!existsFile(getWorkingDirectory() ~ directory)) {
createDirectory(getWorkingDirectory() ~ directory);
if (!existsFile(watchingDir)) {
createDirectory(watchingDir);
}
scan(getWorkingDirectory() ~ directory);
DirectoryWatcher watcher = watchDirectory(getWorkingDirectory() ~ directory, true);
scan(watchingDir);
DirectoryWatcher watcher = watchDirectory(watchingDir, true);
bool shouldStop = false;
while (!shouldStop) {
@ -57,16 +63,16 @@ void initPages(T, C)(C *cache, const string directory)
DirectoryChange[] changes;
shouldStop = !watcher.readChanges(changes);
foreach(change; changes) {
logf("=======[New changes]======");
logInfo("=======[New changes]======");
string[] changeTypes = ["added", "removed", "modified"];
logf("Path: %s, type: %s", change.path.toString(), changeTypes[change.type]);
logInfo("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) {
warningf("Error while updating %s: %s", change.path.toString(), e.msg);
logWarn("Error while updating %s: %s", change.path.toString(), e.msg);
}
break;
case modified:
@ -74,17 +80,17 @@ void initPages(T, C)(C *cache, const string directory)
try {
newPage = new T(change.path.toString());
cache.changeItem(newPage);
} catch(ArticleParseException e) {
warningf("Could not parse %s", change.path.toString());
} catch(page.ArticleParseException e) {
logWarn("Could not parse %s", change.path.toString());
} catch (Exception e) {
warningf("Error while updating %s: %s", change.path.toString(), e.msg);
logWarn("Error while updating %s: %s", change.path.toString(), e.msg);
}
break;
case removed:
try {
cache.removeItemByName(change.path.toString());
} catch(Exception e) {
logf("Error while trying to remove %s: %s", T.stringof, e.msg);
logInfo("Error while trying to remove %s: %s", T.stringof, e.msg);
}
break;
default: break;

View file

@ -17,6 +17,12 @@ msgid "template.menu.contact"
msgstr "Contact"
msgid "template.page.copyright"
msgstr "&copy; Chris Josten, 2021. If not specified otherwise, all content on this"
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"

View file

@ -30,5 +30,11 @@ msgid "template.menu.contact"
msgstr "Contact"
msgid "template.page.copyright"
msgstr "&copy; Chris Josten, 2021. Tenzij anders vermeld staat, valt alle inhoud op deze webstek"
msgstr "&copy; Chris Josten, 2024. 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,2 +1,28 @@
msgid "page.header-bottom-text"
msgstr ""
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"

View file

@ -12,7 +12,7 @@ block content
- if (articleList.length == 0)
p No posts found
- else
- import nl.netsoj.chris.blog.utils;
- import 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 nl.netsoj.chris.blog.utils;
- import utils;
header
h1.title(itemprop="headline") #{content.title}
p.subtitle

View file

@ -1,21 +0,0 @@
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& template.page.copyright
small !{trWeb("template.page.copyright")}

View file

@ -1,11 +1,12 @@
doctype html
html(prefix="og: http://ogp.me/ns#")
- import nl.netsoj.chris.blog.constants;
- import 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;
@ -43,23 +44,25 @@ html(prefix="og: http://ogp.me/ns#")
li
a(href="#{link}") #{trWeb(text)}
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");
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
block sidebar
footer.hide-small
block footer
include parts/footer
nav
ul
- menuItem("template.menu.home", "/");
- menuItem("template.menu.posts", "/posts/");
- menuItem("template.menu.projects", "/projects/");
- menuItem("template.menu.contact", "/contact");
main
block content
footer.hide-big
footer
block footer
include parts/footer