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; } }