2 layerinfo "type" = "layout";
3 layerinfo "name" = "Zesty";
4 layerinfo "lang" = "en";
5 layerinfo "author" = "Sam Angove";
6 layerinfo "author_email" = "net.rephrase@sam";
7 layerinfo "is_public" = 1;
8 layerinfo "source_viewable" = 1;
9 layerinfo "redist_uniq" = "zesty/layout";
11 ###################################################
22 # Customization/i18n Properties #
23 # ----------------------------- #
28 # ~2. Utility functions #
36 # Methods used on multiple views for getting or #
37 # printing information about entries. #
45 # Templates used on all views as well as methods #
46 # overridden by specific views. #
52 # These four views have substantially similar #
62 # These views require significant extra logic. #
63 # They are not available to free users. #
68 # Miscellaneous views #
69 # ------------------- #
70 # These views cannot print entries. #
76 ###################################################
78 ###################################################
82 ###################################################
84 # 2006-07-18 -- I can't remember, but I'm releasing it now. ;)
85 # 2006-07-10 -- English stripping
86 # 2006-07-09 -- general cleanup, fix footer
87 # 2006-07-03 -- initial build
90 ###################################################
94 ###################################################
96 # "Zesty" LiveJournal S2 style
98 # Copyright (c) 2006 Sam Angove
100 # This program is free software; you can redistribute it and/or modify
101 # it under the terms of the GNU General Public License as published by
102 # the Free Software Foundation; either version 2 of the License, or
103 # (at your option) any later version.
105 # This program is distributed in the hope that it will be useful,
106 # but WITHOUT ANY WARRANTY; without even the implied warranty of
107 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
108 # GNU General Public License for more details.
110 # You should have received a copy of the GNU General Public License along
111 # with this program; if not, write to the Free Software Foundation, Inc.,
112 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
114 ###################################################
118 ###################################################
120 # - The CSS is very messy. Haven't had time to clean it up. Sorry!
122 # - Most customization properties are for i18n purposes. Note that many of
123 # them use the `lay_string_placeholders()` function; it's not wholly
124 # satisfactory but it's more flexible than the core.
126 # - There are no props for colours, borders etc. It's a huge headache and
127 # I can't be bothered. Since this isn't a core style, no-one but a paid
128 # user can use it anyway, and they'll be able to edit the CSS directly.
130 # - I have deliberately ignored OOP and used
131 # `View::lay_print_obj(Obj o)` over `var Obj o; $o->print()`
132 # wherever possible. It's usually futile to use the latter because
133 # the method needs to be overridable in different views -- different
134 # requirements for entry printing on MonthPage and EntryPage, for example.
136 # Global functions have been avoided for the same reason.
139 ###################################################
141 # !1. # !Properties #
143 ###################################################
145 propgroup presentation {
146 property use num_items_recent;
147 property use num_items_reading;
148 property use use_journalstyle_entry_page;
149 property use tags_page_type;
150 property use use_shared_pic;
151 property use userlite_interaction_links;
152 property use entry_management_links;
153 property use comment_management_links;
156 set num_items_recent = 10;
157 set num_items_reading = 20;
160 property use text_day_next;
161 property use text_day_prev;
162 property use text_skiplinks_back;
163 property use text_skiplinks_forward;
165 property use text_permalink;
166 property use text_stickyentry_subject;
167 property use text_post_comment;
168 property use text_post_comment_friends;
170 set text_permalink = "permalink";
171 set text_stickyentry_subject = "Sticky: ";
172 set text_post_comment = "reply";
173 set text_post_comment_friends = "reply";
175 property use text_nosubject;
176 property use text_poster_anonymous;
178 set text_nosubject = "(no subject)";
179 set text_poster_anonymous = "(anonymous)";
181 property use text_meta_mood;
182 property use text_meta_music;
184 set text_meta_mood = "Mood";
185 set text_meta_music = "Music";
187 property use text_view_archive;
188 property use text_view_recent;
189 property use text_view_friends;
190 property use text_view_month;
191 property use text_view_userinfo;
193 set text_view_archive = "Calendar";
194 set text_view_recent = "Recent";
195 set text_view_friends = "Read";
196 set text_view_month = "Monthly Archive";
197 set text_view_userinfo = "Profile";
200 # For these properties I use a format vaguely similar to printf/sprintf.
201 # They're passed to a method which gives them an array of strings.
202 # you can use printf-style %s to insert the strings one at a time, or
203 # use %1, %2 .. %9 to select them by number.
205 # Example: this string "posted by %1 at %2 on %3" is passed an array
206 # containing the entry poster, the time of posting and the date of
209 # %1 will always refer to the poster, so a reformulation might be
210 # something like "at %2 on %3, %1 wrote:".
212 # It's perfectly okay to ignore some or all of the arguments. That is,
213 # there's nothing wrong with something like "I said on %3".
215 property string posted_by_at_on {
217 des = "Posted by [1:poster] at [2:time] on [3:date] string.";
219 set posted_by_at_on = "posted by %1 at %2 on %3";
221 property string posted_by_at_on_in {
223 des = "Posted by [1:poster] at [2:time] on [3:date] under [4:tags] string.";
225 set posted_by_at_on_in = "posted by %1 at %2 on %3 under %4";
227 property string posted_by_at_on_from {
229 des = "Posted by [1:poster] at [2:time] on [3:date] from [4:ip address] string.";
231 set posted_by_at_on_from = "posted by %1 at %2 on %3 from %4";
233 property string poster_in_journal {
235 des = "[1:poster] in [2:journal] string";
237 set poster_in_journal = "%1 in %2";
239 property string posted_time_format {
241 des = "[time] format for from 'posted by [poster] at [time] ...'";
243 set posted_time_format = "%%hh%%:%%min%%%%a%%m";
245 property string posted_time_format_24 {
247 des = "[time] format for from 'posted by [poster] at [time] ...'";
249 set posted_time_format_24 = "%%HH%%:%%min%%";
251 property string posted_date_format {
253 des = "[date] format for 'posted by [poster] at [time] on [date] ...'";
255 set posted_date_format = "%%dd%%/%%mm%%/%%yyyy%%";
258 # I want to have links like this:
260 # There are <a href="...">2 comments</a> on this entry.
262 # The HTML can't be part of the property, because S2 "helpfully" escapes
263 # it for me. This ugly hack is used instead, wrapping $*text_a_comment_link
264 # inside $*text_a_comment.
266 # ('There is <a href="%1">%2 comment</a> on this entry.')
267 property string text_a_comment_link { }
268 property string text_a_comment { }
269 set text_a_comment_link = "%1 comment";
270 set text_a_comment = "There is %1 on this entry.";
272 property string text_some_comments_link { }
273 property string text_some_comments { }
274 set text_some_comments_link = "%1 comments";
275 set text_some_comments = "There are %1 on this entry.";
277 property string text_some_comments_over_pages { }
278 set text_some_comments_over_pages = "There are %1 over %2 pages.";
280 property string text_no_comments {}
281 set text_no_comments = "There are no comments on this entry.";
283 property string text_comments_disabled {}
284 set text_comments_disabled = "Comments are disabled.";
286 property string text_errorpage_title {
287 des = "Error page title.";
290 set text_errorpage_title = "No content";
293 # For some reason the core only provides a message for recent and day pages.
295 property string error_monthpage_no_entries {
297 des = "Error message shown if no entries are available on a MonthPage";
299 set error_monthpage_no_entries = "No entries were posted on the selected month.";
301 property string error_yearpage_no_entries {
303 des = "Error message shown if no entries are available on a YearPage";
305 set error_yearpage_no_entries = "No entries were posted on the selected year.";
307 property string text_html_title {
308 des = "Title that goes in the HTML <title> element. Is given two parameters, global title and view title.";
310 set text_html_title = "%2 [%1]";
312 property string collapsed_entry_comments_disabled {
313 des = "String shown on a collapsed entry if comments are disabled.";
315 set collapsed_entry_comments_disabled = "(-)";
317 property string collapsed_entry_comments_max_flag {
318 des = "Passed into the comment-count string as %2 if an entry's maximum comments have been reached.";
320 set collapsed_entry_comments_max_flag = "!";
321 property string collapsed_entry_comments_screened_flag {
322 des = "Passed into the comment-count string as %3 if an entry has screened comments visible to the user.";
324 set collapsed_entry_comments_screened_flag = "*";
326 property string collapsed_entry_comments_count {
327 des = "Comment-count shown on a collapsed entry, not shown if there are no comments.";
328 note = "'%1' will be replaced by the number of comments. If the maximum number of comments
329 has been reached, %2 will contain the max flag. If there are screened comments visible to
330 the user, %3 will contain the screened flag.";
332 set collapsed_entry_comments_count = "(%1%2%3)";
334 property string linklist_default_title {
335 des = "Linklist title.";
337 set linklist_default_title = "Links";
339 property string reply_link_link_text {
340 des = "Text for the reply link.";
342 set reply_link_link_text = "Reply";
344 property string reply_link_text {
345 des = "Non-linked reply link text. %1 is replaced with the link.";
347 set reply_link_text = "(%1.)";
349 property string top_link_text {
350 des = "Text of the link to return to the top of the page.";
352 set top_link_text = "Top";
354 property use text_comment_frozen;
355 property use text_comment_parent;
356 property use text_comment_reply;
358 set text_comment_frozen = "thread is frozen";
359 set text_comment_parent = "parent";
360 set text_comment_reply = "reply";
363 property string text_comment_permalink {
364 des = "Permalink to the comment.";
366 set text_comment_permalink = "link";
368 property string text_comment_poster_is_suspended {
369 des = "Show on comments posted by suspended users.";
370 note = "Due to limitations in S2 this text will only be displayed if the comment is shown directly, i.e. as the focus of the thread.";
372 set text_comment_poster_is_suspended = "user is suspended";
374 property string text_comment_parent_entry {
375 des = "Text for linking to a comment's parent.";
377 set text_comment_parent_entry = "parent entry";
382 propgroup Miscellaneous {
384 property string custom_favicon {
385 des = "URL of custom favicon.";
386 example = "http://example.com/favicon.ico";
388 set custom_favicon = "";
390 property string default_view_mode {
391 des = "Show entries expanded or collapsed by default. Currently this setting affects the reading page only.";
392 values = "collapsed|Entries collapsed|expanded|Entries expanded";
394 set default_view_mode = "expanded";
399 # Yes, tags are enabled.
401 set tags_aware = true;
403 ###################################################
405 # !2. # Utility functions. #
407 ###################################################
410 # Converts an associative array to an argument list:
412 # var string var = {"id" => "5", "page" => "b"};
413 # lay_array_to_args($var);
417 function lay_array_to_args(string{} items) : string
418 "Converts an associative array to an argument list, i.e. {\"id\" => \"5\", \"page\" => \"b\"} => ?id=5&page=b"
423 foreach var string key ($items) {
429 $args = $args + "&";
431 $args = $args + "$key=" + $items{"$key"};
437 # pushes a string on to the end of an array, assuming that it's
438 # indexed naturally from zero.
440 function lay_array_push(string[] input, string add) : string[]
441 "Pushes a new element on to the end of an array."
443 $input[size $input] = $add;
447 # A bit like sprintf, this inserts an array of strings into a string.
448 # Knows %s, literal %%, and numbered placeholders %1 .. %9.
450 function lay_string_placeholders( string format, string[] args ) : string
451 "A bit like sprintf, this inserts an array of strings into a string.
452 Handles %s, literal %%, and numbered placeholders %1 .. %9."
454 var string output = "";
456 var bool state_found_placeholder = false;
457 var int found_count = 0;
459 foreach var string s ($format) {
460 if ( $state_found_placeholder ) {
462 $output = $output + $s;
465 elseif ( $s == "s" ) {
466 $output = $output + $args[$found_count];
468 $state_found_placeholder = false;
470 # numbered placeholder
471 elseif ( $s == "1" or $s == "2" or $s == "3" or $s == "4" or $s == "5" or $s == "6" or $s == "7" or $s == "8" or $s == "9" ) {
472 $output = $output + $args[int($s) - 1];
473 $state_found_placeholder = false;
475 } elseif ( $s == "%" ) {
476 $state_found_placeholder = true;
478 $output = $output + $s;
485 # Returns the current url plus arguments. Needs to be overridden
486 # on most views where it's used.
488 function Page::lay_build_url(string{} items) : string {
489 return $.base_url + lay_array_to_args($items);
492 # For paid user override in theme layers
493 function lay_print_extra_boxes() : void
494 "Paid users can override this in theme layers to easily add content in the 'extra boxes' section of the footer."
497 ###################################################
499 # !3. # Stylesheet. #
501 ###################################################
503 function print_stylesheet() { """
508 font-family: Verdana, sans-serif;
528 /* the main header */
532 padding: 20px 10px 20px 10px;
536 font: normal 4em Georgia, serif;
543 font: 1.2em normal Verdana, sans-serif;
547 /* the navigation menu */
550 This had to be hacked up to work with IE and I haven't gotten around
551 to cleaning it up yet. Sorry!
559 font: normal 0.6em Verdana, sans-serif;
564 padding:0px 10px 0 5px;
573 border-top: 1px solid #bbb;
579 padding:4px 12px 5px 10px;
585 text-decoration: none;
590 border-bottom: 1px solid white;
599 #navi li#tab-current {
600 border-top: 1px solid #eee;
603 #navi li#tab-current a {
611 #navi li#tab-current span {
613 border-bottom: 1px solid #eee;
617 /* back-and-forward navigation */
624 .back-forward a, .back-forward a:visited {
626 text-decoration: none;
629 .back-forward a:active,
630 .back-forward a:hover {
634 .back-forward .forward {
636 font: normal 2em Verdana, sans-serif;
639 .back-forward .back {
643 .back-forward .forward {
653 font: 0.6em normal Verdana, sans-serif;
656 padding: 10px 5px 5px 5px;
657 background-color: #fff;
665 /* extra boxes below main content */
674 list-style-type: square;
676 padding: 2px 2px 2px 10px;
680 font: normal 1.4em Verdana, sans-serif;
707 font: normal 2em Verdana, sans-serif;
708 letter-spacing: -0.1em;
716 text-decoration: none;
726 /* shared entry and comments */
730 font-weight: inherit;
738 border: 1px solid #cde;
744 border: 1px solid #dee;
748 border: 1px dashed #999;
759 margin: 0 10px 5px 10px;
760 border: 1px solid #eee;
768 letter-spacing: 0.01em;
769 margin: 10px 0 40px 0;
773 padding: 0px 10px 10px 0;
795 background-color: #def;
796 border: 1px solid #cde;
811 margin: 2px 0 2px 150px;
812 font: normal 1.4em Verdana, sans-serif;
817 /* collapsed entries */
823 .collapsed-entry .poster {
828 font: normal 1.4em Verdana, sans-serif;
833 text-decoration: none;
839 .collapsed-entry .title {
840 font: normal 1.2em Verdana, sans-serif;
841 letter-spacing: -0.1em;
865 letter-spacing: 0.01em;
881 border-bottom: 1px solid #eee;
886 font: normal 1.3em Verdana, sans-serif;
887 letter-spacing: -0.1em;
901 padding: 10px 10px 10px 0;
920 background-color: #def;
921 border: 1px solid #cde;
929 /* Collapsed comments */
934 .collapsed-comment .title {
935 font: normal 1.2em Verdana, sans-serif;
936 letter-spacing: -0.1em;
937 text-decoration: none;
940 .collapsed-comment .poster {
943 .comment-pagination {
950 .entry-comments-bar {
955 .entry-comments-bar .comments-title {
956 font: normal 1.5em Georgia, serif;
968 border: 1px solid #cde;
973 /* YearPage calendar */
985 #calendar .header a {
987 text-decoration: none;
1000 border: 1px solid #cde;
1002 .month .cell.empty {
1003 border: 1px solid #eee;
1011 .month .cell.empty .day {
1022 /* Comment quickreply */
1028 border: 0px !important;
1031 .quickreply span.de {
1038 border: 1px solid #cde;
1040 .quickreply td[align="right"] {
1045 /* TagsPage tag cloud */
1054 text-decoration: none;
1057 .module-tags_cloud li, .tags_cloud li {
1059 list-style-type: none;
1070 .sorting-options ul {
1074 .sorting-options ul li {
1078 .icons-container .icon {
1085 margin-bottom: .25em;
1098 .icon-info .default {
1099 text-decoration: underline;
1102 .icon-info .keywords ul {
1107 .icon-info .keywords ul li {
1110 padding: 0 .25em 0 0;
1113 /* ReplyPage reply box */
1116 margin: 10px 10px 10px 165px;
1122 border: 1px solid #cde;
1133 ###################################################
1137 ###################################################
1139 # Shared methods used on/for both entries and comments.
1144 # Gets the entry or comment's link icons (freeze, add to memories etc.),
1145 # with the exception of the 'nav_prev' and 'nav_next' which are handled by
1146 # Page::lay_back_forward().
1148 function EntryLite::lay_get_linkbar() : string {
1151 foreach var string k ($.link_keyseq) {
1152 if ( $k != "nav_prev" and $k != "nav_next" ) {
1153 $link = $this->get_link($k);
1154 if ( defined $link ) {
1155 $o = $o + $link->as_string();
1162 # Returns a comma-separated string of tags.
1164 function EntryLite::lay_get_tags() : string {
1165 var string tags = "";
1167 foreach var int i (0 .. (size $.tags - 1)) {
1168 var Tag t = $.tags[$i];
1170 $tags = $tags + """<a rel="tag" href="$t.url">$t.name</a>""";
1172 if ( $i < size $.tags - 1 ) {
1173 $tags = $tags + ", ";
1180 # Return a string representing the poster of this entry or comment.
1182 function Page::lay_get_poster(EntryLite e) : string {
1183 if ( not defined $e.poster ) {
1184 return $*text_poster_anonymous;
1185 } elseif ( not $e.poster->equals($e.journal) and $.view == "read" ) {
1187 # default: "%1 in %2"
1188 return lay_string_placeholders( $*poster_in_journal, [$e.poster->as_string(), $e.journal->as_string()] );
1190 return $e.poster->as_string();
1196 # Print a string containing any or all of the poster, date, time, tags
1197 # and ip address of this entry or comment.
1199 function Page::lay_print_posted_by(EntryLite e) : void {
1203 var string timeformat;
1205 if ($this.timeformat24) {
1206 $timeformat = $*posted_time_format_24;
1208 $timeformat = $*posted_time_format;
1211 # default: "posted by %1 at %2 on %3";
1212 $format = $*posted_by_at_on;
1215 $this->lay_get_poster($e),
1216 $e.time->date_format( $timeformat ),
1217 $e.time->date_format( $*posted_date_format )
1220 $tags = $e->lay_get_tags();
1221 if ( $tags != "" ) {
1223 # default: "posted by %1 at %2 on %3 under %4";
1224 $format = $*posted_by_at_on_in;
1225 lay_array_push($args, $tags);
1227 } elseif ($e.metadata{"poster_ip"}) {
1229 # default: "posted by %1 at %2 on %3 from %4";
1230 $format = $*posted_by_at_on_from;
1231 lay_array_push($args, $e.metadata{"poster_ip"});
1234 print lay_string_placeholders( $format, $args );
1237 # Prints an entry or comment's text.
1239 function Page::lay_print_text(EntryLite e) : void
1240 "Prints an entry or comment's text."
1242 println """<div class="text">""";
1250 ###################################################
1252 # ~5. # CommentInfo #
1254 ###################################################
1256 # Get details of an entry's comments.
1257 # show_read_link is included so EntryPage needn't show a link to itself.
1259 function CommentInfo::lay_get_details(int pages, bool show_read_link) : string {
1263 if ( lang_map_plural($.count) ) {
1265 $link = lay_string_placeholders($*text_some_comments_link, [string($.count)]);
1267 if ( $show_read_link ) {
1268 $link = """<a href="$.read_url">$link</a>""";
1272 $link = lay_string_placeholders($*text_some_comments_over_pages, [$link, string($pages)]);
1274 $link = lay_string_placeholders($*text_some_comments, [$link]);
1278 $link = lay_string_placeholders($*text_a_comment_link, [string($.count)]);
1279 if ( $show_read_link ) {
1280 $link = """<a href="$.read_url">$link</a>""";
1282 $link = lay_string_placeholders($*text_a_comment, [$link]);
1287 # default: "There are no comments on this entry."
1288 $link = $*text_no_comments;
1291 if (not $.enabled) {
1293 # default: "Comments are disabled."
1294 $link = $link + " " + $*text_comments_disabled;
1299 function Page::lay_print_comment_details(CommentInfo c) : void {
1300 print $c->lay_get_details(0, true);
1303 # Prints a link to an entry's ReplyPage.
1305 function Page::lay_print_entry_reply_link(CommentInfo c) : void {
1306 if ($c.show_postlink) {
1307 var string link = """<a href="$c.post_url">$*reply_link_link_text</a>""";
1308 print " " + lay_string_placeholders( $*reply_link_text, [$link] );
1313 ###################################################
1317 ###################################################
1319 function Page::lay_print_entry_linkbar(Entry e) {
1321 var string bar = $e->lay_get_linkbar();
1325 println """<div class="tools">$bar</div>""";
1328 function Page::lay_print_entry_meta(Entry e) : void {
1334 if (size $e.metadata == 0) {
1342 foreach var string k ($e.metadata) {
1344 $val = $e.metadata{$k};
1345 if ($k == "music") {
1346 $caption = $*text_meta_music;
1347 } elseif ($k == "mood") {
1348 $caption = $*text_meta_mood;
1349 if (defined $e.mood_icon) {
1351 $val = $i->as_string("'$e.metadata{$k}'")+" "+$val;
1355 <div class="meta-item"><span class="meta-label">$caption:</span> $val</div>
1364 function Page::lay_print_entry_header(Entry e) {
1365 var string subject = ($e.subject != "" ? $e.subject : $*text_nosubject);
1367 <div class="header">
1369 <h2 id="entry-$e.itemid"><a href="$e.permalink_url">$subject</a></h2>
1371 if ($e.security != "") {
1372 print """<span class="security"><img src="$e.security_icon.url" alt="[$e.security]" /></span>""";
1376 <div class="posted">"""; $this->lay_print_posted_by($e); """</div>""";
1381 function Page::lay_print_entry_left(Entry e) : void {
1383 <div class="userpic">
1386 print $e.userpic->as_string();
1393 function Page::lay_print_entry_footer(Entry e) {
1397 $this->lay_print_comment_details( $e.comments );
1398 $this->lay_print_entry_reply_link( $e.comments );
1405 function Page::lay_print_entry(Entry e) {
1410 $this->lay_print_entry_left($e);
1415 $this->lay_print_entry_header($e);
1416 $this->lay_print_text($e);
1417 $this->lay_print_entry_meta($e);
1418 $this->lay_print_entry_footer($e);
1425 function Page::print_entry(Entry e) {
1426 $this->lay_print_entry($e);
1429 function RecentPage::print_sticky_entry(StickyEntry s) {
1430 $this->lay_print_entry($s);
1433 ###################################################
1435 # ~6b. # Collapsed Entry #
1437 ###################################################
1439 # MonthPage and FriendsPage by default show only a shortened version of an
1440 # entry. I'm considering the same thing for RecentPage past a threshold --
1441 # one or two full entries followed by a longer list of previously-posted
1444 # On FriendsPage these entries can be expanded in-place; on MonthPage
1445 # the entry text isn't populated so they can only link to the full entry.
1448 # Print "expand" link.
1450 function Page::lay_print_collapsed_entry_expand(Entry e) : void {
1451 var string expand_url = $this->lay_build_url({".id" => string($e.itemid)}) + "#entry-$e.itemid";
1452 print """<span class="expand"><a href="$expand_url" title="Expand this entry.">+</a></span>""";
1457 function Page::lay_print_collapsed_entry_title(Entry e) : void {
1458 var string subject = ($e.subject != "" ? $e.subject : $*text_nosubject);
1459 print """<span class="title"><a href="$e.permalink_url" title="View this entry.">$subject</a></span>""";
1462 # Entry security unless public.
1464 function Page::lay_print_collapsed_entry_security(Entry e) : void {
1465 if ($e.security != "") {
1466 print """ <span class="security">""" + $e.security_icon->as_string() + "</span>";
1470 # Entry comment count / "comments disabled" message.
1472 function Page::lay_print_collapsed_entry_comments(Entry e) : void {
1473 var string count = "";
1474 var string max = "";
1475 var string screened = "";
1476 if ($e.comments.count > 0) {
1477 if ($e.comments.maxcomments) {
1479 $max = $*collapsed_entry_comments_max_flag;
1481 if ($e.comments.screened) {
1483 $screened = $*collapsed_entry_comments_screened_flag;
1485 # Assuming max and screened comments, default output is "(5000!*)"
1486 # Just screened comments is "(343*)" etc.
1487 $count = lay_string_placeholders( $*collapsed_entry_comments_count, [string($e.comments.count), $max, $screened] );
1488 } elseif (not $e.comments.enabled) {
1489 $count = $*collapsed_entry_comments_disabled;
1491 print """ <span class="comments">$count</span>""";
1495 function Page::lay_print_collapsed_entry_poster(Entry e) : void {
1496 if ( $.view == "read" or not $e.poster->equals($.journal as UserLite) ) {
1497 print """ — <span class="poster">""" + $this->lay_get_poster($e) + "</span>";
1501 # Print the actual entry.
1503 function Page::lay_print_collapsed_entry(Entry e) {
1505 println """<div class="collapsed-entry">""";
1507 $this->lay_print_collapsed_entry_expand($e);
1508 $this->lay_print_collapsed_entry_title($e);
1509 $this->lay_print_collapsed_entry_security($e);
1510 $this->lay_print_collapsed_entry_comments($e);
1511 $this->lay_print_collapsed_entry_poster($e);
1513 println """</div>""";
1518 ###################################################
1522 # # These methods do nothing, but are #
1523 # # overridden in child layers. #
1525 ###################################################
1527 # Show the full version of an entry, or the collapsed
1529 function Page::lay_entry_is_expanded(Entry e) : bool {
1533 # Some pages have extra content to print in the footer.
1535 function Page::lay_print_extra_box() {
1539 # Lay back-and-foward navigation. Between pages of
1540 # entries on RecentPage, between months on MonthPage, etc.
1542 function Page::lay_back_forward() : void {}
1544 # Returns a combination of page title and view title;
1545 # only used in the `<title>` element of the HTML output.
1547 function Page::title() : string {
1548 var string title = $this.global_title;
1549 var string view = $this->view_title();
1551 return lay_string_placeholders( $*text_html_title, [$title, $view] );
1555 # Prints an error page.
1557 function Page::lay_print_errorpage(string message) {
1560 <h2 class="error-header">$*text_errorpage_title</h2>
1566 # Print the current tab. Made separate so it can be overridden
1569 function Page::lay_print_navigation_current_tab() : void {
1570 println """<li id="tab-current"><span>""" + lang_viewname($.view) + "</span></li>";
1572 function Page::lay_navigation() {
1576 # Time to play "making up for S2's deficiencies"!
1577 # No link is supplied to the TagsPage yet.
1578 var string{} vu = $.view_url;
1579 var string[] vo = $.views_order;
1581 if ( $vu{"tags"} == "" ) {
1582 $vu{"tags"} = $.base_url + "/tag/";
1583 $vo[size $vo] = "tags";
1590 foreach var string v ($vo) {
1592 $this->lay_print_navigation_current_tab();
1594 println """<li><a href="$vu{$v}"><span>""" + lang_viewname($v) + "</span></a></li>";
1603 function Page::lay_print_extra_box_open(string title) : void {
1604 var string alt = alternate("odd", "even");
1606 <div class="extra-box $alt">
1607 <h2 class="title">$title</h3>
1611 function Page::lay_print_extra_box_close() : void {
1615 # Prints a linklist. More complicated than it'd normally be because the
1616 # style splits a list with headings into multiple lists. Sub-lists are
1617 # not supported because they're not implemented in the core yet and show
1618 # no signs of ever being so.
1620 function Page::print_linklist() {
1621 if ( size $.linklist == 0 ) {
1624 var bool open = false;
1625 var UserLink l = $.linklist[0];
1627 if (not $l.is_heading) {
1628 $this->lay_print_extra_box_open( $*linklist_default_title );
1630 <ul class="linklist">
1635 foreach var UserLink l ($.linklist) {
1636 if ($l.is_heading) {
1641 $this->lay_print_extra_box_close();
1644 $this->lay_print_extra_box_open($l.title);
1646 <ul class="linklist">
1651 <li><a href="$l.url">$l.title</a></li>
1660 $this->lay_print_extra_box_close();
1666 # Print one week in a calendar month.
1668 function Page::lay_print_week(YearWeek w) : void {
1672 if ($w.pre_empty > 0) {
1673 foreach var int i (1..$w.pre_empty) {
1675 <td class="blank cell"> </td>
1679 foreach var YearDay d ($w.days) {
1680 if ($d.num_entries > 0) {
1682 <td class="full cell">
1683 <span class="day">$d.day</span>
1684 <div class="count"><a href="$d.url">$d.num_entries</a></div>
1689 <td class="empty cell">
1690 <span class="day">$d.day</span>
1691 <div class="count"> </div>
1696 if ($w.post_empty > 0) {
1697 foreach var int i (1..$w.post_empty) {
1699 <td class="blank-cell"> </td>
1708 # Print a calendar month.
1710 function Page::lay_print_month(YearMonth m) {
1712 <table summary="Monthly calendar with links to each day's entries" class="month">
1715 foreach var int d (weekdays()) {
1716 """<th class="weekday">$*lang_dayname_short[$d]</th>""";
1721 foreach var YearWeek w ($m.weeks) {
1722 $this->lay_print_week($w);
1731 function Page::lay_header() {
1736 var string subtitle;
1737 if ($.global_subtitle != "") {
1738 $subtitle = $this.global_subtitle + ". " + $this->view_title() + ".";
1740 $subtitle = $this->view_title() + ".";
1743 <h1>$.global_title</h1>
1750 function Page::lay_print_mini_calendar_box() {
1751 var YearMonth m = $this->get_latest_month();
1752 if ( defined $m and $m.has_entries ) {
1753 $this->lay_print_extra_box_open( $m->month_format("%%month%%") );
1754 $this->lay_print_month($m);
1755 $this->lay_print_extra_box_close();
1759 function Page::lay_footer() {
1764 $this->lay_print_extra_box();
1765 $this->print_linklist();
1766 $this->lay_print_mini_calendar_box();
1767 lay_print_extra_boxes();
1772 <p><span class="top-link"><a href="#header">$*top_link_text</a></span> <a href="http://www.dreamwidth.org/customize/advanced/layerbrowse.bml?id=zesty/layout">Zesty</a>. """; server_sig(); """</p>
1778 function Page::print() {
1781 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
1782 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-loose.dtd">
1783 <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
1785 <title>"""+$this->title()+"""</title>
1787 $this->print_head();
1789 <link rel="stylesheet" type="text/css" href="$.stylesheet_url" />
1791 if ($*custom_favicon != "") {
1792 """<link rel="shortcut icon" href="$*custom_favicon" />""";
1796 <body class="$.journal.username-$.view">
1798 $this->print_control_strip();
1799 $this->lay_header();
1800 $this->lay_navigation();
1802 $this->print_body();
1804 $this->lay_footer();
1812 ###################################################
1814 # ~8. # RecentPage #
1816 ###################################################
1819 function RecentPage::lay_build_url(string{} items) : string {
1820 if ($.nav.skip != 0) {
1821 $items{"skip"} = string($.nav.skip);
1823 return $.base_url + lay_array_to_args($items);
1826 function RecentPage::lay_back_forward() : void {
1828 if ($.nav.backward_url == "" and $.nav.forward_url == "") {
1832 <div class="back-forward">
1834 if ($.nav.backward_url != "") {
1835 var string previous = get_plural_phrase($.nav.backward_count, "text_skiplinks_back");
1838 <a href="$.nav.backward_url" title="$previous">←</a>
1842 if ($.nav.forward_url != "") {
1843 var string next = get_plural_phrase($.nav.forward_count, "text_skiplinks_forward");
1845 <div class="forward">
1846 <a href="$.nav.forward_url" title="$next">→</a>
1856 function RecentPage::print_body() {
1857 $this->lay_back_forward();
1861 if (size $.entries == 0) {
1862 $this->lay_print_errorpage($*text_noentries_recent);
1864 foreach var Entry e ($.entries) {
1865 $this->print_entry($e);
1871 $this->lay_back_forward();
1875 ###################################################
1877 # ~9. # FriendsPage #
1879 ###################################################
1882 function FriendsPage::lay_build_url(string{} items) : string {
1883 var string url = $.base_url;
1885 # Page might be "friendsfriends".
1887 if ($.friends_mode != "") {
1888 $url = $url + "/$.friends_mode";
1890 $url = $url + "/read";
1893 # Page might be a friends group.
1895 if ($.filter_active) {
1896 $url = $url + "/$.filter_name";
1899 if ($.nav.skip != 0) {
1900 $items{"skip"} = string($.nav.skip);
1902 return $url + lay_array_to_args($items);
1905 # Is the default view mode for entries "expanded" or "collapsed"?
1906 # Currently only actually used on the FriendsPage.
1908 function FriendsPage::lay_get_current_view_mode() : string {
1909 var string mode = $*default_view_mode;
1910 if ($.args{"mode"} != "" and $mode != $.args{"mode"}) {
1911 $mode = $.args{"mode"};
1916 # Returns the opposite of the current view mode for entries.
1918 function FriendsPage::lay_get_alternate_view_mode() : string {
1919 var string current = $this->lay_get_current_view_mode();
1920 if ( $current == "expanded" ) {
1927 function FriendsPage::lay_entry_is_expanded(Entry e) : bool {
1928 var bool expanded = false;
1930 # We expand entries if they match the `.id=$entry_id` argument, but
1931 # there's a problem in that the item id isn't necessarily unique.
1932 # Because LJ uses (internally) a composite key of user id and item id,
1933 # two different users' posts on the same page could have the same item id.
1935 # I mention it mostly out of interest, since I'm not going to do anything
1936 # to stop it happening.
1938 # It's vanishingly unlikely, S2 doesn't expose the user ID (and the
1939 # username isn't necessarily unique *or* safe), and even if there's a
1940 # clash, what's the damage? But it might still happen at some point.
1942 # It's not quite as simple as the birthday paradox, because low item ids
1943 # are exponentially more likely to appear than high ones, and people who
1944 # joined the site at the same time and have similar posting habits are
1945 # quite likely to stay in the same range of item ids.
1947 # This comment was much too long. Sorry!
1949 if ($.args{"id"} != "" and int($.args{"id"}) == $e.itemid) {
1951 } elseif ($this->lay_get_current_view_mode() == "expanded") {
1961 # The FriendsPage tab has an extra mode-switching button on it.
1963 function FriendsPage::lay_print_navigation_current_tab() : void {
1964 var string alt = $this->lay_build_url( {".mode" => $this->lay_get_alternate_view_mode()} );
1965 println """<li id="tab-current"><span>""" + lang_viewname($.view) + """ <a href="$alt">+</a></span></li>""";
1968 function FriendsPage::print_entry(Entry e) {
1970 print """<div class="new-day">""" + $e.time->date_format($*lang_fmt_date_long) + "</div>";
1972 if ( $this->lay_entry_is_expanded($e) ) {
1973 $this->lay_print_entry($e);
1975 $this->lay_print_collapsed_entry($e);
1981 ###################################################
1985 ###################################################
1987 function DayPage::lay_back_forward() : void {
1989 if ($.prev_url == "" and $.next_url == "") {
1993 <div class="back-forward">
1995 if ($.prev_url != "") {
1998 <a href="$.prev_url" title="$*text_day_prev">←</a>
2002 if ($.next_url != "") {
2004 <div class="forward">
2005 <a href="$.next_url" title="$*text_day_next">→</a>
2015 function DayPage::print_body() {
2017 if (not $.has_entries) {
2018 $this->lay_print_errorpage($*text_noentries_day);
2020 foreach var Entry e ($.entries) {
2021 $this->print_entry($e);
2028 ###################################################
2030 # ~11. # MonthPage #
2032 ###################################################
2034 # Can't expand MonthPage entries. Bah humbug.
2036 function MonthPage::lay_print_collapsed_expand(Entry e) : void {
2040 function MonthPage::lay_back_forward() : void {
2042 if ($.prev_url == "" and $.next_url == "") {
2046 <div class="back-forward">
2048 if ($.prev_url != "") {
2051 <a href="$.prev_url" title="Previous day.">←</a>
2055 if ($.next_url != "") {
2057 <div class="forward">
2058 <a href="$.next_url" title="Next day.">→</a>
2067 # Can't get entry text in this view, so no full entries possible.
2069 function MonthPage::print_entry(Entry e) : void {
2070 return $this->lay_print_collapsed_entry($e);
2075 # Print a box containing information about other linkable months.
2077 function MonthPage::lay_print_extra_box() : void {
2078 if (size $.months == 0) {
2083 <div class="extra-box">
2084 <h3 class="title">$.date.year</h3>
2088 foreach var MonthEntryInfo m ($.months) {
2089 if ($.date.year == $m.date.year) {
2090 println """<li><a href="$m.url">"""+$m.date->date_format("%%month%%")+"</a></li>";
2102 function MonthPage::print_body {
2103 var bool any = false;
2104 $this->lay_back_forward();
2108 foreach var MonthDay d ($.days) {
2109 if ($d.has_entries) {
2110 print """<div class="new-day">""" + $d.date->date_format($*lang_fmt_date_long) + "</div>";
2111 foreach var Entry e ($d.entries) {
2112 $this->print_entry($e);
2121 # default: "No entries were posted on the selected month."
2122 return $this->lay_print_errorpage( $*error_monthpage_no_entries );
2124 $this->lay_back_forward();
2129 ###################################################
2131 # ~12. # EntryPage #
2133 ###################################################
2135 # TODO: this is broken in the core.
2136 # Waiting on http://rt.livejournal.org/Ticket/Display.html?id=1266 .
2138 function EntryPage::lay_comment_poster_is_suspended(Comment c) : bool {
2139 return $.viewing_thread and not $c.full and $c.depth == 1;
2142 function EntryPage::lay_print_comment_details(CommentInfo c) : void {
2143 print $c->lay_get_details($.comment_pages.total, false);
2146 # "Ideally layouts should never override this"... well how about you
2147 # actually make it work on all views, then?
2148 function EntryPage::view_title() : string {
2149 var string subject = ($.entry.subject != "" ? $.entry.subject : $*text_nosubject);
2150 if ( $.viewing_thread ) {
2151 $subject = lay_string_placeholders( "%1 : comments", [$subject] );
2156 function EntryPage::lay_back_forward() : void {
2157 var Link prev = $.entry->get_link("nav_prev");
2158 var Link next = $.entry->get_link("nav_next");
2160 if ( isnull $prev and isnull $next ) {
2164 <div class="back-forward">
2166 if ( defined $prev ) {
2169 <a href="$prev.url" title="$prev.caption">←</a>
2173 if ( defined $next ) {
2175 <div class="forward">
2176 <a href="$next.url" title="$next.caption">→</a>
2187 function EntryPage::lay_print_comment_linkbar(Comment c)
2188 "Same as Page::lay_print_entry_linkbar except that it also prints
2189 the multiform checkbox if the multiform is on. "
2191 var string bar = $c->lay_get_linkbar();
2193 if ( $bar == "" and not $.multiform_on ) {
2197 print """<div class="tools" id="tools$c.talkid">$bar""";
2199 if ($.multiform_on) {
2200 $c->print_multiform_check();
2207 function EntryPage::lay_print_comment_links(Comment c) : void {
2209 println """<div class="links">""";
2211 if ( $.viewing_thread and $c.depth == 1 ) {
2212 """[<a href="$.entry.permalink_url">$*text_comment_parent_entry</a>] """;
2215 """[<a href="$c.permalink_url">$*text_comment_permalink</a>] """;
2217 if ( $c.parent_url != "" ) {
2218 """[<a href="$c.parent_url">$*text_comment_parent</a>] """;
2222 """[$*text_comment_frozen]""";
2223 } elseif ( $this->lay_comment_poster_is_suspended($c) ) {
2224 """[$*text_comment_poster_is_suspended]""";
2226 """["""; $c->print_reply_link({"linktext" => $*text_comment_reply}); """]""";
2229 println """</div>""";
2233 function EntryPage::print_comment(Comment c) : void {
2235 var string class = "comment " + alternate("odd", "even");
2236 var string subject = ($c.subject == "") ? $*text_nosubject : $c.subject;
2238 var string state = "state";
2241 } elseif ($c.screened) {
2242 $state = "screened";
2245 println """<div class="nest">""";
2247 # This "state" div is a dodgy hack for the JavaScript set_handler stuff.
2248 # It'll be changed to "frozen", "screened" etc. if the quick-change buttons
2252 <div class="$state" id="state$c.talkid">
2253 <div class="$class" id="$c.dom_id">
2257 """<div class="userpic">""";
2258 print $c.userpic->as_string();
2261 """<div class="userpic empty"> </div>""";
2263 $this->lay_print_comment_linkbar($c);
2268 <div class="header">
2270 <h3 id="t$c.talkid"><a href="$c.permalink_url">$subject</a></h2>
2272 if (defined $c.subject_icon) {
2273 print """<span class="icon">""" + $c.subject_icon->as_string() + "</span>";
2277 <div class="posted">"""; $this->lay_print_posted_by($c); """</div>
2280 $this->lay_print_text($c);
2281 $this->lay_print_comment_links($c);
2288 $c->print_reply_container({"class" => "quickreply"});
2290 <div id="c-reply-$c.talkid"></div>
2293 $this->print_comments($c.replies);
2298 function EntryPage::print_comment_partial(Comment c) {
2300 var string poster = defined $c.poster ? $c.poster->as_string() : $*text_poster_anonymous;
2301 var string subject = $c.subject != "" ? $c.subject : $*text_nosubject;
2305 <div class="collapsed-comment">
2308 """<span class="title">(deleted comment)</span>""";
2309 } elseif ( $c.screened ) {
2311 <a class="title" href="$c.permalink_url">$subject</a> — <span class="poster">$poster</span> [screened]
2315 <a class="title" href="$c.permalink_url">$subject</a> — <span class="poster">$poster</span>
2321 $this->print_comments($c.replies);
2329 function EntryPage::print_comments(Comment[] comments) {
2330 if (size $comments == 0) {
2334 foreach var Comment c ($comments) {
2336 $this->print_comment($c);
2338 # special case for suspended comments.
2339 elseif ( $this->lay_comment_poster_is_suspended($c) ) {
2340 $this->print_comment($c);
2342 $this->print_comment_partial($c);
2349 function EntryPage::lay_print_entry_left(Entry e) : void {
2351 <div class="userpic">
2354 print $e.userpic->as_string();
2359 $this->lay_print_entry_linkbar($.entry);
2361 function EntryPage::lay_print_entry_footer(Entry e) {
2365 function EntryPage::print_entry(Entry e) {
2366 $this->lay_print_entry($e);
2369 function EntryPage::lay_comment_pagination() : void {
2372 if ($.entry.comments.count == 0) {
2376 var ItemRange range = $.comment_pages;
2378 # only one page of comments
2379 if ($range.all_subitems_displayed) {
2384 <div class="comment-pagination">
2387 if ( $range.url_last != "" ) {
2388 """<a href="$range.url_last">←</a>""";
2390 foreach var int page (1 .. $range.total) {
2391 if ($range.current != $page) {
2392 """ <a href='""" + $range->url_of($page) + """'>$page</a> """;
2397 if ( $range.url_next != "" ) {
2398 """<a href="$range.url_next">→</a>""";
2406 function EntryPage::lay_print_comments() : void {
2408 if ( $.entry.comments.enabled and size $.comments > 0 ) {
2410 # JavaScript voodoo.
2413 set_handler("screen_comment_#", [
2414 [ "set_class", "state#", "screened" ]
2416 set_handler("freeze_comment_#", [
2417 [ "set_class", "state#", "frozen" ]
2419 set_handler("unscreen_comment_#", [
2420 [ "set_class", "state#", "state" ]
2422 set_handler("unfreeze_comment_#", [
2423 [ "set_class", "state#", "state" ]
2430 if ($.multiform_on) {
2431 $this->print_multiform_start();
2434 $this->print_comments($.comments);
2436 if ($.multiform_on) {
2438 <div id="multiform">
2440 $this->print_multiform_actionline();
2441 $this->print_multiform_end();
2454 function EntryPage::lay_print_entry_comments_bar() : void {
2456 <div class="entry-comments-bar">
2457 <span class="comments-title">""";
2458 $this->lay_print_comment_details( $.entry.comments );
2459 $this->lay_print_entry_reply_link( $.entry.comments );
2461 """; $this->lay_comment_pagination(); """
2466 function EntryPage::print_body() : void {
2468 if ( $.viewing_thread ) {
2469 return $this->lay_print_comments();
2472 $this->lay_back_forward();
2476 $this->print_entry($.entry);
2480 $this->lay_back_forward();
2482 $this->lay_print_entry_comments_bar();
2484 $this->lay_print_comments();
2486 # Show comment pagination again if necessary.
2487 if ( $.comment_pages.total > 1 ) {
2488 $this->lay_print_entry_comments_bar();
2497 ###################################################
2499 # ~13. # ReplyPage #
2501 ###################################################
2504 function ReplyPage::lay_print_comment(EntryLite e) {
2506 var string subject = ($e.subject != "" ? $e.subject : $*text_nosubject);
2509 <div class="comment">
2511 <div class="userpic">
2514 print $e.userpic->as_string();
2520 <div class="header">
2522 <h2><a href="$e.permalink_url">$subject</a></h2>
2524 <div class="posted">"""; $this->lay_print_posted_by($e); """</div>
2527 $this->lay_print_text($e);
2529 <div class="links">[<a href="$.entry.permalink_url">parent entry</a>] [<a href="$e.permalink_url">$*text_permalink</a>]</div>
2535 function ReplyPage::print_body() : void {
2537 # replying to a comment, not an entry.
2542 # replying to an entry?
2543 if ( $this.replyto.depth == 0 ) {
2544 $this->print_entry($.entry);
2546 # no, it's a comment
2548 $this->lay_print_comment($.replyto);
2562 ###################################################
2566 ###################################################
2568 # Prints a list of other linkable years.
2570 function YearPage::lay_print_extra_box() {
2572 if (size $.years < 2) {
2577 <div class="extra-box">
2578 <h3 class="title">Years</h3>
2581 foreach var YearYear y ($.years) {
2583 $year = string($y.year);
2585 $year = """<a href="$y.url">$y.year</a>""";
2587 println """<li>$year</li>""";
2595 function YearPage::lay_back_forward() : void {
2596 if (size $.years < 2) {
2603 foreach var YearYear y ($.years) {
2604 if ( $y.year == $.year - 1 ) {
2606 } elseif ( $y.year == $.year + 1 ) {
2612 <div class="back-forward">
2614 if ( defined $last ) {
2617 <a href="$last.url" title="Previous year.">←</a>
2621 if ( defined $next ) {
2623 <div class="forward">
2624 <a href="$next.url" title="Next year.">→</a>
2635 function YearPage::print_month(YearMonth m) {
2637 <div class="calendar-month">
2638 <h2 class="title"><a href="$m.url">"""+$m->month_format("%%month%%")+"""</a></h2>
2640 $this->lay_print_month($m);
2647 function YearPage::print_body {
2648 if ( size $.months == 0 ) {
2649 return $this->lay_print_errorpage( $*error_yearpage_no_entries );
2652 $this->lay_back_forward();
2656 foreach var YearMonth m ($.months) {
2657 if ($m.has_entries) {
2658 $this->print_month($m);
2664 $this->lay_back_forward();
2667 ###################################################
2669 # ~15. # MessagePage #
2671 # # Just a stub. AFAICT it's not used in the #
2672 # # core yet, so I can't test it. #
2674 ###################################################
2676 function MessagePage::print_body() {
2679 <p>"""; $this->print_message(); """</p>
2684 ###################################################
2688 ###################################################
2690 # Weighted tag cloud / heatmap.
2692 function TagsPage::print_body() {
2693 # since there is no heading, make invisible one here for
2696 <div id="tag-cloud" class="tags_cloud">
2697 <h2 class="invisible">Visible tags</h2>
2701 # font min and max as % values
2702 var int fontmin = 80;
2703 var int fontmax = 400;
2704 var int fontspread = $fontmax - $fontmin;
2706 var int fontstep = 0;
2709 var int countspread;
2713 var int highest = 0;
2714 var int lowest = 999999;
2717 foreach var TagDetail tag ($.tags) {
2718 if ($tag.use_count > $highest) {
2719 $highest = $tag.use_count;
2721 if ($tag.use_count < $lowest) {
2722 $lowest = $tag.use_count;
2726 $countspread = $highest - $lowest;
2728 if ($countspread > 0) {
2729 $fontstep = $fontspread/$countspread;
2732 foreach var TagDetail tag ($.tags) {
2733 if ($highest == $lowest) {
2734 $font = string($fontmin) + "%";
2736 $fontsize = $fontmin + (($tag.use_count - $lowest) * $fontstep);
2737 $font = string($fontsize) + "%";
2742 <a rel="tag" class="used-$tag.use_count visible-to-$tag.visibility" href="$tag.url" style="font-size: $font;">$tag.name</a>
2743 <span class ="invisible"> used $tag.use_count times</span></li>