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