1 module vibelog.post; 2 3 import vibelog.settings; 4 5 import vibe.data.bson; 6 import vibe.textfilter.markdown; 7 import vibe.textfilter.html; 8 9 import std.array; 10 import std.conv; 11 import std.string : strip; 12 public import std.datetime; 13 14 import stringex.unidecode; 15 16 17 final class Post { 18 BsonObjectID id; 19 bool isPublic; 20 bool commentsAllowed; 21 string slug; // url entity to identify this post - generated from the header by default 22 string author; // user name 23 string category; // can be hierarchical using dotted.syntax.format 24 SysTime date; 25 string header; // Title/heading 26 string headerImage; // URL of large header image 27 string summaryTitle; // Short title used for the summary (<=70chars) 28 string summary; // Short summary of the article (<=240 chars), displayed on cards 29 string subHeader; // First paragraph of the articule, displayed on overview pages 30 string content; 31 string[] filters; 32 string[] tags; 33 string[] trackbacks; 34 35 this() 36 { 37 id = BsonObjectID.generate(); 38 date = Clock.currTime().toUTC(); 39 } 40 41 @property string name() const { return slug.length ? slug : id.toString(); } 42 43 static Post fromBson(Bson bson) 44 { 45 auto ret = new Post; 46 ret.id = cast(BsonObjectID)bson["_id"]; 47 ret.isPublic = cast(bool)bson["isPublic"]; 48 ret.commentsAllowed = cast(bool)bson["commentsAllowed"]; 49 ret.slug = cast(string)bson["slug"]; 50 ret.author = cast(string)bson["author"]; 51 ret.category = cast(string)bson["category"]; 52 ret.date = SysTime.fromISOExtString(cast(string)bson["date"]); 53 ret.headerImage = cast(string)bson["headerImage"]; 54 ret.header = cast(string)bson["header"]; 55 ret.subHeader = cast(string)bson["subHeader"]; 56 ret.content = cast(string)bson["content"]; 57 ret.summary = bson["summary"].opt!string; 58 ret.summaryTitle = bson["summaryTitle"].opt!string; 59 60 if (bson["filters"].isNull) ret.filters = ["markdown"]; 61 else { 62 foreach (f; cast(Bson[])bson["filters"]) 63 ret.filters ~= cast(string)f; 64 } 65 66 if (!bson["tags"].isNull) 67 foreach (t; cast(Bson[])bson["tags"]) 68 ret.tags ~= cast(string)t; 69 70 return ret; 71 } 72 73 Bson toBson() 74 const { 75 76 Bson[string] ret; 77 ret["_id"] = Bson(id); 78 ret["isPublic"] = Bson(isPublic); 79 ret["commentsAllowed"] = Bson(commentsAllowed); 80 ret["slug"] = Bson(slug); 81 ret["author"] = Bson(author); 82 ret["category"] = Bson(category); 83 ret["date"] = Bson(date.toISOExtString()); 84 ret["headerImage"] = Bson(headerImage); 85 ret["header"] = Bson(header); 86 ret["subHeader"] = Bson(subHeader); 87 ret["content"] = Bson(content); 88 ret["summary"] = Bson(summary); 89 ret["summaryTitle"] = Bson(summaryTitle); 90 91 import std.algorithm : map; 92 import std.array : array; 93 ret["filters"] = Bson(filters.map!Bson.array); 94 ret["tags"] = Bson(tags.map!Bson.array); 95 96 return Bson(ret); 97 } 98 99 string renderSubHeaderAsHtml(VibeLogSettings settings) 100 const { 101 import std.algorithm : canFind; 102 if (filters.canFind("markdown")) 103 { 104 auto ret = appender!string(); 105 filterMarkdown(ret, subHeader, settings.markdownSettings); 106 return ret.data; 107 } 108 else 109 { 110 return subHeader; 111 } 112 } 113 114 string renderContentAsHtml(VibeLogSettings settings, string page_path = "", int header_level_nesting = 0) 115 const { 116 117 import std.algorithm : canFind; 118 string html = content; 119 if (filters.canFind("markdown")) 120 { 121 scope ms = new MarkdownSettings; 122 ms.flags = settings.markdownSettings.flags; 123 ms.headingBaseLevel = settings.markdownSettings.headingBaseLevel + header_level_nesting; 124 if (page_path != "") 125 { 126 ms.urlFilter = (lnk, is_image) { 127 import std.algorithm : startsWith; 128 if (lnk.startsWith("http://") || lnk.startsWith("https://")) 129 return lnk; 130 if (lnk.startsWith("#")) return lnk; 131 auto pp = InetPath(page_path); 132 if (!pp.endsWithSlash) 133 pp = pp.parentPath; 134 return (settings.siteURL.path~("posts/"~slug~"/"~lnk)).relativeTo(pp).toString(); 135 }; 136 } 137 html = filterMarkdown(html, ms); 138 } 139 foreach (flt; settings.textFilters) 140 html = flt(html); 141 return html; 142 } 143 } 144 145 string makeSlugFromHeader(string header) 146 { 147 Appender!string ret; 148 auto decoded_header = unidecode(header).replace("[?]", "-"); 149 foreach (dchar ch; strip(decoded_header)) { 150 switch (ch) { 151 default: 152 ret.put('-'); 153 break; 154 case '"', '\'', '´', '`', '.', ',', ';', '!', '?', '¿', '¡': 155 break; 156 case 'A': .. case 'Z'+1: 157 ret.put(cast(dchar)(ch - 'A' + 'a')); 158 break; 159 case 'a': .. case 'z'+1: 160 case '0': .. case '9'+1: 161 ret.put(ch); 162 break; 163 } 164 } 165 return ret.data; 166 } 167 168 unittest { 169 assert(makeSlugFromHeader("sample title") == "sample-title"); 170 assert(makeSlugFromHeader("Sample Title") == "sample-title"); 171 assert(makeSlugFromHeader(" Sample Title2 ") == "sample-title2"); 172 assert(makeSlugFromHeader("反清復明") == "fan-qing-fu-ming"); 173 assert(makeSlugFromHeader("φύλλο") == "phullo"); 174 assert(makeSlugFromHeader("ខេមរភាសា") == "khemrbhaasaa"); 175 assert(makeSlugFromHeader("zweitgrößte der Europäischen Union") == "zweitgrosste-der-europaischen-union"); 176 assert(makeSlugFromHeader("østlige og vestlige del udviklede sig uafhængigt ") == "ostlige-og-vestlige-del-udviklede-sig-uafhaengigt"); 177 assert(makeSlugFromHeader("¿pchnąć w tę łódź jeża lub ośm skrzyń fig?") == "pchnac-w-te-lodz-jeza-lub-osm-skrzyn-fig"); 178 assert(makeSlugFromHeader("¼ €") == "1-4-eu"); 179 }