# -*-s2-*-

##[ layerinfo ]

layerinfo "type" = "core";
layerinfo "name" = "LiveJournal S2 Core, v1";
layerinfo "redist_uniq" = "core1";
layerinfo "majorversion" = "1";
layerinfo "author_name" = "LiveJournal Webmaster";
layerinfo "author_email" = "webmaster@livejournal.com";

##[ S2 core classes ]

class int
"An integer number.  This isn't really a class, as suggested by its lower-case name.  Parameters of type int pass by value, unlike all variables of real object types, which pass by reference.  Instead, this is just a pseudo-class which provides convenience methods on instances of integers.  The other pseudo-class is [class[string]]."
{
    function builtin zeropad(int digits) : string
    "Return the integer as a string formatted at least \$digits characters long, left-padded with zeroes.";
}

class string
"A series of characters.  This isn't really a class, as suggested by its lower-case name.  Parameters of type string pass by value, unlike all variables of real object types, which pass by reference.  Instead, this is just a pseudo-class which provides convenience methods on instances of strings.  The other pseudo-class is [class[int]]."
{
    function builtin substr(int start, int length) : string
    "Returns up to \$length characters from string, skipping \$start characters from the beginning.";

    function builtin ends_with (string sub) : bool
    "Returns true if string ends in \$sub";

    function builtin starts_with (string sub) : bool
    "Returns true if string begins with \$sub";

    function builtin contains (string sub) : bool
    "Return true if string contains \$sub";

    function builtin lower : string
    "Returns string in lower case.";

    function builtin upper : string
    "Returns string in upper case";

    function builtin upperfirst : string
    "Return string with the first character capitalized.";

    function builtin length() : int
    "Return the number of characters in the string.";

    function builtin repeat(int n) : string
    "Returns the string repeated n times";
}

class Color
"Represents a color."
{
  var readonly int r "Red value, 0-255.";
  var readonly int g "Green value, 0-255.";
  var readonly int b "Blue value, 0-255.";
  var  string as_string "HTML hex encoded: #rrggbb";
  function builtin Color(string s) : Color "Constructor for color class.  Lets you make a Color object from a string of form #rrggbb";
  function builtin set_hsl (int h, int s, int v) "Set the HSL value for a color class.";

  function builtin red(int r) "Set the red value. (0-255)";
  function builtin green(int g) "Set the green value. (0-255)";
  function builtin blue(int b) "Set the blue value. (0-255)";
  function builtin red() : int "Get the red value.";
  function builtin green() : int "Get the green value.";
  function builtin blue() : int "Get the blue value.";

  function builtin hue(int h) "Set the hue value. (0-255)";
  function builtin saturation(int s) "Set the saturation value. (0-255)";
  function builtin lightness(int v) "Set the lightness value. (0-255)";
  function builtin hue() : int "Get the hue value. (0-255)";
  function builtin saturation() : int "Get the saturation value. (0-255)";
  function builtin lightness() : int "Get the lightness value. (0-255)";

  function builtin clone() : Color "Returns identical color.";
  function builtin lighter() : Color "Returns a new color with lightness increased by 30.";
  function builtin lighter(int amt) : Color "Returns a new color with lightness increased by amount given.";
  function builtin darker() : Color "Returns a new color with lightness decreased by 30.";
  function builtin darker(int amt) : Color "Returns a new color with lightness decreased by amount given.";
  function builtin inverse() : Color "Returns inverse of color.";
  function builtin average(Color other) : Color "Returns color averaged with \$other color.";
}


##[ site-core classes ]

class Date
"Represents a date."
{
    var int year "Year; 4 digits.";
    var int month "Month; 1-12.";
    var int day "Day; 1-31.";

    function builtin day_of_week() : int
    "Returns the day of the week this date falls on, from Sunday=1 to Saturday=7";

    function builtin date_format () : string
    "Returns date formatted as normal.  // SeeAlso: siteapi.core1.dateformats";

    function builtin date_format (string fmt) : string
    "Returns date formatted as indicated by \$fmt.  One of: short, med, long, med_day, long_day.  Or a custom format.  Default is 'short'. // SeeAlso: siteapi.core1.dateformats";
}

class DateTime extends Date
"Represents both a date and time."
{
    var int hour "Hour; 0-23.";
    var int min "Minute; 0-59.";
    var int sec "Second; 0-59.";

    function builtin time_format () : string
    "Returns time formatted as normal.  // SeeAlso: siteapi.core1.dateformats";

    function builtin time_format (string fmt) : string
    "Returns time formatted as indicated by \$fmt, or normal if blank.  // SeeAlso: siteapi.core1.dateformats";
}

class Image
"Represents an image."
{
    var readonly string url "URL of the image";
    var int width "Width in pixels";
    var int height "Height in pixels";
    var string alttext "Default alternative text for image";

    function builtin set_url (string url)
    "Sets the URL, doing any necessary escaping.";

    function print ()
    "Print an HTML tag for this Image";

    function print (string alttext)
    "Print an HTML tag for this Image with given alttext";

    function print (string{} opts)
    "Print the HTML for an image, Supported keys are 'href' to create a link to the image source
     and 'a_attr' which adds attributes to the anchor tag if a link is to be printed.";

    function as_string () : string
    "Return the HTML tag for this image";

    function as_string (string alttext) : string
    "Return an HTML tag for this Image with given alttext";

    function as_string (string{} opts) : string
    "Return the HTML for an image, Supported keys are 'href' to create a link to the image source
     and 'a_attr' which adds attributes to the anchor tag if a link is to be printed.";
}

class Link
"A link or button"
{
    var readonly string url "URL which the link points to";
    var readonly string caption "The caption for the link";
    var Image icon 
    "A suggestion from the server as to which icon to use. layouts/users can override this of course. 
    alt text works similarly to [member[Link.caption]].";

    function print_button
    "Output this Link as a clickable button using [member[Link.icon]]";

    function as_string() : string
    "Return the button HTML link.";
}

class ItemRange
"Represents a range of items which optionally contain items."
{
  var bool all_subitems_displayed "True if the subitems in this range represent the entire set. In this case, all of the URL members are blank.";
  var int num_subitems_displayed "The number of subitems in this range.";
  var int total "The total number of items that are navigable to.";
  var int current "The currently-active item.";
  var int from_subitem "The index of the first subitem in this range.";
  var int to_subitem "The index of the last subitem in this range.";
  var int total_subitems "The number of subitems.";
  var readonly string url_next "URL for the 'next' link.  Blank if there isn't a next URL."; 
  var readonly string url_prev "URL for the 'previous' link.  Blank if there isn't a previous URL.";
  var readonly string url_first "URL for the 'first' link.  Blank if already on the first page.";
  var readonly string url_last "URL for the 'last' link.  Blank if already on the last page.";
  function builtin url_of(int n) : string "Returns the URL to use to link to the nth item";

  function print () "Prints the item range links";
  function print(string labeltext) "Prints the item range links with the given \$labeltext";
}

### LJ Specific Classes

class CommentInfo
"Information about comments attached to something."
{
    var readonly string read_url "URL pointer to the 'Read Comments' view.";
    var readonly string post_url "URL pointer to the 'Post Comments' view.";
    var int count "Current number of comments available to be read by the viewer.";
    var bool screened "Set to true if there are screened comments and remote user can unscreen them.";
    var bool enabled "Set to false if comments disabled journal-wide or just on this item.";
    var bool maxcomments "Set to true if entry has reached a comment maximum.";

    function print
    "Print all comment related links";
    function print_readlink
    "Print the formatted link to the 'Read Comments' view";
    function print_postlink
    "Print the formatted link to the 'Post Comments' view";
}

class UserLink
"A user-defined link to an outside resource."
{
    var readonly bool is_heading "Is this link a heading or category name?  If so, it has no url and a list of children.";
    var readonly string title "The title or label for the link";
    var readonly string url "The url to which the link points";
    var readonly UserLink[] children "Not Implemented: An array of child UserLink objects.";
}

class UserLite
"A 'lite' version of a [class[User]] which the system often has more readily-available than a full version."
{
    var readonly string username "Canonical Username, ex: johnqpub.  Note that if journal_type is an external identity, there will be no username, so this field will be a display version of their URL, longer than 15 characters, and with characters other than a-z, 0-9 and underscore.";
    var readonly string name "User's formatted name, ex: John Q. Public";
    var readonly string journal_type "Type of account: P (personal), C (community), Y (syndicated), S (shared), I (external identity) etc";

    function builtin equals(UserLite u) : bool "Returns true if the two user objects refer to the same user. Use this rather than comparing usernames, since usernames aren't globally unique.";
    function base_url () : string "Returns URL of user's journal.";
    function tag_manage_url () : string "Returns URL to user's tag management page.";
    function as_string() : string;
    function print ();
}

class Tag
"Represents a tag."
{
    var readonly string name "Textual representation of this tag.";
    var readonly string url "URL to view entries with this tag.";
}

class EntryLite
"Base class for both journal entries and comments."
{
    var readonly string subject "Subject.  May contain HTML.  Don't do substring chops on this.";
    var readonly string text "Text (will be blank on some viewtypes, like 'month' view, or with collapsed threads)";
    var DateTime time "The user-specified time of the post, or the GMT time if it's a comment.";

    var UserLite poster "Author of the entry, or null if an anonymous comment";
    var UserLite journal "Journal the entry has been posted to";

    var readonly Tag[] tags "Array of tags applied to this entry.";

    var Image userpic "The userpic selected to relate to this entry.";

    var readonly string{} metadata "Post metadata. Keys: 'music', 'mood'";

    var readonly string dom_id "The DOM 'id' attribute you should put on your outer-most element";

    var readonly string permalink_url "A URL at which this specific entry can be viewed, for linking purposes.";
    var int depth "Visual depth of entry.  Top-level journal entries are always depth zero.  Comments have a depth greater than or equal to one, depending on where the thread is rooted at.";

    var string[] link_keyseq "An array of keys which you should pass to [method[EntryLite.get_link(string)]] to produce an entry 'toolbar'. Does not contain nav_next and nav_prev for entries; you should retrieve those separately and put them somewhere appropriate for your layout.";

    function builtin get_link (string key) : Link "Get a link to some action related to this entry or comment. You can iterate over [member[EntryLite.link_keyseq]] to get keys to pass in here to produce a 'toolbar' of links.";
    function builtin get_plain_subject () : string "For Entries that can contain HTML subjects, this returns the subjeft without HTML.  Comments can't have HTML in subjects, so this is equivalent to just using \$.subject.  The returned 'plain' subject may still contain HTML entities, so don't do substring chops on it either.";
    function builtin get_tags_text () : string "Returns a string containing a div of class 'ljtags' with the tags for the entry.  If there are no tags on the entry, returns a blank string.  The string is formatted according to the 'text_tags' property.";
    function print_linkbar() "Print the link bar for this entry or comment.";
}

class Entry extends EntryLite
"A journal entry"
{
    var readonly string security "The security level of the entry ('private', 'protected'), or blank if public.";
    var Image security_icon "A little icon which should be displayed somewhere on an entry to represent the security setting";

    var Image mood_icon "Mood icon, or null.";
    var CommentInfo comments "Comment information on this entry";

    var bool new_day "Is this entry on a different day to the previous one?";
    var bool end_day "Is this the last entry of a day?";

    var int itemid "Server stored ID number for this entry";

    function print_metadata ();
    function builtin plain_subject () : string
        "Return entry's subject as plain text, with all HTML removed.";

    function print_link_next() "Print the link to the next entry in this journal.";
    function print_link_prev() "Print the link to the previous entry in this journal.";
}

class Comment extends EntryLite
"A comment to a journal entry, or to another comment."
{
    var Image subject_icon "Subject icon, or null.";
    var int talkid "Server stored ID number for this comment.";
    var Comment[] replies "Comments replying to this comment.";
    var bool full "True if all information is available for this comment.  False if only the subject, poster, and date are available.  (collapsed threads)";
    var readonly string parent_url "URL to parent comment, or blank if a top-level comment.";
    var readonly string reply_url "URL to reply to this comment.";
    var readonly string thread_url "URL to view threaded rooted at this comment, or blank if comment has no children.";
    var readonly bool screened "True if comment is in screened state.";
    var readonly bool frozen "True if comment is in frozen state.";
    var readonly string anchor "Direct link to comment, via HTML name anchors";

    function builtin print_multiform_check "Prints the select checkbox in CSS class 'ljcomsel' with DOM id 'ljcomsel_\$talkid' for a multi-action form started with [method[EntryPage.print_multiform_start()]].";
}
 
### Userinfo

class Friend extends UserLite 
"Represents a friends or friendof list"
{
    var Color bgcolor "Background color selected for friend";
    var Color fgcolor "Foreground color selected for friend";
}

class User extends UserLite 
"A more information-rich userinfo structure"
{
    var Image default_pic "Information about default userpic";
    var readonly string userpic_listing_url "URL of a page listing this user's userpics";
    var readonly string website_url "URL pointer to user's website";
    var readonly string website_name "'pretty' name of user's website";
}

### Other

class Redirector
"A redirector makes either a GET URL which redirects to a pretty URL or an HTML form which posts to a URL that redirects to a pretty URL.  This class exists because it's often desirable to use a form to end up at a URL, instead of doing a GET request.  It's also used in cases where finding the previous or next URL would incur database overhead which would be wasteful, considering most people don't click previous/next links.  Instead, the system will give you a Redirector object which has a URL that'll do the lookup for you later, followed by a redirect."

{
    var readonly string user;
    var readonly string vhost;
    var readonly string type;
    var readonly string url;
    
    function start_form() "Starts an inline HTML form, then calls [method[Redirector.print_hiddens()]].  You can also make it yourself, using [member[Redirector.url]], if you need special form attributes.";
    function print_hiddens() "Prints the necessary hidden elements for a form.  Called automatically by [method[Redirector.start_form()]].";
    function end_form() "Prints a form close tag.";
    
    function builtin get_url(string redir_key) "Returns a GET URL, given a redir_key";
}

### Pages

class Page
"Base template for all views"
{
    var readonly string view "The view type (recent, friends, archive, month, day, entry)";
    var readonly string{} args
    "Arguments from the URL's query string (after the question mark). S2 code can only access arguments starting with a period, and this period is not included in the hash key.";

    var User journal "User whose journal is being viewed";
    var readonly string journal_type "Journal type, ex: 'P' (personal), 'C' (community), etc.";
    var readonly string base_url "The base URL of the journal being viewed.";
    var readonly string{} view_url
    "Links to top-level views where id equals the name of the view being linked to.
    (if one of views == \$.view, already looking at that view)";

    var readonly string[] views_order "An array of view identifiers which can be used to order the views hash.";
    var readonly string head_content
    "Extra tags supplied by the server to go in the <head> section of the output HTML document. Layouts
    should include this in the head section if they are writing HTML.";

    var readonly string stylesheet_url
    "The URL to use in a link element for the server-supported external stylesheet to put stuff in it)";

    var readonly string global_title
    "A title selected by the user for their whole journal.";

    var readonly string global_subtitle
    "A sub-title selected by the user for their whole journal.";

    var readonly UserLink[] linklist
    "An array of UserLink objects defined by the user to be displayed on their journal.";

    var readonly DateTime time
    "A DateTime object filled with the time (GMT) when the page was created.";

    function print
    "The main entry point that LiveJournal calls. Layouts should override this to create HTML that's the
    same for all view types, and use \$this->title, \$this->head and \$this->body to include view-specific
    content into the template.";

    function view_title : string
    "Return a title for this particular page, such as \"Friends' Recent Entries\" for the friends view,
    or a date for the day view. Should be overridden in i18n layers. Ideally, layout layers should never override
    this.  See [method[Page.title()]].";

    function title : string 
    "Return a relevant combination of [member[Page.global_title]] and [method[Page.view_title()]].  May be
    overridden in layout layers or left untouched for the core layer to handle.";
    
    function print_body 
    "Call from [method[Page.print()]] to render parts of the view that are specific to the view, eg print
    the recent set of journal entries, recent friends entries, or rows of user information";

    function print_head [fixed]
    "Print server side supplied head content. This is a fixed function, so you can't override it. See 
    [method[Page.print_custom_head()]] if you want to supply custom head content.";

    function print_custom_head
    "Layers can override this to add extra HTML to the head section of the HTML document. 
    Note that layouts are not intended to override this method.";

    function print_linklist
    "Print the list of UserLink objects specified by the user.";
 
    function print_entry(Entry e) 
    "Output a journal entry. Layouts should override this and the inherited versions in RecentPage, FriendsPage
    and DayPage to change how entries display.";
    
    function print_entry_poster(Entry e) 
    "Output a line of text which says who posted an entry (just \"user\", or \"user posting in somejournal\")";

    function builtin get_latest_month() : YearMonth
    "Returns information about the latest month the user posted (or the current month, if no posts), so that the page may include a mini-calendar or similar features.";

    function builtin visible_tag_list() : Tag[]
    "Returns a list of tags that the logged in user can see for the journal being viewed.";
}

class RecentNav 
"Navigation position within a [class[RecentPage]] or [class[FriendsPage]] and URLs to move about."
{
    var int version        "Currently version 1.  A new method of navigation has been frequently discussed, so this is planning for the future";

    # version 1 attributes:
    var int skip            "Indicates how many entries are being skipped back.";
    var int count           "Indicates how many entries we're currently seeing";
    var readonly string forward_url  "URL to go forward in time, or blank if furthest forward.";
    var int forward_skip    "Number of items we'd be skipping going forward.";
    var int forward_count   "Number of items we'd be potentially seeing going forward.";
    var readonly string backward_url "URL to go backward in time, or blank if furthest back server will allow.";
    var int backward_skip   "Number of items we'd be skipping going back more.";
    var int backward_count  "Number of items we'd be potentially seeing going backward.";
}

class RecentPage extends Page 
"Most recent entries page, formally known as the LASTN view in the previous style system"
{
    var Entry[] entries
    "Array of entries available to be seen by the viewer of the page.";

    var RecentNav nav;
}

class FriendsPage extends RecentPage
"Friends most recent entries"
{
    var Friend{} friends
    "A mapping from friend username to color association information.  There will only be keys for friends whose entries are in the entries array.";

    var readonly string friends_title
    "A user-selected title for their friends page.";

    var string friends_mode
    "The 'mode' of this view. An empty string indicates a normal friends view, while 'friendsfriends' indicates the Friends-of-friends view.";
    
    var bool filter_active
    "If true, some kind of filter is in effect. If this filter has a name, it will be included in [member[FriendsPage.filter_name]]";
    
    var string filter_name
    "The name of the filter in effect, if it has a name. This is only used when 'custom' [member[FriendsPage.filter_active]] is true.";
}

class DayPage extends Page 
"View entries by specifc day"
{
    var Date prev_date "Previous day";
    var Date next_date "Next day";
    var readonly string prev_url "URL to previous day";
    var readonly string next_url "URL to next day";
    var bool has_entries "True if there are entries on the specified day";
    var Entry[] entries "Array of entries available to be seen by the viewer of the page";
    var Date date "Date of the current day";
}

### Archive classes

class YearYear
"Information on how to link to a year in the year archive"
{
    var int year "Number of the year, eg 2001.";
    var readonly string url "URL to link to for this year.";
    var bool displayed "If this is the year currently being displayed, this will be true.";
}

class YearDay 
"Information on how to link to a day in the year archive"
{
    var int day "Day of month number";
    var Date date "Date of day";

    # http://zilla.livejournal.org/show_bug.cgi?id=504
    # var bool is_today "True if the day represents the current day.";

    var int num_entries "Number of entries made on this day";
    var readonly string url "A URL to view the day, if there are entries, else blank.";
}

class YearWeek
"Represents a week on the [class[YearMonth]] on the [class[YearPage]]."
{
    var int pre_empty "How many days at the start of the week are blank? (From previous month)";
    var int post_empty "How many days at the end of the week are blank? (From next month)";
    var YearDay[] days "An array of the days of the week (0=sunday)";

    function print()
    "Print formatted week";
}

class YearMonth 
"A month on the [class[YearPage]]."
{
    var bool has_entries "If this is false, you probably don't want to display this month.";
    var int month "The number of the month";
    var int year "The number of the year";
    var YearWeek[] weeks "An array of the weeks of the month (for ease of building a row-per-week calendar)";
    var readonly string url "A url to link to in order to view this month.";
    var readonly string prev_url "A url to link to in order to view the previous month.";
    var readonly string next_url "A url to link to in order to view the next month.";
    var Date prev_date "Date of previous month, with day of zero, or null if none.";
    var Date next_date "Date of next month, with day of zero, or null if none.";

    function builtin month_format () : string
    "Returns month formatted long (February 1980)  // SeeAlso: siteapi.core1.dateformats";
    function builtin month_format (string fmt) : string
    "Returns time formatted as indicated by \$fmt, or 'long' if blank.  // SeeAlso: siteapi.core1.dateformats";
}

class YearPage extends Page 
"Entire calendar page for a single year."
{
    var int year "The year being viewed";
    var YearYear[] years "Information for linking to other years";
    var YearMonth[] months "12 months objects, even if no entries are in that month.";

    function print_month(YearMonth m)
    "Print the calendar cell for the given month";

    function print_year_links()
    "Print the navigation links to move between years";
}

class MonthDay extends YearDay
"Summaries of posts on a given day on the [class[MonthPage]]."
{
    var bool has_entries "True if there are entries on this day.";
    var Entry[] entries "Only populated on the month view.  Entry text not present.";

    function print_subjectlist 
    "Print a list of entry summaries including subjects";
}

class MonthEntryInfo
"A month the user has journal entries, along with information to link to it."
{
    var Date date "Date of month, with day of zero.";
    var readonly string url "URL for the [class[MonthPage]] month view.";
    var readonly string redir_key "The 'redir_key' parameter for a [class[Redirector]] instance.";
}

class MonthPage extends Page 
"A page which contains a list of posts made in that month"
{
    var Date date "Date of this month, with day of zero.";
    var MonthDay[] days "One entry for each day of the month.";
    var MonthEntryInfo[] months "Other months this journal has entries.";
    var Date prev_date "Date of previous month, with day of zero, or null if none.";
    var Date next_date "Date of next month, with day of zero, or null if none.";
    var readonly string prev_url "URL of previous month, or empty string if none.";
    var readonly string next_url "URL of next month, or empty string if none.";
    var Redirector redir "Necessary to make a form which POSTs to a redirector";
}

class EntryPage extends Page
"A page with a single journal entry and associated comments."
{
    var Entry entry "Journal entry being viewed";
    var ItemRange comment_pages "Represents what comment page is being displayed.";
    var Comment[] comments "Comments to journal entry, or at least some of them.";
    var bool viewing_thread "True if viewing a specific sub-thread of the comments.  Style may which to hide the journal entry at this point, since the focus is the comments.";

    function print_comments(Comment[] comments) "Prints comments";
    function print_comment(Comment comment) "Prints a full comment";
    function print_comment_partial(Comment comment) "Prints a collapsed comment";

    var bool multiform_on "Set to true if the multi-action is to be printed, which requires both comments and applicable permissions for the remote user.";
    function builtin print_multiform_start "Prints start of form tag and hidden elements to do a multi-comment action (multiple delete, screen, unscreen, etc...)";
    function builtin print_multiform_end "Prints end of form tag to do a multi-comment action.";
    function builtin print_multiform_actionline "Prints the line of the multiform giving instructions, options, and the submit button, using the text of the different \$*text_multiform_ properties.";
}

class ReplyForm 
"This class will be used more in the future to set options on the reply form before
it's printed out by the system.  The system has to print it since it contains
sensitive information which can't be made available to S2."
{
    var readonly bool subj_icons "Whether user has enabled subject icons or not.  Currently read-only until policy is decided on whether layers should be able to change it (rather than changing it in the user preferences)";
    function builtin print() "Prints the reply form";
}

class ReplyPage extends Page
"A page to reply to a journal entry or comment"
{
    var Entry entry "The journal entry for this talk page";
    var EntryLite replyto "The object which is being replied to, either the entry or a comment";
    var ReplyForm form "The reply form.";
}

class PalItem
"A specification for a numbered palette index in a GIF or PNG to be changed to a certain color"
{
    var int index "Integer palette index.";
    var Color color "Color to put at specified index.";
}

##[ Built-in Functions ]

function builtin eurl (string s) : string
"URL escape";

function builtin ehtml (string s) : string
"Escapes all HTML tags and entities from the text";

function builtin etags (string s) : string
"Escapes all HTML tags (but not entities) from text";

function builtin clean_url (string s) : string
"Returns the given URL back if it's a valid URL.";

function builtin rand (int high) : int
"Returns a random integer between 1 and \$high, inclusive.";

function builtin rand (int low, int high) : int
"Returns a random integer between \$low and \$high, inclusive.";

function builtin alternate (string a, string b) : string
"With each call, this function will alternate between the two values and return one of them.
Useful for making tables whose rows alternate in background color.";

function builtin zeropad (int n, int digits) : string
"Returns the number padded with zeroes so it has the amount of digits indicated.";

function builtin zeropad (string n, int digits) : string
"Returns the number padded with zeroes so it has the amount of digits indicated.";

function builtin striphtml (string s) : string 
"Similar to ehtml, but the HTML tags are stripped rather than escaped.";

function builtin viewer_logged_in() : bool
"Returns true if the user viewing the page is logged in. It's recommended that your page links to the site
login page if the user isn't logged in.";

function builtin viewer_is_owner() : bool
"Returns true if the user viewing the page is both logged in, and is the owner of the content in question.
Useful for returning links to manage information, or edit entries.";

function builtin get_page () : Page
"Gets the top-level [class[Page]] instance that LiveJournal ran the [method[Page.print()]] method on.";

function builtin get_url(string user, string view) : string 
"Returns a URL to the specified view for the specified user. Views use the same names as elsewhere. (recent, friends, archive, month, userinfo)";

function builtin get_url(UserLite user, string view) : string
"Returns a URL to the specified view for the specified user. Views use the same names as elsewhere. (recent, friends, archive, month, userinfo)";

function builtin string(int i) : string
"Return the given integer as a string";

function builtin int(string s) : int
"Convert the string to an integer and return";

function builtin set_content_type(string text)
"Set the HTTP Content-type response header (for example, if outputting XML). Must be called before printing any data.";

function builtin get_plural_phrase(int n, string prop) : string
"Picks the phrase with the proper plural form from those in the property \$prop, passing \$n to [function[lang_map_plural(int)]] to get the proper form for the current language, and then substituting the # character with \$n.  Also, returned string is HTML-escaped.";

function builtin weekdays() : int[] 
"Integers representing the days of the week. This will start on Monday (2) or Sunday (1) depending on the property setting for start-of-week and go to Sunday (1) or Saturday (7)";

function builtin PalItem(int index, Color c) : PalItem
"Convenience constructor to make populating an array of PalItems (like in [function[palimg_modify]]) easy.";

function builtin palimg_modify(string filename, PalItem[] items) : string
"Return a URL to the specified filename (relative to the palimg root) with its palette table altered, once for each provided [class[PalItem]].  Restrictions:  only 7 palette entries may be modified, and the PalItem indexes must be 0-15.";

function builtin palimg_tint(string filename, Color bright) : string
    "Return a URL to the specified filename (relative to the palimg root) with its palette table tinted.  The given 'bright' color will be the new white, and darkest color remains black.";

function builtin palimg_tint(string filename, Color bright, Color dark) : string
    "Return a URL to the specified filename (relative to the palimg root) with its palette table tinted.  The given 'bright' color will be the new white, and the given 'dark' color is the new black.";

function builtin palimg_gradient(string filename, PalItem start, PalItem end) : string
    "Return a URL to the specified filename (relative to the palimg root) with its palette table made into a gradient.  All palette entries between the inclusive indexes of \$start and \$end will fade from the colors in \$start and \$end.  The palette indexes for the start and end can be between 0 and 255.";

function builtin set_handler(string eventname, string[][] commands);

function builtin userlite_base_url(UserLite ul) : string;
function builtin userlite_as_string(UserLite ul) : string "Access to the LJ::ljuser function";

##[ properties ]

propgroup colors = "Colors";
propgroup fonts = "Fonts";
propgroup presentation = "Presentation";
propgroup other = "Other";
propgroup text = "Text";

property string lang_current {
        noui = 1;
        des = "Current language code.  So layouts can change date/time formats more safely if they want.";
}
set lang_current = "en";  # core is English.

property string lang_fmt_date_short {
    noui = 1; 
    des = "Short date format.  All numeric.";
}
set lang_fmt_date_short = "%%m%%/%%d%%/%%yy%%";

property string lang_fmt_date_med {
    noui = 1; 
    des = "Medium date format.  Abbreviated month name, no day of the week.";
}
set lang_fmt_date_med = "%%mon%%. %%dayord%%, %%yyyy%%";

property string lang_fmt_date_med_day {
    noui = 1; 
    des = "Medium date format with day of week.  Abbreviated month name and abbreviated day of the week.";
}
set lang_fmt_date_med_day = "%%da%%, %%mon%%. %%dayord%%, %%yyyy%%";

property string lang_fmt_date_long {
    noui = 1; 
    des = "Long date format.  With full month name, but no day of the week.";
}
set lang_fmt_date_long = "%%month%% %%dayord%%, %%yyyy%%";

property string lang_fmt_date_long_day {
    noui = 1; 
    des = "Long date format.  With full month name and full day of the week.";
}
set lang_fmt_date_long_day = "%%day%%, %%month%% %%dayord%%, %%yyyy%%";

property string lang_fmt_time_short {
    noui = 1; 
    des = "Time format.";
}
set lang_fmt_time_short = "%%hh%%:%%min%% %%a%%m";

property string lang_fmt_month_short {
    noui = 1; 
    des = "Short month format.";
}
set lang_fmt_month_short = "%%m%%/%%yy%%";

property string lang_fmt_month_med {
    noui = 1; 
    des = "Medium month format.";
}
set lang_fmt_month_med = "%%mon%% %%yyyy%%";

property string lang_fmt_month_long {
    noui = 1; 
    des = "Long month format.";
}
set lang_fmt_month_long = "%%month%% %%yyyy%%";

property string[] lang_monthname_long {
    noui = 1;
    des = "Months of the year.  Indexed from 1 (January) to 12 (December).";
}
set lang_monthname_long = [ "", "January",  "February", "March",
                            "April", "May", "June",
                            "July", "August", "September",
                            "October", "November", "December" ];

property string[] lang_monthname_short {
    noui = 1;
    des = "Months of the year, in their short forms.  Indexed from 1 (Jan) to 12 (Dec).";
}
set lang_monthname_short = [ "", "Jan",  "Feb", "Mar",
                             "Apr", "May", "Jun",
                             "Jul", "Aug", "Sep",
                             "Oct", "Nov", "Dec" ];

property string[] lang_dayname_long {
    noui = 1;
    des = "Days of the week.  Indexed from 1 (Sunday) to 7 (Saturday).";
}
set lang_dayname_long = [ "", "Sunday", "Monday",  "Tuesday", "Wednesday", 
                          "Thursday", "Friday", "Saturday" ];

property string[] lang_dayname_short {
    noui = 1;
    des = "Days of the week, in their short forms.  Indexed from 1 (Sun) to 7 (Sat).";
}
set lang_dayname_short = [ "", "Sun", "Mon",  "Tue", "Wed", 
                           "Thu", "Fri", "Sat" ];

property string IMGDIR {
    noui = 1;
    doc_flags = "[sys]";
    des = "The base URL of the current LiveJournal site's image directory, without a trailing slash.  Example: \"http://www.livejournal.com/img\".";
}
property builtin string SITENAME {
    noui = 1;
    doc_flags = "[sys]";
    des = "Name of the current LiveJournal site.  Example: \"LiveJournal.com\".";
}

property builtin string SITENAMESHORT {
    noui = 1;
    doc_flags = "[sys]";
    des = "Shorter name of the current LiveJournal site.  Example: \"LiveJournal\".";
}

property builtin string SITENAMEABBREV {
    noui = 1;
    doc_flags = "[sys]";
    des = "Abbreviation of the current LiveJournal site.  Example: \"LJ\".";
}

property builtin string SITEROOT {
    noui = 1;
    doc_flags = "[sys]";
    des = "The base URL of the current LiveJournal site, without a trailing slash.  Example: \"http://www.livejournal.com\".";
}

property builtin string PALIMGROOT {
    noui = 1;
    doc_flags = "[sys]";
    des = "The base URL of palimg files, without a trailing slash.  Example: \"http://www.livejournal.com/palimg\".";
}

property int page_recent_items {
    des = "Number of journal entries to show on recent entry page";
    doc_flags = "[construct]";
    min = 1;
    max = 50;
}
property int page_friends_items {
    des = "Number of journal entries to show on friends page";
    doc_flags = "[construct]";
    min = 5;
    max = 50;
}
set page_recent_items = 20;
set page_friends_items = 20;

property string page_day_sortorder {
    des = "Order of entries shown on a Day page";
    values = "forward|Least recent first|reverse|Most recent first";
}
property string page_year_sortorder {
    des = "Order of months shown on the year archive page";
    values = "forward|Least recent first|reverse|Most recent first";
}
set page_day_sortorder = "forward";
set page_year_sortorder = "forward";

property bool page_month_textsubjects {
    des = "If set, subjects will be provided for the month view in plain text only, with HTML removed.";
    doc = "If set, subjects will be provided for the [class[MonthPage]] in plain text only, with HTML removed.";
}
set page_month_textsubjects = true;

property string text_meta_music {
    des = "Text for 'Current Music'";
}
set text_meta_music = "Current Music";

property string text_meta_mood {
    des = "Text for 'Current Mood'";
}
set text_meta_mood = "Current Mood";

property string text_post_comment {
    des = "Link text to leave a comment.";
    example = "Leave a comment";
}
property string text_max_comments {
    des = "Text when entry has reached a comment maximum";
    example = "Maximum comments reached";
}
property string text_read_comments {
    des = "Link text to read comments.";
    format = "plurals";
    example = "1 comment // # comments";
}
set text_post_comment = "Leave a comment";
set text_max_comments = "Maximum comments reached";
set text_read_comments = "1 comment // # comments";

property string text_post_comment_friends {
    des = "Link text to leave a comment on friends view entry.";
    example = "Leave a comment";
}
property string text_read_comments_friends {
    des = "Link text to read comments on a friends view entry.";
    format = "plurals";
    example = "1 comment // # comments";
}
set text_post_comment_friends = "Leave a comment";
set text_read_comments_friends = "1 comment // # comments";

property string text_skiplinks_back {
    des = "Text to show in a link to skip back through entries";
    maxlength = 20;
    "size" = 15;
    example = "Go back #";
    note = "Include a # character to insert the number of entries that will be viewable when skipping back.";
}
property string text_skiplinks_forward {
    des = "Text to show in a link to skip forward through entries";
    maxlength = 20;
    "size" = 15;
    example = "Go forward #";
    note = "Include a # character to insert the number of entries that will be viewable when skipping forward.";
}
property string text_skiplinks_forward_words {
    des = "Text to show in a link to skip forward through entries";
    maxlength = 20;
    "size" = 15;
    example = "Go forward";
}
set text_skiplinks_back="Previous #";
set text_skiplinks_forward="Next #";

property string text_view_recent {
    des = "Text used to link to the 'Recent Entries' view.";
    maxlength = 20;
    "size" = 15;
    example = "Recent Posts";
}
property string text_view_friends {
    des = "Text used to link to the 'Friends' view, if no custom friends title set";
    maxlength = 20;
    "size" = 15;
    example = "My Friends' Entries";
}
property string text_view_friends_comm {
    des = "Text used to link to the 'Friends' view for a community, if no custom friends title set";
    maxlength = 20;
    "size" = 15;
    example = "Members' Journals";
}
property string text_view_friendsfriends {
    des = "Title of the 'Friends of Friends' view";
    maxlength = 20;
    "size" = 15;
    example = "Friends of Friends";
}
property string text_view_friends_filter {
    des = "Title of a Friends page with an unnamed filter in effect";
    maxlength = 20;
    "size" = 15;
    example = "Friends (Custom filter)";
}
property string text_view_friendsfriends_filter {
    des = "Title of a Friends of Friends page with an unnamed filter in effect";
    maxlength = 20;
    "size" = 15;
    example = "Friends of Friends (Custom filter)";
}
property string text_view_archive {
    des = "Text used to link to the archive view.";
    maxlength = 20;
    "size" = 15;
    example = "Journal Archive";
}
property string text_view_userinfo {
    des = "Text used to link to the 'User Information' view.";
    maxlength = 20;
    "size" = 15;
    example = "My Profile";
}
property string text_view_month {
    des = "Text used to link to a list of subjects for a month.";
    maxlength = 20;
    "size" = 15;
    example = "View Subjects";
}
set text_view_recent = "Recent Entries";
set text_view_friends = "Friends";
set text_view_friends_comm = "Members";
set text_view_friends_filter = "Friends (Custom filter)";
set text_view_friendsfriends = "Friends of Friends";
set text_view_friendsfriends_filter = "Friends of Friends (Custom filter)";
set text_view_archive = "Archive";
set text_view_userinfo = "User Info";
set text_view_month = "View Subjects";

property string text_nosubject {
    des = "Text to replace a subject line when no subject is specified";
    maxlength = 20;
    size = 10;
    example = "No Subject";
    note = "This only appears in places where a subject line is required, such as on a month view.";
}
set text_nosubject = "(no subject)";

property string text_noentries_recent {
    des = "Text to display when there are no entries on the recent or friends views";
    maxlength = 255;
    size = 50;
}
set text_noentries_recent = "There are no entries to display.";

property string text_noentries_day {
    des = "Text to display when there are no entries on the day view";
    maxlength = 255;
    size = 50;
}
set text_noentries_day = "There were no entries on this day.";

property string text_permalink {
    des = "Text for a entry's permanent link";
    maxlength = 50;
    size = 20;
}
set text_permalink = "Link";

property string text_month_screened_comments {
    des = "Text to indicate there are screened comments.";
}
set text_month_screened_comments = "w/ Screened";

property string text_multiform_check {
    des = "Text beside a comment multi-action checkbox.";
}
set text_multiform_check = "Select:";

property string text_multiform_des {
    des = "Text on the multiform action line.";
}
set text_multiform_des = "Mass action on selected comments:";

property string text_multiform_btn {
    des = "Text on the multiform action button.";
}
set text_multiform_btn = "Perform Action";

property string text_multiform_opt_unscreen {
    des = "Text for the comment unscreening action";
}
set text_multiform_opt_unscreen = "Unscreen";

property string text_multiform_opt_screen {
    des = "Text for the comment screening action";
}
set text_multiform_opt_screen = "Screen";

property string text_multiform_opt_unfreeze {
    des = "Text for the comment unfreezing action";
}
set text_multiform_opt_unfreeze = "Unfreeze";

property string text_multiform_opt_freeze {
    des = "Text for the comment freezing action";
}
set text_multiform_opt_freeze = "Freeze";

property string text_multiform_opt_delete {
    des = "Text for the comment delete action";
}
set text_multiform_opt_delete = "Delete";

property string text_multiform_conf_delete {
    des = "Text for the confirming mass-delete action";
}
set text_multiform_conf_delete = "Delete selected comments?";

property string text_tags {
    des = "Text for 'Tags' header, use a # where you want the tag list inserted.";
    example = "Tags: #";
}
set text_tags = "Tags: #";

property string font_base {
    des = "Preferred Font";
    maxlength = 25;
    "size" = 10;
    example = "Arial";
    note = "Leave blank if you don't care.";
}
set font_base = "";   # In core, default is not to care. Layouts will probably specify fonts the author likes instead.

property string font_fallback {
    des = "Alternative font style";
    values = "sans-serif|Sans-serif|serif|Serif|cursive|Cursive|monospace|Monospaced|none|Use browser's default";
    note = "This general style will serve as a fallback if your preferred font is unavailable.";
}
set font_fallback = "none"; # Default in core is to let the browser handle it.

property string reg_firstdayofweek {
    des = "The day of the week the calendar weeks starts on.";
    doc = "The day of the week the calendar weeks starts on.  Either 'sunday' or 'monday'.";
    doc_flags = "[construct]";
    values = "sunday|Sunday|monday|Monday";
}
set reg_firstdayofweek = "sunday";

property string text_day_prev {
    des = "Text to link to the previous day";
    example = "Previous Day";
    maxlength = 20;
}
property string text_day_next {
    des = "Text to link to the next day";
    example = "Next Day";
    maxlength = 20;
}
set text_day_prev = "Previous Day";
set text_day_next = "Next Day";

property string text_comment_from {
    des = "Text of the 'from' header in comments";
    example = "From:";
    maxlength = "20";
}
property string text_comment_date {
    des = "Text of the 'date' header in comments";
    example = "Date:";
    maxlength = "20";
}
property string text_comment_ipaddr {
    des = "Text of the 'IP Address' header in comments";
    example = "IP Address:";
    maxlength = "20";
}
set text_comment_from = "From:";
set text_comment_date = "Date:";
set text_comment_ipaddr = "IP Address:";

property string text_comment_reply {
    des = "Text to link to reply for for comment";
    example = "Reply to this";
    maxlength = "50";
}
property string text_comment_frozen {
    des = "Text to replace reply link with if comment is frozen";
    example = "Replies frozen";
    maxlength = "50";
}
property string text_comment_parent {
    des = "Text to link to parent comment of current comment";
    example = "Parent";
    maxlength = "50";
}
property string text_comment_thread {
    des = "Text to link to the thread stemming from the comment";
    example = "Thread";
    maxlength = "50";
}
set text_comment_reply = "Reply";
set text_comment_frozen = "Frozen";
set text_comment_parent = "Parent";
set text_comment_thread = "Thread";

property string text_poster_anonymous {
    des = "The placeholder used when something is posted by an anonymous user";
    example = "(Anonymous)";
}
set text_poster_anonymous = "(Anonymous)";

property string text_reply_back {
    des = "Text to link back to the single entry view from the read comments page";
    example = "Read Comments";
    maxlength = "50";
}
set text_reply_back = "Read Comments";

property string text_reply_nocomments_header {
    des = "Heading text that explains that comments are disabled";
    example = "Comments Disabled:";
    maxlength = "50";
}
set text_reply_nocomments_header = "Comments Disabled:";

property string text_reply_nocomments {
    des = "Text that explains that comments are not allowed for this post.";
    example = "Comments have been disabled for this post.";
    maxlength = "100";
}
set text_reply_nocomments = "Comments have been disabled for this post.";

property string text_website_default_name {
    des = "If an account's website is specified, but there's no website name, use this text instead";
    noui = 1;
}
set text_website_default_name = "My Website";

property string text_icon_alt_protected {
    des = "Alternative text for icons of protected entries";
    noui = 1;
}
property string text_icon_alt_private {
    des = "Alternative text for icons of protected entries";
    noui = 1;
}
set text_icon_alt_protected = "[protected post]";
set text_icon_alt_private = "[private post]";

property bool use_shared_pic {
    des = "Use shared journal user pictures instead of poster's user picture.";
}
set use_shared_pic = false;

property bool view_entry_disabled {
    des = "Enable to use the old comment page instead of the newer style specific view";
}
set view_entry_disabled = false;

property bool tags_aware {
    des = "When enabled, style is responsible for handling tags.  When disabled, tags are automatically inserted into entry bodies.";
    noui = 1;
}
set tags_aware = false;

property Color color_comment_bar {
   des = "Color of comment bar header";
}
set color_comment_bar = "#d0d0ff";

property string comment_userpic_style {
   des = "Userpic display style for comments";
   doc = "Userpic display style for comments.  Either '' for full, 'small' for small, or 'off' for none.";
   doc_flags = "[construct]";
   values = "|Full|small|Small|off|Off";  
}
set comment_userpic_style = "";

property bool linklist_support {
    des = "Link List Support";
    note = "If you've created a list of links that you would like to include in this layout, change this option to 'yes'.";
}
set linklist_support = true;

property bool external_stylesheet {
    des = "Use linked stylesheet";
    note = "If true, a stylesheet link element will point to a file containing the layout's CSS data.";
    noui = 1;
}
set external_stylesheet = false;

###[ global function implementations ]

function prop_init ()
  "This function is the first thing called and is the place to set properties based on the values of other properties.  It's called before the style system looks at its builtin properties, so if you need to conditionally setup something based on your own custom properties, do it here.  You can't print from this function."
{
  # do nothing, just exist.
}

function print_stylesheet ()
  "Prints a stylesheet, the URL of which can be referenced by [member[Page.stylesheet_url]].  This is another S2 entry point, in addition to [method[Page.print()]]." {
}

function builtin htmlattr(string name, string value) : string
"If the value isn't blank, return in HTML attribute format with a leading space.  HTML of name is not escaped.";

function builtin htmlattr(string name, int value) : string
"If the value isn't blank, return in HTML attribute format with a leading space.  HTML of name is not escaped.";

### Language

function lang_map_plural (int n) : int {
    if ($n == 1) { return 0; } # singular
    return 1;             # plural
}

function lang_page_of_pages (int pg, int pgs) [notags] : string {
    return "Page $pg of $pgs";
}

function lang_user_wrote(UserLite u) : string "Returns text describing that the user wrote something. i18nc layers should override this." {
    if (defined $u) {
        return $u->as_string()+" wrote";
    }
    else {
        return "An anonymous user wrote";
    }
}

function lang_at_datetime(DateTime d) : string "Returns a string saying \"at {the data and time given}\". Used in the core implementation of EntryPage and ReplyPage. i18nc layers should override this." {
    return "on " + $d->date_format("long") + " at " + $d->time_format();
}

function lang_ordinal(int num) [notags] : string
"Make an ordinal number from a cardinal number"
{
    if ($num % 100 >= 4 and $num % 100 <= 20) { return $num+"th"; }
    if ($num % 10 == 1) { return $num+"st"; }
    if ($num % 10 == 2) { return $num+"nd"; }
    if ($num % 10 == 3) { return $num+"rd"; }
    return $num+"th";
}

function lang_ordinal(string num) [notags, fixed] : string 
"Make an ordinal number from a cardinal number.  Don't override this, since the core layer implementation just calls [func[lang_ordinal(int)]], which i18nc layers should override."
{
    return lang_ordinal(int($num)); 
}


function lang_viewname(string viewid) [notags] : string 
"Get some words representing a view"
{
    if ($viewid == "recent") { return $*text_view_recent; }
    if ($viewid == "archive") { return $*text_view_archive; }
    if ($viewid == "friends") { return $*text_view_friends; }
    if ($viewid == "day") { return "Day"; }
    if ($viewid == "month") { return "Month"; }
    if ($viewid == "userinfo") { return $*text_view_userinfo; }
    if ($viewid == "entry") { return "Read Comments"; }
    if ($viewid == "reply") { return "Post Comment"; }
    return "Unknown View";
}

### Navigation


### Image Manipulation

function Image::as_string(string{} opts) [fixed] : string {
    var string img = "";
    if ($opts{"href"} != "") { $img = $img + "<a href=\"" + eurl($opts{"href"}) + "\" $opts{"a_attr"}>"; }
    $img = $img + $this->as_string($opts{"alt"});
    if ($opts{"href"} != "") { $img = $img + "</a>"; }

    return $img;
}

function Image::as_string(string alttext) [fixed] : string {
    return "<img src=\"$.url\" alt=\"" + ehtml($alttext) + "\"" +
        htmlattr("height", $.height) +
        htmlattr("width", $.width) + " />";
}

function Image::as_string() [fixed] : string {
    return $this->as_string($this.alttext);
}

function Image::print (string{} opts)
{
    # must do safe here because opts could have the 'a_attr' key set
    print safe $this->as_string($opts);
}

function Image::print (string alttext)
{
    print $this->as_string($alttext);
}

function Image::print
{
    print $this->as_string($this.alttext);
}

function userinfoicon(UserLite user) : Image {
    var Image uimage;
    $uimage.width = 16;
    $uimage.height = 16;

    if ($user.journal_type == "C") {
        $uimage->set_url("$*IMGDIR/community.gif");
    } elseif ($user.journal_type == "Y") {
        $uimage->set_url("$*IMGDIR/syndicated.gif");
    } elseif ($user.journal_type == "N") {
        $uimage->set_url("$*IMGDIR/newsinfo.gif");
    } else {
        $uimage->set_url("$*IMGDIR/userinfo.gif");
        $uimage.width = 17;
        $uimage.height = 17;
    }
    return $uimage;
}

### UserLite functions

function UserLite::base_url() [fixed] : string {
    return userlite_base_url($this);
}

function UserLite::tag_manage_url() [fixed] : string {
    return "$*SITEROOT/manage/tags.bml?authas=$.username";
}

function UserLite::as_string() [fixed] : string {
    return userlite_as_string($this);
}

function UserLite::print() {
    print $this->as_string();
}

function Friend::print() {
    print $this->as_string();
}

### Generic Page Functions

function Page::view_title() [notags] : string {
    return lang_viewname($.view);
}
function FriendsPage::view_title() : string {
    if ($.friends_mode == "") {
        if ($.filter_active) {
            if ($.filter_name != "") {
                if ($.friends_title != "") {
                    return $.friends_title+" ("+$.filter_name+")";
                } else {
                    return $*text_view_friends+" ("+$.filter_name+")";
                }
            } else {
                return $*text_view_friends_filter;
            }
        } else {
            if ($.friends_title != "") {
                return $.friends_title;
            } elseif ($.journal.journal_type == "C") {
                return $*text_view_friends_comm;
            } else {
                return $*text_view_friends;
            }
        }
    }
    elseif ($.friends_mode == "friendsfriends") {
        if ($.filter_active) {
            if ($.filter_name != "") {
                return $*text_view_friendsfriends+" ("+$.filter_name+")";
            }
            else {
                return $*text_view_friendsfriends_filter;
            }
        }
        else {
            return $*text_view_friendsfriends;
        }
    }
    else {
        return "Unknown Friends View";
    }
}
function DayPage::view_title : string {
    return $.date->date_format("long");
}
function YearPage::view_title() : string {
    return string($.year);
}
function EntryPage::view_title() : string {
    return $.entry.subject ? $.entry->get_plain_subject() : "";
}
function ReplyPage::view_title() : string {
    return "Post a comment";
}
function Page::title() [notags] : string {
    var string title = $.global_title;
    if ($title == "") {
        $title = $.journal.name;
    }
    if ($.view != "recent") {
        $title = $title + " - " + $this->view_title();
    }
    if ($.view == "friends") {
        $title = $this->view_title();
    }
    return $title;
}

function server_sig() {
    """Powered by <a href="$*SITEROOT/">$*SITENAME</a>""";
}

function Page::print() {
    "<html>\n<head>\n<title>"+$this->title()+"</title>\n";

    if ($*font_base != "" or $*font_fallback != "none") {
"""<style type="text/css">
<!--
body, td, th, table, p, div {
    font-family: """;
        if ($*font_base != "") {
            "\"$*font_base\"";
            if ($*font_fallback != "none") {
                ", ";
            }
        }
        if ($*font_fallback != "none") {
            print $*font_fallback;
        }
        ";\n}\n--></style>\n";
    }

    $this->print_head();
    "</head>\n<body>\n";
    if (defined $.journal.default_pic and
        $.view != "entry" and
        $.view != "reply") {
            $.journal.default_pic->print();
    }
    print "<h1>" + $this->title() + "</h1>\n";

    $this->print_body();
    """\n<hr><address>""";
    server_sig();
    """</address>\n</body></html>""";
}

function Page::print_body() {
    """<h2>No Default Renderer</h2><p>There is no body renderer for viewtype <tt>$.view</tt> defined.</p>""";
}

function Page::print_head() {
    print $.head_content;
    $this->print_custom_head();
}

function Page::print_custom_head() {
    # blank
}

function Page::print_linklist() {
    if (size $.linklist <= 0) {
        return;
    } elseif (not $*linklist_support) {
        return;
    }

    foreach var UserLink l ($.linklist) {
        if ($l.title) {
            if ($l.is_heading) {
                "<b>$l.title</b>";
            } else {
                "<a href='$l.url'>$l.title</a>";
            }
        }
        "<br />";
    }
}

function Page::print_entry_poster(Entry e) {
    $e.poster->print();
    if ($.view == "friends" and $e.poster.username != $e.journal.username) {
        " posting in ";
        $e.journal->print();
    }
}
function Page::print_entry(Entry e) {
    ## For most styles, this will be overridden by FriendsPage::print_entry and such.
    """<div class="entry" id="$e.dom_id">\n""";
    "<h3>$e.security_icon $e.subject</h3>\n";
    if ($.view == "friends" or $e.poster.username != $e.journal.username) {
        "<div>"; $this->print_entry_poster($e); "</div>";
    }
    """<div class="entrytext">\n$e.text\n</div>\n""";
    $e->print_metadata();
    if ($e.comments.enabled) {
        $e.comments->print();
    }
    "</div>\n\n";
}
function FriendsPage::print_entry(Entry e) {
    ## For most styles, this will be overridden by FriendsPage::print_entry and such.
    """<div class="entry" id="$e.dom_id">\n""";
    var Color bg;
    var Color fg;
    $bg = $.friends{$e.journal.username}.bgcolor;
    $fg = $.friends{$e.journal.username}.fgcolor;
    "<h3>";
    $this->print_entry_poster($e);
    "$e.security_icon $e.subject";
    "</h3>\n";
    """<div style="color: """ + $fg + "; background: " + $bg + """ none;">"""; $this->print_entry_poster($e); "</div>";
    """<div class="entrytext">\n$e.text\n</div>\n""";
    $e->print_metadata();
    if ($e.comments.enabled) {
        $e.comments->print();
    }
    "</div>\n\n";
}
function Entry::print_metadata() {
    if (size $.metadata) {
        """<div class="metadata">\n""";
        foreach var string m ($.metadata) {
            "<strong>$m</strong>: "; print $.metadata{$m}; "<br />";
        }
        "</div>\n";
    }
}

function Comment::print_linkbar() {
    var Link link;
    foreach var string k ($.link_keyseq) {
        $link = $this->get_link($k);
        " $link ";
    }
}
function Entry::print_link_next() {
    var Link link = $this->get_link("nav_next");
    " $link";
}
function Entry::print_link_prev() {
    var Link link = $this->get_link("nav_prev");
    "$link ";
}
function Entry::print_linkbar() {
    ## There's no point in showing previous/next links on pages which show
    ## multiple entries anyway, so we only print them on EntryPage and ReplyPage.

    var Page p = get_page();
    var bool show_interentry = ($p.view == "entry" or $p.view == "reply");
    if ($show_interentry) {
        $this->print_link_prev();
    }
    var Link link;
    foreach var string k ($.link_keyseq) {
        $link = $this->get_link($k);
        " $link ";
    }
    if ($show_interentry) {
        $this->print_link_next();
    }
}

### RecentPage and related functions

function RecentPage::print_body {
    # Creator for both the Recent and Friends views, since they are similar
    # If someone wants to do the two views differently, they can create
    # FriendsPage::print_body since FriendsPage extends RecentPage.

    foreach var Entry e ($.entries) {
        if ($e.end_day) {
            "</div>";
        }
        if ($e.new_day) {
            """<div class="day" id="dayYYYYMMMDD">\n<h2>""";
            print $e.time->date_format("long_day");
            "</h2>\n";
        }
        # Print the entry
        $this->print_entry($e);
    }
}


### Year view

function YearPage::print_body {
    $this->print_year_links();
    foreach var YearMonth m ($.months) {
        $this->print_month($m);
    }
}
function YearPage::print_year_links() {
    """<ul>\n""";
    foreach var YearYear y ($.years) {
        if ($y.displayed) {
            """<li class="active">$y.year</li>\n""";
        } else {
            """<li><a href="$y.url">$y.year</a></li>\n""";
        }
    }
    """</ul>\n""";
}
function YearPage::print_month(YearMonth m) {
    if (not $m.has_entries) { return; }
    """<table style="margin-left: 25%; margin-right: 25%; margin-top: 1em; margin-bottom: 1em;
       border-collapse: collapse; border: 1px solid;" border="1">\n
       <tr><th colspan="7" style="text-align: center; border: 1px solid;">""";
    print $m->month_format();
    """</th></tr>\n<tr>\n""";
    foreach var int d (weekdays()) {
        "<th>"+$*lang_dayname_short[$d]+"</th>\n";
    }
    "</tr>\n";
    foreach var YearWeek w ($m.weeks) {
        $w->print();
    }
    """<tr><td colspan="7" style="text-align: center; border: 1px solid;">
        <a href="$m.url">$*text_view_month</a></td></tr>\n""";
    "</table>";
}

function YearWeek::print() {
   """<tr valign="top" style="height: 2em;">\n""";
   if ($.pre_empty > 0) {
      """<td class="emptyday" colspan="$.pre_empty">&nbsp;</td>\n""";
   }
   foreach var YearDay d ($.days) {
       """<td style="border: 1px solid;">\n""";
       """<div style="text-align: right;">$d.day</div>\n""";
       if ($d.num_entries > 0) {
           """<div style="text-align: center;"><a href="$d.url">$d.num_entries</a></div>\n""";
       }
       """</td>\n""";
   }
   if ($.post_empty > 0) {
      """<td colspan="$.post_empty">&nbsp;</td>\n""";
   }
   "</tr>";
}

function MonthPage::view_title : string {
    return $.date->date_format($*lang_fmt_month_long);
}

function MonthPage::print_body {
    "<form method='post' action='$.redir.url'><center>";
    $.redir->print_hiddens();
    if ($.prev_url != "") { "[<a href='$.prev_url'>&lt;&lt;&lt;</a>]\n"; }
    if (size $.months > 1) {
        "<select name='redir_key'>\n";
        foreach var MonthEntryInfo mei ($.months) {
            var string sel;
            if ($mei.date.year == $.date.year and $mei.date.month == $.date.month) {
                $sel = " selected='selected'";
            }
            "<option value='$mei.redir_key'$sel>" + $mei.date->date_format($*lang_fmt_month_long) + "</option>";
        }
        "</select>\n<input type='submit' value='View' />";
    }
    if ($.next_url != "") { "\n[<a href='$.next_url'>&gt;&gt;&gt;</a>]\n"; }
    "</center></form>\n<dl>";
    foreach var MonthDay d ($.days) {
        if ($d.has_entries) {
            "<dt><a href=\"$d.url\"><b>";
            print lang_ordinal($d.day);
            "</b></a></dt>\n<dd>";
            $d->print_subjectlist();
            "</dd>\n";
        }
    }
    "</dl>\n";
}

function MonthDay::print_subjectlist() {
    # Too many tables...
    foreach var Entry e ($.entries) {
        print $e.time->time_format("short") + ": ";
        if ($e.poster.username != $e.journal.username) {
            $e.poster->print(); " ";
        }
        "$e.security_icon";
        if ($e.subject != "") {
            " <a href=\"$e.permalink_url\">$e.subject</a>";
        } else {
            " <a href=\"$e.permalink_url\" style=\"font-style: italic;\">($*text_nosubject)</a>";
        }
        if ($e.comments.count > 0) {
            " - " + get_plural_phrase($e.comments.count, "text_read_comments");
        }
        if ($e.comments.screened) {
            " <b>$*text_month_screened_comments</b>";
        }
        "<br />\n";
    }
}

### Day view

function DayPage::print_body() {
    if ($.has_entries) {
        "<div class=\"day\" id=\"dayyymmmmmdddd\">\n<h2>";
        print $.date->date_format("long");
        "</h2>\n";

        foreach var Entry e ($.entries) {
            $this->print_entry($e);
        }

        "</div>";
    } else {
        "<p>$*text_noentries_day</p>";
    }

    "<div class=\"skiplinks\">\n";
    "<a href=\"$.prev_url\">$*text_day_prev</a> | ";
    "<a href=\"$.next_url\">$*text_day_next</a>\n</div>";

}

### CommentInfo functions

function CommentInfo::print_readlink {
    var Page p = get_page();
    "<a href=\"$.read_url\">"+
        get_plural_phrase($.count, $p.view == "friends" ? 
                          "text_read_comments_friends" : "text_read_comments")+
    "</a>";
}
function CommentInfo::print_postlink() {
    var Page p = get_page();
    if ($.maxcomments) {
        "$*text_max_comments";
    } else {
        "<a href=\"$.post_url\">"+($p.view == "friends" ? $*text_post_comment_friends : $*text_post_comment)+"</a>";
    }
}
function CommentInfo::print() {
    if (not $.enabled) { return; }
    """<div style="text-align: right;">\n(""";
    if ($.count > 0 or $.screened) {
        $this->print_readlink();
        " | ";
    }
    $this->print_postlink();
    ")</div>";
}

### Link object functions

function Link::print_button() [fixed] {
    print $this->as_string();
}

function Link::as_string() [fixed] : string {
    if ($.url == "") { return ""; }
    var string ealt = ehtml($.caption);
    return """<a href="$.url"><img border='0' width="$.icon.width" height="$.icon.height" alt="$ealt" title="$ealt" src="$.icon.url" /></a>""";
}

# Redirector

function Redirector::start_form ()
{
    "<form method='post' action='$.url' style='display: inline'>";
    $this->print_hiddens();
}
function Redirector::print_hiddens ()
{
    "<input type='hidden' name='redir_user' value='$.user' />\n";
    "<input type='hidden' name='redir_vhost' value='$.vhost' />\n";
    "<input type='hidden' name='redir_type' value='$.type' />\n";
}
function Redirector::end_form ()
{
    "</form>";
}

### EntryPage functions

function EntryPage::print_comments (Comment[] cs) {
    if (size $cs == 0) { return; }
    foreach var Comment c ($cs) {
        var int indent = ($c.depth - 1) * 25;
        "<div id='$c.dom_id' style='margin-left: ${indent}px; margin-top: 5px'>\n";
        if ($c.full) {
            $this->print_comment($c);
        } else {
            $this->print_comment_partial($c);
        }
        "</div>";
        $this->print_comments($c.replies);
    }
}

function EntryPage::print_comment (Comment c) {
    var Color barlight = $*color_comment_bar->clone();
    $barlight->lightness(($barlight->lightness() + 255) / 2);
    var Color barc = $c.depth % 2 ? $*color_comment_bar : $barlight;
    var string poster = defined $c.poster ? $c.poster->as_string() : "<i>$*text_poster_anonymous</i>";

    "<a name='$c.anchor'></a><div style='background-color: $barc; margin-top: 10px; width: 100%'>";
    "<table cellpadding='2' cellspacing='0' summary='0' style='width: 100%'><tr valign='top'>";
    if (defined $c.userpic and $*comment_userpic_style != "off") { 
        var int w = $c.userpic.width;
        var int h = $c.userpic.height;
        # WARNING: this will later be done by the system (it'll be a
        # constructional property), so don't copy this hack into your
        # layout layers or you'll be messed up later.
        if ($*comment_userpic_style == "small") {
            $w = $w / 2;
            $h = $h / 2;
        }
        print "<td style='width: 102px'><img src='$c.userpic.url' width='$w' height='$h' alt='' /></td>";
    }
    "<td><table style='width: 100%'><tr>";

    "<td align='left' style='width: 50%'>";
      print "<table>";
      print "<tr><th align='right'>$*text_comment_from</th><td>$poster</td></tr>\n";
      print "<tr><th align='right'>$*text_comment_date</th><td style='white-space: nowrap'>";
        print $c.time->date_format("long") + " - " + $c.time->time_format() + "</td></tr>";
      if ($c.metadata{"poster_ip"}) { print "<tr><th align='right'>$*text_comment_ipaddr</th><td>(" + $c.metadata{"poster_ip"} + ")</td></tr>"; }
    "</table></td>";

    print "<td align='right' style='width: 50%'>";
    if ($this.multiform_on) {
        " <label for='ljcomsel_$c.talkid'>$*text_multiform_check</label> ";
        $c->print_multiform_check();
    }
    $c->print_linkbar();
    "</td></tr>";

    print "<tr valign='top'><td style='width: 50%'>";
    if (defined $c.subject_icon or $c.subject != "") { "<h3>$c.subject_icon $c.subject</h3>\n"; }
    print "</td>";

    print "<td style='width:50%;' align='right'><strong>(<a href='$c.permalink_url'>$*text_permalink</a>)</strong></td></tr>\n";
    print "</table></td></tr></table></div>";

    print "<div style='margin-left: 5px'>$c.text</div>\n";
    print "<div style='margin-top: 3px; font-size: smaller'>";
    if ($c.frozen) {
        "($*text_comment_frozen) ";
    } else {
        "(<a href='$c.reply_url'>$*text_comment_reply</a>) ";
    }
    if ($c.parent_url != "") { "(<a href='$c.parent_url'>$*text_comment_parent</a>) "; }
    if ($c.thread_url != "") { "(<a href='$c.thread_url'>$*text_comment_thread</a>) "; }
    "</div>\n";
}

function EntryPage::print_comment_partial (Comment c) {
    var string poster = defined $c.poster ? $c.poster->as_string() : "<i>$*text_poster_anonymous</i>";
    var string subj = $c.subject != "" ? $c.subject : $*text_nosubject;
    print "<a href='$c.permalink_url'>$subj</a> - $poster";
}

function ItemRange::print() {
    if ($.all_subitems_displayed) { return; }
    print "<a name='comments'></a><table align='center' border='0' cellpadding='3'>";
    print "<tr><td align='center' colspan='3'><b>" +
        lang_page_of_pages($.current, $.total) + "</b>";
    print "</td></tr>";
    var string url_prev = $this->url_of($.current - 1);
    if ($.current != 1) {
        print "<tr><td align='center'><a href='$url_prev#comments'>&lt;&lt;</a></td>";
    } else {
        print "<tr><td align='center'>&lt;&lt;</td>";
    }
    print "<td align='center'>";
    foreach var int i (1..$.total) {
        if ($i == $.current) { "<b>[$i]</b> "; }
        else {
            var string url_of = $this->url_of($i);
            "<a href='$url_of#comments'><b>[$i]</b></a> ";
        }
    }
    var string url_next = $this->url_of($.current + 1);
    if ($.current != $.total) {
        print "<td align='center'><a href='$url_next#comments'>&gt;&gt;</a></td>";
    } else {
        print "<td align='center'>&gt;&gt;</td>";
    }
    print "</tr></table>";
}

function EntryPage::print_body 
{
    var Entry e = $.entry;
    """<div class="entry" id="$e.dom_id">\n""";

    "<table><tr valign='middle'>";
    if (defined $e.userpic) { 
        print "<td>$e.userpic</td>"; 
    }
    print "<td>"+lang_user_wrote($e.poster);
    print "<br />"+lang_at_datetime($e.time);
    "</td></tr></table>\n";

    print "<div style='text-align: center'>";
    var Link link;
    $link = $e->get_link("nav_prev"); " $link";
    $link = $e->get_link("edit_entry"); " $link";
    $link = $e->get_link("mem_add"); " $link";
    $link = $e->get_link("tell_friend"); " $link";
    $link = $e->get_link("nav_next"); " $link";
    print "</div>";

    "<h2>$e.security_icon $e.subject</h2>\n"; 

    if (not $.viewing_thread) {
        "<div class='entrytext'>\n$e.text\n</div>\n";
        $e->print_metadata();
        "</div>\n\n";
    }

    if ($.entry.comments.enabled)
    {
        print """<hr />(<a href="$e.comments.post_url">$*text_post_comment</a>)""";
        if ($.comment_pages.total_subitems > 0) {
            "<hr />";
            $.comment_pages->print();
            $this->print_multiform_start();
            $this->print_comments($.comments);
            print "<hr />";
            print """(<a href="$e.comments.post_url">$*text_post_comment</a>)""";
            if ($.comment_pages.all_subitems_displayed) { print "<hr />"; }
            $this->print_multiform_actionline();
            $this->print_multiform_end();
            $.comment_pages->print();
        }
    }
}

function ReplyPage::print_body
{
    if (not $.entry.comments.enabled) {
        print "<h2>$*text_reply_nocomments_header/h2><p>$*text_reply_nocomments</p>";
        return; 
    }

    "<table><tr valign='middle'>";
        if (defined $.replyto.userpic) { 
            print "<td>$.replyto.userpic</td>"; 
        }
    print "<td>"+lang_user_wrote($.replyto.poster);
    print "<br />"+lang_at_datetime($.replyto.time);
    "</td></tr></table>\n";

    print "<h2>$.replyto.subject</h2>";
    print "<div>$.replyto.text</div>";

    print "\n<hr />\n";
    print """(<a href="$.entry.permalink_url">$*text_reply_back</a>)""";
    $.form->print();
}