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