chris-website/source/nl/netsoj/chris/blog/model/page.d

144 lines
4.0 KiB
D

import std.exception;
import std.experimental.logger;
import std.file;
import std.process;
import std.stdio;
import dyaml;
import vibe.vibe;
import utils;
/**
* Exception thrown when a page has syntax errors e.g.
*/
class ArticleParseException : Exception {
mixin basicExceptionCtors;
}
/**
* Represents a page on the blog. Every other page, including blog articles,
* projects and so on derrive from this class.
*/
class Page {
/**
* Internal name of the article. Usually the file name.
*/
protected string m_name;
/**
* Slug either manually assigned or generated based on file name.
* Only used in the url.
*/
protected string m_slug;
protected string m_title;
protected string m_content;
protected string m_contentSource;
protected bool m_hidden;
protected string m_language;
/**
* Option for the markdown parser: the amount of levels the header found in the markdown should
* be shifted. For example, 0 means H1 -> H1, 1 means H1 -> H2, 2 means H1 -> H3 and so on.
*/
protected int m_headerShift = 1;
private bool hasCalledSuper = false;
/**
* Creates a page from a file. This will read from the file and parse it.
*/
this(string file) {
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");
enforce!ArticleParseException(seperatorIndex >= 0);
string header = m_contentSource[0..seperatorIndex];
Node node = Loader.fromString(header).load();
loadHeader(node);
assert(hasCalledSuper);
this.m_content = Page.parseMarkdown(m_contentSource[seperatorIndex + 4..$],
this.m_headerShift);
}
/**
* Parse metadata from the header. Subclasses should override this method,
* to parse their own metadata and call super.
* Params:
* headerNode = the YAML node to parse the header metadata from.
*/
@safe
protected void loadHeader(Node headerNode){
this.m_hidden = headerNode.getOr!bool("hidden", false);
if (headerNode.containsKey("title")) {
this.m_title = headerNode["title"].as!string;
} else {
warningf("%s does not contain a title", this.m_name);
}
if (headerNode.containsKey("slug")) {
this.m_slug = headerNode["slug"].as!string;
} else {
this.m_slug = this.m_title;
infof("%s does not have a slug. Using %s", this.m_name, this.m_slug);
}
this.m_language = headerNode.getOr!string("language", "unknown");
hasCalledSuper = true;
}
/**
* Starts pandoc to convert MarkDown to HTML
* Params:
* source = The MarkDown source as a string (not a path!)
* shiftHeader = (Optional) The amount a header needs to be shifted. If for example, it
* is set to 1, first level headings within MarkDown become second level
* headers within HTML.
*/
public static string parseMarkdown(string source, int shiftHeader = 0) {
string[] args = ["pandoc",
"-f", "markdown",
"-t", "html",
"--lua-filter", "defaultClasses.lua"];
if (shiftHeader != 0) args ~= "--shift-heading-level-by=" ~ to!string(shiftHeader);
ProcessPipes pandoc = pipeProcess(args);
pandoc.stdin.write(source);
pandoc.stdin.writeln();
pandoc.stdin.flush();
pandoc.stdin.close();
pandoc.pid.wait();
string result;
string line;
while ((line = pandoc.stdout.readln()) !is null) {
result ~= line;
debug {
//logf("Pandoc stdout: %s", line);
}
}
while ((line = pandoc.stderr.readln()) !is null) {
debug {
logf("Pandoc stderr: %s", line);
}
}
return result;
}
@property string name() { return m_name; }
@property string title() { return m_title; }
@property string slug() { return m_slug; }
@property string content() { return m_content; }
@property string contentSource() { return m_contentSource; }
@property bool isHidden() { return m_hidden; }
@property string language() { return m_language; }
}