143 lines
4 KiB
D
143 lines
4 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 long seperatorIndex = indexOf(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; }
|
|
}
|