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 }