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 public import std.datetime;
12 
13 
14 final class Post {
15 	BsonObjectID id;
16 	bool isPublic;
17 	bool commentsAllowed;
18 	string slug; // url entity to identify this post - generated from the header by default
19 	string author;  // user name
20 	string category; // can be hierarchical using dotted.syntax.format
21 	SysTime date;
22 	string header;
23 	string subHeader;
24 	string content;
25 	string headerImage;
26 	string[] tags;
27 	string[] trackbacks;
28 
29 	this()
30 	{
31 		id  = BsonObjectID.generate();
32 		date = Clock.currTime().toUTC();
33 	}
34 
35 	@property string name() const { return slug.length ? slug : id.toString(); }
36 	
37 	static Post fromBson(Bson bson)
38 	{
39 		auto ret = new Post;
40 		ret.id = cast(BsonObjectID)bson["_id"];
41 		ret.isPublic = cast(bool)bson["isPublic"];
42 		ret.commentsAllowed = cast(bool)bson["commentsAllowed"];
43 		ret.slug = cast(string)bson["slug"];
44 		ret.author = cast(string)bson["author"];
45 		ret.category = cast(string)bson["category"];
46 		ret.date = SysTime.fromISOExtString(cast(string)bson["date"]);
47 		ret.header = cast(string)bson["header"];
48 		ret.subHeader = cast(string)bson["subHeader"];
49 		ret.headerImage = cast(string)bson["headerImage"];
50 		ret.content = cast(string)bson["content"];
51 		foreach( t; cast(Bson[])bson["tags"] )
52 			ret.tags ~= cast(string)t;
53 		return ret;
54 	}
55 	
56 	Bson toBson()
57 	const {
58 		Bson[] btags;
59 		foreach( t; tags )
60 			btags ~= Bson(t);
61 
62 		Bson[string] ret;
63 		ret["_id"] = Bson(id);
64 		ret["isPublic"] = Bson(isPublic);
65 		ret["commentsAllowed"] = Bson(commentsAllowed);
66 		ret["slug"] = Bson(slug);
67 		ret["author"] = Bson(author);
68 		ret["category"] = Bson(category);
69 		ret["date"] = Bson(date.toISOExtString());
70 		ret["header"] = Bson(header);
71 		ret["subHeader"] = Bson(subHeader);
72 		ret["headerImage"] = Bson(headerImage);
73 		ret["content"] = Bson(content);
74 		ret["tags"] = Bson(btags);
75 
76 		return Bson(ret);
77 	}
78 
79 	string renderSubHeaderAsHtml(VibeLogSettings settings)
80 	const {
81 		auto ret = appender!string();
82 		filterMarkdown(ret, subHeader, settings.markdownSettings);
83 		return ret.data;
84 	}
85 
86 	string renderContentAsHtml(VibeLogSettings settings, string page_path, int header_level_nesting = 0)
87 	const {
88 		import std.algorithm : startsWith;
89 		scope ms = new MarkdownSettings;
90 		ms.flags = settings.markdownSettings.flags;
91 		ms.headingBaseLevel = settings.markdownSettings.headingBaseLevel + header_level_nesting;
92 		ms.linkFilter = (lnk) {
93 			if (lnk.startsWith("http://") || lnk.startsWith("https://"))
94 				return lnk;
95 			auto pp = Path(page_path);
96 			if (!pp.endsWithSlash)
97 				pp = pp[0 .. $-1];
98 			return Path("/posts/"~slug~"/"~lnk).relativeTo(pp).toString();
99 		};
100 
101 		auto html = filterMarkdown(content, ms);
102 		foreach (flt; settings.textFilters)
103 			html = flt(html);
104 		return html;
105 	}
106 }
107 
108 final class Comment {
109 	BsonObjectID id;
110 	BsonObjectID postId;
111 	bool isPublic;
112 	SysTime date;
113 	int answerTo;
114 	string authorName;
115 	string authorMail;
116 	string authorHomepage;
117 	string authorIP;
118 	string header;
119 	string content;
120 
121 	static Comment fromBson(Bson bson)
122 	{
123 		auto ret = new Comment;
124 		ret.id = cast(BsonObjectID)bson["_id"];
125 		ret.postId = cast(BsonObjectID)bson["postId"];
126 		ret.isPublic = cast(bool)bson["isPublic"];
127 		ret.date = SysTime.fromISOExtString(cast(string)bson["date"]);
128 		ret.answerTo = cast(int)bson["answerTo"];
129 		ret.authorName = cast(string)bson["authorName"];
130 		ret.authorMail = cast(string)bson["authorMail"];
131 		ret.authorHomepage = cast(string)bson["authorHomepage"];
132 		ret.authorIP = bson["authorIP"].opt!string();
133 		ret.header = cast(string)bson["header"];
134 		ret.content = cast(string)bson["content"];
135 		return ret;
136 	}
137 	
138 	Bson toBson()
139 	const {
140 		Bson[string] ret;
141 		ret["_id"] = Bson(id);
142 		ret["postId"] = Bson(postId);
143 		ret["isPublic"] = Bson(isPublic);
144 		ret["date"] = Bson(date.toISOExtString());
145 		ret["answerTo"] = Bson(answerTo);
146 		ret["authorName"] = Bson(authorName);
147 		ret["authorMail"] = Bson(authorMail);
148 		ret["authorHomepage"] = Bson(authorHomepage);
149 		ret["authorIP"] = Bson(authorIP);
150 		ret["header"] = Bson(header);
151 		ret["content"] = Bson(content);
152 		return Bson(ret);
153 	}
154 
155 	string renderContentAsHtml()
156 	const {
157 		auto ret = appender!string();
158 		filterMarkdown(ret, htmlEscape(content));
159 		return ret.data;
160 	}
161 }
162 
163 string makeSlugFromHeader(string header)
164 {
165 	Appender!string ret;
166 	foreach( dchar ch; header ){
167 		switch( ch ){
168 			default:
169 				ret.put('-');
170 				break;
171 			case '"', '\'', '´', '`', '.', ',', ';', '!', '?':
172 				break;
173 			case 'A': .. case 'Z'+1:
174 				ret.put(cast(dchar)(ch - 'A' + 'a'));
175 				break;
176 			case 'a': .. case 'z'+1:
177 			case '0': .. case '9'+1:
178 				ret.put(ch);
179 				break;
180 		}
181 	}
182 	return ret.data;
183 }