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 auto pp = Path(page_path); 125 if (!pp.endsWithSlash) 126 pp = pp[0 .. $-1]; 127 return (settings.siteURL.path~("posts/"~slug~"/"~lnk)).relativeTo(pp).toString(); 128 }; 129 } 130 html = filterMarkdown(html, ms); 131 } 132 foreach (flt; settings.textFilters) 133 html = flt(html); 134 return html; 135 } 136 } 137 138 UniDecoder unidecoder; 139 140 string makeSlugFromHeader(string header) 141 { 142 Appender!string ret; 143 auto decoded_header = getDecoder().decode(header); 144 foreach( dchar ch; strip(decoded_header) ){ 145 switch( ch ){ 146 default: 147 ret.put('-'); 148 break; 149 case '"', '\'', '´', '`', '.', ',', ';', '!', '?', '¿', '¡': 150 break; 151 case 'A': .. case 'Z'+1: 152 ret.put(cast(dchar)(ch - 'A' + 'a')); 153 break; 154 case 'a': .. case 'z'+1: 155 case '0': .. case '9'+1: 156 ret.put(ch); 157 break; 158 } 159 } 160 return ret.data; 161 } 162 163 unittest { 164 assert(makeSlugFromHeader("sample title") == "sample-title"); 165 assert(makeSlugFromHeader("Sample Title") == "sample-title"); 166 assert(makeSlugFromHeader(" Sample Title2 ") == "sample-title2"); 167 assert(makeSlugFromHeader("反清復明") == "fan-qing-fu-ming"); 168 assert(makeSlugFromHeader("φύλλο") == "phullo"); 169 assert(makeSlugFromHeader("ខេមរភាសា") == "khemrbhaasaa"); 170 assert(makeSlugFromHeader("zweitgrößte der Europäischen Union") == "zweitgrosste-der-europaischen-union"); 171 assert(makeSlugFromHeader("østlige og vestlige del udviklede sig uafhængigt ") == "ostlige-og-vestlige-del-udviklede-sig-uafhaengigt"); 172 assert(makeSlugFromHeader("¿pchnąć w tę łódź jeża lub ośm skrzyń fig?") == "pchnac-w-te-lodz-jeza-lub-osm-skrzyn-fig"); 173 assert(makeSlugFromHeader("¼ €") == "1-4-eu"); 174 } 175 176 private UniDecoder getDecoder() { 177 if (unidecoder is null) { 178 unidecoder = new UniDecoder(); 179 } 180 return unidecoder; 181 }