2655c4e11e8306f3483d8859a9522eb397ab14ca
[debian/pino.git] / src / template.vala
1 using Gee;
2 using PinoEnums;
3
4 public class Template : Object {
5         
6         private VisualStyle visual_style;
7         
8         private string main_tpl = """
9                 <html>
10                         <head>
11                         <script type="text/javascript">
12                         function menu(e, data) {
13                                 if(e.button == 2) {
14                                         location.href="contextmenu://" + data;
15                                         return true;
16                                 }
17                         }
18                         function reply(e, data) {
19                                 var sel = window.getSelection();
20                                 sel.removeAllRanges();
21                                 location.href="reply://" + data;
22                                 return true;
23                         }
24                         </script>
25                         %s
26                         </head>
27                         <body>
28                         %s
29                         </body>
30                 </html>
31         """;
32         
33         /*
34         private string header_tpl = """
35                 <style type="text/css">
36         body {
37                 color: {{fg_color}};
38                 #font-family: Droid Sans;
39                 #font-size: 9pt;
40         }
41         .status, .status-fresh {
42                 margin-bottom: 10px;
43         }
44         .tri {
45                 z-index: 3;
46                 position: absolute;
47                 top: 16px;
48                 left: 0px;
49                 width: 14px;
50                 height: 14px;
51                 background-color: {{bg_color}};
52                 border: 1px solid #ddd;
53                 border-right-style: none;
54                 border-top-style: none;
55                 -webkit-transform: rotate(45deg);
56                 -webkit-border-radius: 0px 0px 0px 2px;
57                 -webkit-box-shadow: 0px 1px 1px  #ccc;
58         }
59         .line {
60                 z-index: 5;
61                 position: absolute;
62                 background-color: {{bg_color}};
63                 top: 14px;
64                 left: 7px;
65                 width: 1px;
66                 height: 19px;
67                 -webkit-border-radius: 3px;
68         }
69         .status-content {
70                 z-index: 4;
71                 position: relative;
72                 background-color: {{bg_color}};
73                 border: 1px solid #ddd;
74                 -webkit-border-radius: 3px;
75                 padding: 6px;
76                 margin-left: 7px;
77                 -webkit-box-shadow: 1px 1px 1px  #ccc;
78                 cursor: default;
79         }
80         a {
81                 color: {{lk_color}};
82         }
83         .tags {
84                 font-weight: bold;
85                 text-decoration: none;
86         }
87         .status-fresh .status-content {
88                 border-width: 2px;
89                 border-color: #478bde;
90         }
91         .status-fresh .tri {
92                 border-width: 2px;
93                 border-color: #478bde;
94         }
95         .status-fresh .line {
96                 top: 16px;
97                 height: 16px;
98                 width: 2px;
99         }
100         .status-own .tri {
101                 position: relative;
102                 float: right;
103                 -webkit-border-radius: 0px 2px 0px 0px;
104                 -webkit-box-shadow: 1px 0px 1px  #ccc;
105         }
106         .status-own .line {
107                 position: relative;
108                 float: right;
109                 width: 3px;
110                 left: 10px;
111                 #background-color: red;
112         }
113         .status-own .status-content {
114                 margin-left: 0px;
115                 margin-right: 7px;
116         }
117         .status-own .right {
118                 margin-left: 0px;
119                 margin-right: 58px;
120         }
121         .status-own .left {
122                 float: right;
123         }
124         .right {
125                 position:relative;
126                 margin-bottom: 10px;
127                 margin-left: 58px;
128         }
129         .left {
130                 float: left;
131                 width: 48px;
132                 height: 48px;
133                 backgrond-color: #fff;
134                 -webkit-border-radius: 3px;
135                 -webkit-background-size: 48px 48px;
136                 -webkit-box-shadow: 1px 1px 1px  #ccc;
137         }
138         .header {
139                 margin-bottom: 3px;
140         }
141         .header a, .re_nick {
142                 font-weight: bold;
143                 text-decoration: none;
144                 color: {{fg_color}};
145                 text-shadow: 1px 1px 0 #fff;
146         }
147         .date, .footer {
148                 font-size: smaller;
149                 font-weight: bold;
150                 text-shadow: 1px 1px 0 #fff;
151                 opacity: 0.6;
152                 float: right;
153         }
154         .footer {
155                 float: none;
156                 display: block;
157                 text-decoration: none;
158                 margin-top: 3px;
159                 color: {{fg_color}};
160         }
161         .rt {
162                 background-color: {{fg_color}};
163                 color: {{bg_color}};
164                 opacity: 0.6;
165                 margin-right: 2px;
166                 font-weight: bold;
167                 padding-left: 3px;
168                 padding-right: 3px;
169                 -webkit-border-radius: 3px;
170         }
171         .menu {
172                 background-color: {{fg_color}};
173                 opacity: 0.0;
174                 width: 15px;
175                 height: 15px;
176                 float: right;
177                 #margin-left: 3px;
178                 margin-top: -9px;
179                 #margin-bottom: 5px;
180                 margin-right: -6px;
181                 -webkit-border-radius: 3px 0 3px 0;
182         }
183         @-webkit-keyframes menu-hover {
184                 from {
185                         opacity: 0.0;
186                 }
187                 to {
188                         opacity: 0.6;
189                 }
190         }
191         .status-content:hover .menu {
192                 opacity: 0.6;
193                 -webkit-animation-name: menu-hover;
194                 -webkit-animation-duration: 1s;
195         }
196                 </style>
197         """;
198         */
199         
200         private string header_tpl = """
201                 <style type="text/css">
202         body {
203                 color: {{fg_color}};
204                 background: {{bg_light_color}}
205                 #font-family: Droid Sans;
206                 #font-size: 9pt;
207                 margin: 0px;
208         }
209         .status, .status-fresh, .status-own {
210                 padding: 6px;
211                 position: relative;
212                 min-height: 50px;
213                 border: 0px solid #edeceb;
214                 border-bottom-width: 1px;
215         }
216         .status-content {
217                 z-index: 4;
218                 position: relative;
219                 margin-left: 7px;
220                 cursor: default;
221         }
222         a {
223                 color: {{lk_color}};
224         }
225         .tags {
226                 font-weight: bold;
227                 text-decoration: none;
228         }
229         .status-fresh {
230                 #background: #c3dff7;
231                 background: -webkit-gradient(linear, 0 -75, 0 bottom, from({{bg_light_color}}), to(#c6ebb1));
232                 border: 0px;
233         }
234         .status-own .status-content {
235                 margin-left: 0px;
236                 margin-right: 7px;
237         }
238         .status-own .right {
239                 margin-left: 0px;
240                 margin-right: 50px;
241         }
242         .status-own .left {
243                 float: right;
244         }
245         .right {
246                 position:relative;
247                 margin-left: 50px;
248         }
249         .left {
250                 float: left;
251                 width: 48px;
252                 height: 48px;
253                 backgrond-color: #fff;
254                 -webkit-border-radius: 3px;
255                 -webkit-background-size: 48px 48px;
256                 -webkit-box-shadow: 1px 1px 1px  #ccc;
257         }
258         .header {
259                 margin-bottom: 3px;
260         }
261         .header a, .re_nick {
262                 font-weight: bold;
263                 text-decoration: none;
264                 color: #404040;
265                 text-shadow: 1px 1px 0 #fff;
266         }
267         .date, .footer {
268                 font-size: smaller;
269                 font-weight: bold;
270                 text-shadow: 1px 1px 0 #fff;
271                 opacity: 0.6;
272                 float: right;
273         }
274         .status-own .header a {
275                 float: right;
276         }
277         .status-own .header .date {
278                 float: none;
279         }
280         .footer {
281                 float: none;
282                 #display: block;
283                 text-decoration: none;
284                 padding-top: 5px;
285                 color: #404040;
286         }
287         .sep{
288                 height: 6px;
289         }
290         .rt {
291                 background-color: #404040;
292                 color: #FCFBFA;
293                 opacity: 0.6;
294                 margin-right: 2px;
295                 font-weight: bold;Goldman Sachs
296                 padding-left: 3px;
297                 padding-right: 3px;
298                 -webkit-border-radius: 3px;
299         }
300         .menu {
301                 background-color: #404040;
302                 opacity: 0.0;
303                 width: 15px;
304                 height: 15px;
305                 float: right;
306                 margin-top: -9px;
307                 margin-right: -6px;
308                 -webkit-border-radius: 3px 0 3px 0;
309         }
310         .clear {
311                 clear: both;
312         }
313         @-webkit-keyframes menu-hover {
314                 from {
315                         opacity: 0.0;
316                 }
317                 to {
318                         opacity: 0.6;
319                 }
320         }
321         .status-content:hover .menu {
322                 opacity: 0.6;
323                 -webkit-animation-name: menu-hover;
324                 -webkit-animation-duration: 1s;
325         }
326                 </style>""";
327         
328         /*
329         private string status_tpl = """
330         <div class="status{{status_state}}">
331                 <div class="left" style="background-image:url('{{user_pic}}');"></div>
332                 <div class="right">
333                         <div class="tri"></div>
334                         <div class="line"></div>
335                         <div class="status-content">
336                         <div class="header">
337                                 {{retweet}} <a href="">{{user_name}}</a>
338                                 <span class="date">about one hour ago</span>
339                         </div>
340                         <div>{{content}}</div>
341                         {{footer}}
342                         <a href=""><div class="menu"></div></a>
343                         </div>
344                 </div>
345         </div>
346         """;
347         */
348         
349         private string status_tpl = """
350         <div class="status{{status_state}}" onmouseup="menu(event, '{{account_hash}}##{{stream_hash}}##{{status_id}}');" ondblclick="reply(event, '{{account_hash}}##{{stream_hash}}##{{status_id}}');">
351                 <div class="left" style="background-image:url('{{user_pic}}');"></div>
352                 <div class="right">
353                         <div class="status-content">
354                                 <div class="header">
355                                         {{retweet}} <a href="">{{user_name}}</a>
356                                         <span class="date">{{created}}</span>
357                                 </div>
358                                 <div>{{content}}</div>
359                                 {{footer}}
360                         </div>
361                 </div>
362                 <div class="clear"></div>
363         </div>
364         """;
365         
366         private string retweet_tpl = """<span class="rt">Rt:</span>""";
367         private string footer_tpl = """<div class="sep"></div><a class="footer" href="">%s</a>""";
368         
369         private string header;
370         
371         private Regex nicks;
372         private Regex tags;
373         private Regex groups;
374         private Regex urls;
375         private Regex clear_notice;
376         
377         public Template(VisualStyle visual_style) {
378                 this.visual_style = visual_style;
379                 
380                 render_header();
381                 
382                 nicks = new Regex("(^|\\s|['\"+&!/\\(-])@([A-Za-z0-9_]+)");
383                 tags = new Regex("(^|\\s|['\"+&!/\\(-])#([A-Za-z0-9_.-\\p{Latin}\\p{Greek}]+)");
384                 groups = new Regex("(^|\\s|['\"+&!/\\(-])!([A-Za-z0-9_]+)"); //for identi.ca groups
385                 urls = new Regex("((https?|ftp)://([A-Za-z0-9+&@#/%?=~_|!:,.;-]*)([A-Za-z0-9+&@#/%=~_|$]))"); // still needs to be improved for urls containing () such as wikipedia's
386                 
387                 // characters must be cleared to know direction of text
388                 clear_notice = new Regex("[: \n\t\r♻♺]+|@[^ ]+");
389         }
390         
391         public string stream_to_list(AStream stream, string hash) {
392                 //changing locale to C
393                 string currentLocale = GLib.Intl.setlocale(GLib.LocaleCategory.TIME, null);
394                 GLib.Intl.setlocale(GLib.LocaleCategory.TIME, "C");
395                 
396                 string result = "";
397                 
398                 foreach(Status status in stream.statuses_fresh) {
399                         result += render_fresh_status(status, stream);
400                 }
401                 
402                 foreach(Status status in stream.statuses) {
403                         result += render_status(status, stream);
404                 }
405                 
406                 string main_result = main_tpl.printf(header, result);
407                 //debug(main_result);
408                 
409                 //back to the normal locale
410                 GLib.Intl.setlocale(GLib.LocaleCategory.TIME, currentLocale);
411                 
412                 return main_result;
413         }
414         
415         public void render_header() {
416                 HashMap<string, string> map = new HashMap<string, string>();
417                 map["fg_color"] = visual_style.fg_color;
418                 map["bg_color"] = visual_style.bg_color;
419                 map["bg_light_color"] = visual_style.bg_light_color;
420                 map["lk_color"] = visual_style.lk_color;
421                 header = render(header_tpl, map);
422                 //debug(header);
423         }
424         
425         public string render_fresh_status(Status status, AStream stream) {
426                 HashMap<string, string> map = new HashMap<string, string>();
427                 map["status_state"] = "-fresh";
428                 return render_status(status,stream,  map);
429         }
430         
431         public string render_status(Status status,
432                 AStream stream, HashMap<string, string> map = new HashMap<string, string>()) {
433                 
434                 Status wstatus = status;
435                 map["retweet"] = "";
436                 map["footer"] = "";
437                 
438                 if(status.retweet != null) { //if this status is retweet
439                         wstatus = status.retweet;
440                         map["retweet"] = retweet_tpl;
441                         
442                         string re_by = _("retweeted by ") + status.user.name;
443                         map["footer"] = footer_tpl.printf(re_by);
444                 }
445                 
446                 if(status.reply != null) { //if we have reply here
447                         string reply_to = _("in reply to ") + status.reply.name;
448                         map["footer"] = footer_tpl.printf(reply_to);
449                 }
450                 
451                 if(!map.has_key("status_state")) //if not fresh
452                         map["status_state"] = "";
453                 
454                 if(wstatus.own) //if it your own status
455                         map["status_state"] = "-own";
456                 
457                 if(img_cache.exist(wstatus.user.pic)) //load from cache, if exist
458                         map["user_pic"] = img_cache.download(wstatus.user.pic);
459                 else
460                         map["user_pic"] = wstatus.user.pic;
461                 
462                 map["user_name"] = wstatus.user.name;
463                 
464                 bool is_search = false;
465                 if(stream.stream_type == StreamEnum.SEARCH)
466                         is_search = true;
467                 
468                 map["created"] = time_to_human_delta(wstatus.created, is_search);
469                 
470                 map["content"] = format_content(wstatus.content, stream);
471                 
472                 //context menu data
473                 map["account_hash"] = stream.account.get_hash();
474                 map["stream_hash"] = stream.account.get_stream_hash(stream);
475                 map["status_id"] = status.id;
476                 
477                 return render(status_tpl, map);
478         }
479         
480         private string render(string text, HashMap<string, string> map) {
481                 string result = text;
482                 
483                 foreach(string key in map.keys) {
484                         var pat = new Regex("{{" + key + "}}");
485                         result = pat.replace(result, -1, 0, map[key]);
486                 }
487                 return result;
488         }
489         
490         /* Performaing to show in html context */
491         private string strip_tags_plus(owned string content) {
492                 content = content.replace("\\", "&#92;");
493                 //content = Markup.escape_text(content);
494                 content = content.replace("<", "&lt;");
495                 content = content.replace(">", "&gt;");
496                 
497                 return content;
498         }
499         
500         private string format_content(owned string data, AStream stream) {
501                 data = strip_tags_plus(data);
502                 
503                 string tmp = data;
504                 
505                 int pos = 0;
506                 while(true) {
507                         //url cutting
508                         MatchInfo match_info;
509                         bool bingo = urls.match_all_full(tmp, -1, pos, GLib.RegexMatchFlags.NEWLINE_ANY, out match_info);
510                         if(bingo) {
511                                 foreach(string s in match_info.fetch_all()) {
512                                         if(s.length > 30) {
513                                                 data = data.replace(s, "<a href='%s' title='%s'>%s...</a>".printf(s, s, s.substring(0, 30)));
514                                         } else {
515                                                 data = data.replace(s, "<a href='%s'>%s</a>".printf(s, s));
516                                         }
517                                         
518                                         match_info.fetch_pos(0, null, out pos);
519                                         break;
520                                 }
521                         } else break;
522                 }
523                 
524                 data = nicks.replace(data, -1, 0, "\\1@<a class='re_nick' href='userinfo://\\2'>\\2</a>");
525                 data = tags.replace(data, -1, 0, "\\1#<a class='tags' href='search://%s::\\2'>\\2</a>".printf(stream.account.get_stream_hash(stream)));
526                 
527                 data = groups.replace(data, -1, 0, "\\1!<a class='tags' href='http://identi.ca/group/\\2'>\\2</a>");
528                 
529                 return data;
530         }
531         
532         private string time_to_human_delta(string created, bool is_search = false) {
533                 int delta = TimeParser.time_to_diff(created, is_search);
534                 
535                 if(delta < 30)
536                         return _("a few seconds ago");
537                 if(delta < 120)
538                         return _("1 minute ago");
539                 if(delta < 3600)
540                         return _("%i minutes ago").printf(delta / 60);
541                 if(delta < 7200)
542                         return _("about 1 hour ago");
543                 if(delta < 86400)
544                         return _("about %i hours ago").printf(delta / 3600);
545                 
546                 return TimeUtils.str_to_time(created).format("%k:%M %b %d %Y");
547         }
548 }