## -*-s2-*-

##[ layerinfo ]

layerinfo "type" = "core";
layerinfo "name" = "Dreamwidth S2 Core, v2";
layerinfo "redist_uniq" = "core2";
layerinfo "majorversion" = "2";
layerinfo "author_name" = "Dreamwidth Webmaster";
layerinfo "author_email" = "webmaster@dreamwidth.org";

##[ 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.";

    function builtin compare(int n) : int
    "Compare one integer with another. Returns a negative number if n is less than the subject, positive if greater or zero if the two are numerically equal.";
}

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 index(string sub) : int
    "Returns the index of the first instance of \$sub in string.";

    function builtin index(string sub, int position) : int
    "Returns the index of the first instance of \$sub in string, starting from \$position";

    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 replace(string find, string replace) : string
    "Replace all instances of \$find with \$replace. Case sensitive.";

    function builtin split (string sub) : string[]
    "Return an array of strings split by \$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";

    function builtin compare(string s) : int
    "Compare one string with another. Returns a negative number if n is alphabetically before the subject, positive if greater or zero if the two are equal. Note that this function currently does a simple ASCII compare, not a proper unicode-aware sort.";

    function builtin css_string() : string
    "Returns the string escaped and quoted as a CSS string literal, safe for insertion into a stylesheet.";

    function builtin css_keyword() : string
    "If the string is syntactically valid as a CSS keyword (only letters and spaces) returns it, else returns an empty string.";

    function builtin css_keyword_list() : string
    "Analyses the string as a space-separated list of CSS keywords and returns a string containing the items that are syntactically acceptable.";

    function builtin css_keyword(string[] allowed) : string
    "Same as [method[string.css_keyword()]] except also imposes a whitelist of valid keywords given in \$allowed.";

    function builtin css_keyword_list(string[] allowed) : string
    "Same as [method[string.css_keyword_list()]] except also imposes a whitelist of valid keywords given in \$allowed.";

    function builtin css_length_value() : string
    "If the string contains a valid CSS length value, returns a canonical version. Else returns an empty string.";

    function builtin css_url_value : string
    "If the string contains a valid HTTP or HTTPS URL it is returned. Otherwise, an empty string is returned.";

    function builtin css_multiply_length(int multiplier) : string
    "If the string contains a valid CSS length value, returns the value multiplied by the multiplier with the unit appended. Otherwise, an empty string is returned";

    function builtin css_divide_length(int divisor) : string
    "If the string contains a valid CSS length value, returns the value divided by the divisor with the unit appended. Otherwise, an empty string is returned";
}

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.";
  function builtin blend(Color other, int value) : Color "Returns color blended with \$other color by percentage value (int between 0 and 100).";
}


##[ 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";

    function builtin date_format (string fmt, bool linkify) : string
    "Returns date formatted in the same way as calling date_format(string fmt), but with day, month, and year as links to the corresponding archive pages.";

    function builtin compare(Date d) : int
    "Compare two dates. Returns a negative number if d is before the subject in time, positive if it is after, or zero if the two dates are equal. When comparing a Date with a DateTime, the time on the bare Date value is assumed to be midnight.";

    function builtin compare(DateTime d) : int
    "Compare two dates. Returns a negative number if d is before the subject in time, positive if it is after, or zero if the two dates are equal. When comparing a Date with a DateTime, the time on the bare Date value is assumed to be midnight.";
}

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";
    var readonly string{} extra "Extra params for img tag";

    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]].";
    var readonly string{} extra "Extra attributes passed to the link";

    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_all "URL to view all items. Blank if we don't support viewing all items for this.";
  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 with a default anchor and CSS class.";
  function print(string{} extra) "Prints the item range links with the given attributes. Accepts 'class' and 'anchor'";
}

### 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 readonly string permalink_url "Permanent URL for the entry.";
    var int count "Current number of comments available to be read by the viewer.";
    var int screened_count "Current number of screened comments (only visible if remote user can view screened comments)";
    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 comments_disabled_maintainer "Set to true if the comments on this entry were disabled by a community administrator.";
    var bool maxcomments "Set to true if entry has reached a comment maximum.";
    var bool show_postlink "Indicates whether the Post Comment link for this entry should be shown.";
    var bool show_readlink "Indicates whether the Read Comments link for this entry should be shown.";
    var bool show_readlink_hidden "Indicates whether to show the Read Comments link for this entry, but hidden, to be shown my JS.";

    function print
    "Print all comment related links";
    function print(Entry e, string{} opts)
    "Print all comment related links, with arguments";
    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 string hover "The hover text for the link";
    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 25 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 user "Backend Username: should match username for registered journals, external identities will be represented as ext_XXX instead";
    var readonly string journal_type "Type of account: P (personal), C (community), Y (syndicated), I (external identity) etc";
    var readonly string userpic_listing_url "URL of a page listing this user's userpics";

    var Link{} data_link "Links to various machine-readable data sources relating to this user";
    var string[] data_links_order "An array of data views which can be used to order the data_link hash";

    var string[] link_keyseq "Array of keys which can be passed into the get_link";

    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 builtin ljuser() : string "Returns an LJ user tag for the user.";
    function builtin ljuser(Color link_color) : string "Returns an LJ user tag for the user.  The color of the link will be link_color.";
    function builtin get_link(string key) : Link "Returns a link based on the given key, or null if the link is unavailable";
    function base_url () : string "Returns URL of user's journal.";
    function linklist_manage_url () : string "Returns URL to user's tag management page.";
    function tag_manage_url () : string "Returns URL to user's tag management page.";
    function icon_manage_url () : string "Returns URL to user's icon management page.";
    function subscriptionfilters_manage_url () : string "Returns URL to user's subscription filter page.";
    function print_interaction_links() "Print the interaction links for this user/journal.";
    function as_string() : string;
    function print ();
}

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

class TagDetail extends Tag
"A rich structure with lots of information about a Tag."
{
    var readonly int use_count "Count of approximately how many times this tag has been used in posts that the viewer has access to.";
    var readonly string visibility "The visibility level for this tag. Equal to the least restrictive security level that it's used on.  Can be one of: public, protected, private, group.";
    var readonly int{} security_counts "How many times this tag has used this security.  The keys are which security, one of: public, protected, private, group. The value is the count of times the tag is used on entries with that security level. If the current user does not have access to that security level, the value is undefined. The group level is only available if the current user is the journal owner.";
}

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
        "Entry Text; Use [method[EntryLite.print_text()]] to print this so that the entry's trust level is not affected by your layer's trust level.";


    var readonly bool text_must_print_trusted
        "Indicates that this entry's text contains some content that must be printed trusted, with [method[EntryLite.print_text()]], rather than printed directly from an untrusted context. Use this to fall back to a plain trusted print if you are doing something unusual with [member[EntryLite.text]]. Most layers can just ignore this and always use [method[EntryLite.print_text()]].";

    var DateTime time "The user-specified time of the post, or the GMT time if it's a comment.";
    var DateTime system_time "The system time (in GMT) this entry or comment was posted.";
    var readonly bool timeformat24 "Indicates whether the time should be displayed in 24-hour format";

    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 Tag tagnav "Which tag to apply to the intra-tag navigation links.";

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

    var readonly string{} metadata "Post metadata. Keys: 'music', 'mood', 'location', 'groups', 'xpost'.  Comment metadata.  Key: 'poster_ip'";

    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 key)]] 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.";

    var readonly bool hidden_child "Indicates if the child is hidden by default.";
    var readonly bool hide_children "Indicates we are hiding the children of this comment";
    var readonly string echi "The Explicit Comment Hierarchy Indicator, if enabled by user.";

    var readonly bool admin_post "Indicates if this is an official post to this community";

    function print_wrapper_start() "Start the wrapper for this entry or comment; includes anchor and classes";
    function print_wrapper_end() "End the wrapper for this entry or comment.";
    function print_metatypes() "Print the metatype icons for this entry (security, age restriction) or comment (subject icon)";
    function print_metadata () "Print the metadata associated with this entry or comment";
    function print_text() [fixed]
        "Print the entry text. Doesn't print anything in some contexts, such as on a month view or in a collapsed comment.";
    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 subject 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_userpic() "Print the userpic for this entry or comment";
    function print_management_links() "Print the links to manage this entry or comment. (Screen, freeze, track, etc)";
    function print_interaction_links() "Print the links to interact with this entry or comment. (Post a comment etc)";
    function print_time () [fixed] "Print the time of this post, with most useful information for user, and with tooltip for more.";
    function print_time (string datefmt, string timefmt) [fixed] "Print the time of the post, with customized date/time formats.";
    function time_display () : string "Show the time of this post, with most useful information for user, and with tooltip for more.";
    function time_display (string datefmt, string timefmt) : string "time_display, with customized date/time formats.";
    function builtin formatted_subject (string{} opts) : string "formats subject - outputs subject as html-link, gets hash of attributes - class and(or) style ";
    function print_subject () [fixed] "Print the formatted subject for this entry or comment";
    function print_subject (string{} opts) [fixed] "Print the formatted subject for this entry or comment, gets hash of attributes - class and(or) style ";
    function print_poster () "Print the poster of the entry or comment (user or user in journal, etc)";

    function print_admin_post_icon () "prints the icon into the subject line";

    function builtin print_reply_link(string{} opts) "Prints a link to reply to the comment. You may specify the link text in the 'linktext' option, and the link CSS class in 'class'. You may also specify the url of an image to use as a button in 'img_url'.";
    function builtin print_reply_container() "Prints the area in which the quickreply box will go. If no container is available, quickreply will not work.";
    function builtin print_reply_container(string{} opts) "Prints the area in which the quickreply box will go. You may 'class' which will be the CSS class used by the container. If no container is available, quickreply will not work.";

}

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 readonly string adult_content_level "The adult content level ('NSFW', '18'), or blank if unrestricted.";
    var Image adult_content_icon "A little icon which should be displayed somewhere on an entry to represent the adult content level";

    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_metatypes(bool icon, bool info) "Print the metatype information for this entry";

    function print_interaction_links(string target) "Print the links to interact with this entry or comment, with target.";

    function print_tags() "Print the tags for this entry.";
    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.";
    function print_link_tag_next() "Print the link to the next entry in this journal with this tag";
    function print_link_tag_prev() "Print the link to the previous entry in this journal with this tag";
    function builtin viewer_sees_ebox() [fixed] : bool
    "True if opaque horizontal site-specific content boxes between entries should be displayed to the user.";
    function builtin print_ebox() [fixed]
    "Prints a small horizontal bar of site-specific content between entries in a journal.";
}

class StickyEntry extends Entry
"An entry that is shown on top of the journal's recent entries page"
{
    var Image sticky_entry_icon "A little icon displayed next to the subject of a sticky entry post";

    function print_sticky_icon () "prints the icon into the subject line";
}



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 string threadroot_url "URL to view the entire thread this comment is part of, or blank if a top-level comment.";
    var readonly bool screened "True if comment is in screened state.";
    var readonly bool screened_noshow "True if comment is in screened state and user isn't allowed to see it.";
    var readonly bool frozen "True if comment is in frozen state.";
    var readonly bool deleted "True if comment has been deleted. Deleted comments still show up if they are the parent of a thread.";
    var readonly bool fromsuspended "True if comment's author has been suspended.";
    var readonly string anchor "Direct link to comment, via HTML name anchors";
    var readonly bool comment_posted "True if comment was just posted by the current user.";
    var readonly bool edited "True if the comment has been edited.";

    var readonly DateTime time_remote "The local time the comment appeared, in the remote user's (reader's) timezone.  Or undef if no remote user, or remote user hasn't set their timezone.";
    var readonly DateTime time_poster "The local time the comment appeared, in the commenter's timezone.  Or undef if anonymous comment, or commenter's timezone is unknown.";
    var readonly int seconds_since_entry "The number of elapsed seconds from the time of the journal entry until the comment was initially posted.";
    var readonly string editreason "The reason this comment was last edited";

    var readonly DateTime edittime "The GMT time the comment was edited.  Or undef if the comment hasn't been edited.";
    var readonly DateTime edittime_remote "The local time the comment was edited, in the remote user's (reader's) timezone.  Or undef if no remote user, or remote user hasn't set their timezone, or the comment hasn't been edited.";
    var readonly DateTime edittime_poster "The local time the comment was edited, in the commenter's timezone.  Or undef if anonymous comment, or commenter's timezone is unknown, or the comment hasn't been edited.";

    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()]].";

    function builtin expand_link () : string "Returns a link to expand a collapsed comment. Uses the value of the 'text_comment_expand' property as the text. Will not work in untrusted layers.";
    function builtin expand_link (string{} opts) : string "Returns a link to expand a collapsed comment. Can pass options 'text', 'title', 'class', and 'img_url' (and other 'img_*' options). Will not work in untrusted layers.";

    function builtin print_expand_link () : string "Prints a link to expand a collapsed comment. Uses the value of the 'text_comment_expand' property as the text.";
    function builtin print_expand_link (string{} opts) : string "Prints a link to expand a collapsed comment. Can pass options 'text', 'title', 'class', and 'img_url' (and other 'img_*' options).";

    function builtin print_hide_link () : string "Prints a link to hide a comment thread. Uses the value of the 'text_comment_hide' property as the text.";
    function builtin print_hide_link (string{} opts) : string "Prints a link to hide a comment. Can pass options 'text', 'title', 'class', and 'img_url' (and other 'img_*' options).";
    function builtin print_unhide_link () : string "Prints a link to unhide a comment thread. Uses the value of the 'text_comment_unhide' property as the text.";
    function builtin print_unhide_link (string{} opts) : string "Prints a link to hide a comment. Can pass options 'text', 'title', 'class', and 'img_url' (and other 'img_*' options).";
    function print_time (string datefmt, string timefmt, bool edittime) "Same as EntryLite::print_time, except can pass in if we want the edit time or not.";
    function time_display (string datefmt, string timefmt, bool edittime) : string "Same as EntryLite::time_display, except can pass in if we want the edit time or not.";
    function print_edittime () "Print the time that this comment was edited, with most useful information for user.  Empty string if the comment hasn't been edited.";
    function print_edittime (string datefmt, string timefmt) "print_edittime, with customized date/time formats.";
    function edittime_display () : string "Show the time that this comment was edited, with most useful information for user.  Empty string if the comment hasn't been edited.";
    function edittime_display (string datefmt, string timefmt) : string "edittime_display, with customized date/time formats.";

    function print_edit_text () "Print the text that says when this comment was edited.";

}

class Icon
"Represents a single icon"
{
    var readonly Image image "Information about the icon";
    var readonly string[] keywords "All keywords";
    var readonly string comment "Comment text";
    var readonly string description "Description text";
    var readonly string link_url "URL this image links to when clicked";

    var readonly bool default "If this icon is the default";
    var readonly bool active "If this icon is active";

    function print() "Prints the icon";
}

class CommentNav
"Represents the comment navigation bar."
{
  var string view_mode "The current view mode (threaded, flat, top-only).";
  var string url "The url for this entry page, complete with style argument if present.";
  var int current_page "The currently displayed page";
  var bool filter "True if the current view is filtered.";
  var bool show_expand_all "True if we should show the expand_all option.";

  function print () "Prints the nav bar, including the comment_pages bar, with a default CSS class";
  function print(string{} extra) "Prints the nav bar with the given attributes. Accepts 'class'";
}


### 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 website_url "URL pointer to user's website";
    var readonly string website_name "'pretty' name of user's website";
    function print_userpic() "Print the userpic for this user";

}

class SubscriptionFilter
{
    var string name "The name of the filter.";
    var bool public "Whether the filter is publicly accessible.";
    var int sortorder "Which order this filter is in compared to others.";
    var string url "A link to the filtered reading page.";
}

### 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 layout_name "Name of the style being used in the journal you're viewing";
    var readonly string theme_name "Name of the theme being used in the journal you're viewing";
    var readonly string layout_url "URL of the style being used in the journal you're viewing";
    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 bool has_activeentries
    "So we don't try to load the activeentries array when there are no entries to display";

    var readonly Entry[] activeentries
    "Array of recently active entries available to be seen by the viewer of the page.";

    var readonly bool timeformat24
    "Indicates whether the time should be displayed in 24-hour format";

    var readonly DateTime local_time
    "A DateTime object filled with the time (in the viewer's timezone) when the page was created.";

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

    var Link{} data_link "Links to various machine-readable data sources relating to this page";
    var string[] data_links_order "An array of data views which can be used to order the data_link hash";

    var readonly string customtext_title "Header of customtext module";
    var readonly string customtext_url "URL for header of customtext module";
    var readonly string customtext_content "Content of customtext module";

    var readonly bool include_meta_viewport
    "Whether we should print the meta viewport tag or not, based on the viewer's preferences";

    function print
    "The main entry point that Dreamwidth 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 print_header
    "Print information that should go in the page header such as page title and subtitle";

    function print_title "Print the 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 print_head_title "Print the title for this particular page, as in print_title, formatted with <title> and the journal username";

    function print_global_title;
    function print_global_subtitle;

    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_module_jump_link()
    "Print a link to jump to modules below the entries";

    function print_meta_tags()
    "Print meta tags. Needs to be within the first 512 bytes to work.";

    function print_wrapper_start() "Start the body wrapper for this page; includes default classes";
    function print_wrapper_start(string{} opts) "Start the body wrapper for this page; includes default classes and takes a hash using the key 'class'";
    function print_wrapper_end() "End the body wrapper for this entry or comment.";
    function print_entry_footer(Entry e) "Close up the entry";

    function print_time
    "Print the time when the page was created.";

    function print_time (string datefmt, string timefmt)
    "Print the time when the page was created, with customized date/time formats.";

    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 builtin print_control_strip [fixed]
    "Prints a control strip for the user's convenience";

    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 builtin get_latest_month() : YearMonth
    "Returns information about the latest month the user posted (or the current month, if no entries), so that the page may include a mini-calendar or similar features.";

    function builtin visible_tag_list() : TagDetail[]
    "Returns an array of tags that the logged in user can see for the journal being viewed.";

    function builtin visible_tag_list(int limit) : TagDetail[]
    "Returns an array of tags that the logged in user can see for the journal being viewed, limited to the top \$limit tags by number of uses.";

    function builtin print_reply_link(string{} opts) "Prints a link to reply to the comment. You may specify the link text in the 'linktext' option, the link CSS class in 'class', and the target container in the 'target' option. You may also specify the url of an image to use as a button in 'img_url'.";

    function builtin print_reply_container(string{} opts) "Prints the area in which the quickreply box will go. Options you may specify are 'target' which will be the target id, and 'class' which will be the CSS class used by the container. If no container is available, quickreply will not work.";

    function print_navigation() [fixed] "Print out the page navigation links.";
    function print_navigation(string{} opts) "Print out the page navigation links. Accepts 'class', an additional CSS class for this container.";

    function print_module_section( string section_name )
    "Prints a module section, given a group name";

    function print_stylesheets
    "Prints all defined stylesheets, including default and user-defined ones.";

    function print_default_stylesheets
    "Prints stylesheets defined by the layout, including any additional CSS defined by the theme.";

    function print_contextual_stylesheet
    "Prints the layout's default stylesheet for contextual popups.";

    function print_default_stylesheet
    "Prints the layout's base default stylesheet.";

    function print_theme_stylesheet
    "Prints theme-specific CSS. Should be overwritten by themes that include CSS not part of the layout's default stylesheet.";

    function builtin print_trusted(string key)
    "Prints a trusted string by key.";
}

class IconsPage extends Page
"A detail page listing a user's icons."
{
    var readonly bool can_manage "Can the current user manage these icons.";
    var readonly Icon[] icons "List of the icons on the current page.";
    var readonly ItemRange pages "All pages";
    var readonly string sortorder "Current sort order. Can be 'upload' or 'keyword'.";

    var readonly string{} sort_urls "An associative array of link for various sort orders.";
    var readonly string[] sort_keyseq "A list of sort orders, indicated by key.";
    function print_icon_manage_link
    "Prints out a link to the icon management page, if the viewer can manage icons.";
}

class TagsPage extends Page
"A detail page listing a user's tags."
{
    var TagDetail[] tags "List of tags visible to the user viewing the page.";
}

class MessagePage extends Page
"A page showing an error or confirmation message."
{
    var readonly string title "The title of the message.";
    var readonly string message "The body of the message. Do not print this directly; use [method[MessagePage.print_body()]] instead.";
    var Link{} links "An associative array of links to be displayed alongside this message. Iterate over [member[MessagePage.link_keyseq]] to find the keys.";
    var string[] link_keyseq "A list of links, indicated by key, that should be displayed alongside this error. They should ideally be displayed in a similar way to the entry links displayed on the entry page.";

    function print_message() [fixed] "Print the message. Call this rather than printing [member[MessagePage.message]] directly.";
    function print_links() "Print the links from the [member[MessagePage.links]] and [member[MessagePage.link_keyseq]] members. Layouts will probably want to override this.";
}

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, formerly 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;

    var StickyEntry stickyentry
    "Entry shown on top of the Recent Entries page";

    function print_sticky_entry(StickyEntry s)
    "function to print the sticky entry";

    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[RecentPage.filter_name]]";

    var string filter_name
    "The name of the filter in effect, if it has a name. This is only used when [member[RecentPage.filter_active]] is true.";

    var bool filter_tags
    "If true, the page is being filtered to only entries tagged with a specific tag. This is only used when [member[RecentPage.filter_active]] is true.";

}

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 readonly string friends_subtitle
    "A user-selected subtitle for their friends page.";

    var string friends_mode
    "The 'mode' of this view. An empty string indicates a normal friends view, while 'network' indicates the network view.";
}

class DayPage extends Page
"View entries by specific 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";
    function builtin month_format (string fmt, bool link) : string
    "Returns time formatted in the same way as calling month_format(string fmt), but with month and year as links to the corresponding archive pages.";
}

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 entries 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 entries 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 CommentNav comment_nav "The comment navigation bar";

    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_comment_section(Entry entry) "Prints comment section";
    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 EntryPreviewPage extends EntryPage
"We disable funtionality for previews, and print messages about that"
{
    var string preview_warn_text "Text to display instead of management controls for previews";
    function print_standout_box(string message) "Prints a standout box containing a message";
}

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.";
    var bool isedit "True if we are editing a comment, false if writing a new one";

    function print_comment(Comment comment) "Prints a full comment";
}

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 pageview_unique_string () : string
"Returns a unique string for the remote user.";

function builtin clean_css_classname (string classname) : string
"Provide a version of a string that's always suitable for classnames, with potentially suspicious words present in original and modified forms.";

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 viewer_is_friend() : bool
"Returns true if the user viewing the page is logged in and has access to the journal being viewed.  Returns true if the user is a member of a community.";

function builtin viewer_is_subscribed() : bool
"Returns true if the user viewing the page is logged in and subscribes to the journal being viewed.";

function builtin viewer_has_access() : bool
"Returns true if the user viewing the page is logged in and has access to the journal being viewed.  In communities, returns true if the viewer is a member of the community.";

function builtin viewer_is_admin() : bool
"Returns true if the user viewing a community is logged in and is an admin of the community.";

function builtin viewer_is_moderator() : bool
"Returns true if the user viewing a community is logged in and a moderator of the community.";

function builtin viewer_is_member() : bool
"Returns true if the user viewing the page is both logged in, and is a member of the community being viewed. Always returns false for personal journals, since they cannot have members.";

function builtin viewer_can_search() : bool
"Returns true if the user viewing the page is logged in and can search that journal.";

function builtin viewer_can_manage_tags() : bool
"Returns true if the user viewing the page can add, edit, and delete tags on the journal being viewed.";

function builtin viewer_sees_control_strip [fixed] : bool
"Returns true if reader will see the built in control strip.";

function builtin control_strip_logged_out_userpic_css [fixed] : string
"Returns CSS for the userpic div in the logged out version of the control strip.";

function builtin control_strip_logged_out_full_userpic_css [fixed] : string
"Returns CSS for the loggedout_userpic div in the logged out version of the control strip.";

function builtin get_page () : Page
"Gets the top-level [class[Page]] instance that Dreamwidth 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(string,PalItem[])]]) easy.";


function builtin UserLite(string username) : UserLite
"Constructor for making a UserLite object from a username";

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 start_css () "Declare that you're about to start printing out CSS that should be buffered, then later cleaned when you call end_css().  WARNING: this is not re-entrant.  You can't call start_css recursively.";
function builtin end_css () "Declare that you're done printing CSS and the output thus buffered should be cleaned and printed.";

function builtin journal_current_datetime() : DateTime
"Returns the current datetime in the timezone of the journal being viewed.";

function builtin journal_subscription_filters() : SubscriptionFilter[]
"Returns the subscription filters for the journal being viewed, in sort order.";

function builtin style_is_active() : bool
"Returns if the style (layout and theme) calling it is active based on a hook.  If hook isn't defined, returns true always.";

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.";

function builtin print_search_form(string button_text)
"Prints a search form, with the button text as the label to the submit button.";

function builtin keys_alpha(string{} elements) : string[]
"Return the keys of the array in alphabetically sorted order.";

function builtin get_image(string key) : Image
"Given an image key to look up, returns an Image object populated with src, width, height taken from the standard images used by S2. Image object may be null if key is not valid";

##[ properties ]

propgroup presentation = "Presentation";
propgroup colors = "Colors";
propgroup fonts = "Fonts";
propgroup images = "Images";
propgroup modules = "Modules";
propgroup text = "Text";
propgroup customcss = "Custom CSS";

propgroup appearance = "Appearance";
propgroup background = "Background";
propgroup sidebar = "Sidebar";
propgroup options = "Options";
propgroup other = "Other";

##=======================================
## Constant system properties
##=======================================

property builtin string IMGDIR {
    noui = 1;
    doc_flags = "[sys]";
    des = "The base URL of the current Dreamwidth site's image directory, without a trailing slash.  Example: \"https://www.dreamwidth.com/img\".";
}
property builtin string STYLES_IMGDIR {
    noui = 1;
    doc_flags = "[sys]";
    des = "The base URL of the current Dreamwidth site's style image directory, without a trailing slash.  Example: \"https://www.dreamwidth.com/img/styles\".";
}
property builtin string STATDIR {
    noui = 1;
    doc_flags = "[sys]";
    des = "The base URL of the current Dreamwidth site's static files directory, without a trailing slash.  Example: \"https://www.dreamwidth.com/stc\".";
}
property builtin string SITENAME {
    noui = 1;
    doc_flags = "[sys]";
    des = "Name of the current Dreamwidth site.  Example: \"Dreamwidth.com\".";
}
property builtin string SITENAMESHORT {
    noui = 1;
    doc_flags = "[sys]";
    des = "Shorter name of the current Dreamwidth site.  Example: \"Dreamwidth\".";
}
property builtin string SITENAMEABBREV {
    noui = 1;
    doc_flags = "[sys]";
    des = "Abbreviation of the current Dreamwidth site.  Example: \"DW\".";
}
property builtin string SITEROOT {
    noui = 1;
    doc_flags = "[sys]";
    des = "The base URL of the current Dreamwidth site, without a trailing slash.  Example: \"https://www.dreamwidth.com\".";
}
property builtin string PALIMGROOT {
    noui = 1;
    doc_flags = "[sys]";
    des = "The base URL of palimg files, without a trailing slash.  Example: \"https://www.dreamwidth.com/palimg\".";
}

##=======================================
## Display properties - Presentation
## Basic options
##=======================================

# FIXME: Not sure if none is a sensible value, but need something appropriate as a default for designers who are not using this option.
# In order to use layout_type on /customize/ in official layouts, the associated S2Theme file needs to be edited, and then changing the layout type will select a different wizard-layer.  Non-official layouts can use this property on the /customize/options.bml page.  Both methods will add the layout_type to <body> in the Page::print-wrapper to allow CSS to be written accordingly.

property string layout_type {
        des = "Layout Type";
        values = "|none|one-column|One column (modules at bottom)|one-column-split|One column split (modules at top & bottom)|two-columns-left|Two columns (sidebar on the left)|two-columns-right|Two columns (sidebar on the right)|three-columns-sides|Three columns (one sidebar on each Side)|three-columns-left|Three column (two sidebars on the left)|three-columns-right|Three column (two sidebars on the right)";
}

property string desktop_media_query {
  des = "If we don't match this media query, we show only one column (mobile-friendly view); if we do match, we use the \$*layout_type defined by the user";
  note = "OBSOLETE: use medium_breakpoint_width instead";
}

property string medium_breakpoint_width {
  des = "The width at which the layout switches from single column mode (mobile view) to multiple columns if selected (tablet view).";
  note = "Set this to 0 to use multiple columns regardless of screen size";
  example = "30em";
}
property string large_breakpoint_width {
  des = "The width at which the layout switches to larger screen mode (desktop view) and can afford some extra whitespace.";
  note = "Set this to 0 to keep the layout the same regardless of screen size";
  example = "40em";
}

property int num_items_recent {
    des = "Number of journal entries to show on the Recent Entries page";
    doc_flags = "[construct]";
    min = 1;
    max = 50;
}
property int num_items_reading {
    des = "Number of journal entries to show on the Reading page";
    doc_flags = "[construct]";
    min = 5;
    max = 50;
}

# Defaults to false, but layouts can set it to true to switch on custom colors for different posters on reading page.
property bool use_custom_friend_colors  { des = "Use my custom reading page colors"; }

property bool use_shared_pic {
    des = "Show community icons instead of posters' icons";
}

# was !view_entry_disabled
property bool use_journalstyle_entry_page {
    des = "Show entry pages in my journal style rather than the site skin";
    note = "OBSOLETE: This option has been moved to Account Settings";
    obsolete = 1;
    noui = 1; # No longer displayed in UI
}

property bool use_journalstyle_icons_page { des = "Show icons page in my journal style rather than the site skin"; }

property string margins_size {
    des = "Size of left and right margins";
    size = 5;
}
property string margins_unit {
    des = "Unit for margins size";
    values = "em|em|%|%|px|px";
}

property string sidebar_width {
    des = "Set the width of a single sidebar.  Ensure that the value is given in em or px.";
    example = "15em";
}
property string sidebar_width_doubled {
    des = "For a setup of two sidebars on the side, set the width to twice the value of a single sidebar (also in em or px).";
    example = "30em";
}

set layout_type = "";
set num_items_recent = 20;
set num_items_reading = 20;
set use_custom_friend_colors = false;
set use_shared_pic = false;
set use_journalstyle_entry_page = true;
set use_journalstyle_icons_page = false;
set margins_size = "0";
set margins_unit = "em";
set sidebar_width = "";
set sidebar_width_doubled = "";

##=======================================
## Display properties - Presentation
## Specific pages
##=======================================

property string[] reverse_sortorder_group {
    des = "Display content in reverse-chronological order on:";
    grouptype = "viewslist";
}
set reverse_sortorder_group = [ "reverse_sortorder_year", "reverse_sortorder_month", "reverse_sortorder_day"];
property bool reverse_sortorder_year {
    des = "Display months in reverse-chronological order on the archive page";
    label = "the archive page";
    grouped = 1;
}
property bool reverse_sortorder_month {
    des = "Display days and entries in reverse-chronological order on the month page";
    label = "the month page";
    grouped = 1;
}
property bool reverse_sortorder_day {
    des = "Display entries in reverse-chronological order on the day page";
    label = "the day page";
    grouped = 1;
}

property string reg_firstdayofweek {
    des = "Day the calendar week starts on";
    doc_flags = "[construct]";
    values = "sunday|Sunday|monday|Monday";
}

property string tags_page_type {
    des = "Type of tags display on the tags page. For the tag module, see the Modules section.";
    values="list|List|cloud|Cloud|multi|Multilevel";
}
property int num_items_icons {
    des = "Number of icons to show on the icons page, or 0 for site default";
    doc_flags = "[construct]";
    max = 50;
}
property string icons_page_sort {
    des = "Default sort order for icons page";
    values="upload|Upload Order|keyword|Keyword Order";
}

set reverse_sortorder_year = false;
set reverse_sortorder_month = false;
set reverse_sortorder_day = false;
set reg_firstdayofweek = "sunday";
set tags_page_type = "multi";
set num_items_icons = 0;
set icons_page_sort = "upload";

##=======================================
## Display properties - Presentation
## Entries & Comments
##=======================================

property string[] entry_datetime_format_group {
    des = "Entry date/time format";
    grouptype = "datetime";
}
set entry_datetime_format_group = ["entry_date_format", "entry_time_format"];
property string entry_date_format {
    des = "Date";
    values = "iso|2010-04-30|short|4/30/10|short_dayfirst|30/4/10|short_dayfirst_full|30/04/2010|med|Apr. 30th, 2010|med_dayfirst|30 Apr 2010|med_day_iso|2010-Apr-30, Friday|med_day|Fri, Apr. 30th, 2010|long|April 30th, 2010|long_dayfirst|30 April 2010|long_day|Friday, April 30th, 2010|long_day_dayfirst|Friday, 30 April 2010";
    grouped = 1;
}
property string entry_time_format {
    des = "Time";
    values = "short|9:00pm|short_24|21:00";
    note = "This format will be displayed to you (personal journal only) and logged-out users. Other logged-in users will keep on seeing time in their preferred format.";
    grouped = 1;
}

property string[] comment_datetime_format_group {
    des = "Comment date/time format";
    grouptype = "datetime";
}
set comment_datetime_format_group = ["comment_date_format", "comment_time_format"];
property string comment_date_format {
    des = "Date";
    values = "iso|2010-04-30|short|4/30/10|short_dayfirst|30/4/10|short_dayfirst_full|30/04/2010|med|Apr. 30th, 2010|med_dayfirst|30 Apr 2010|med_day_iso|2010-Apr-30, Friday|med_day|Fri, Apr. 30th, 2010|long|April 30th, 2010|long_dayfirst|30 April 2010|long_day|Friday, April 30th, 2010|long_day_dayfirst|Friday, 30 April 2010";
    grouped = 1;
}
property string comment_time_format {
    des = "Time";
    values = "short|9:00pm|short_24|21:00";
    note = "This format will be displayed to you (personal journal only) and logged-out users. Other logged-in users will keep on seeing time in their preferred format.";
    grouped = 1;
}

property string[] userpics_style_group {
    des = "Select the size of icons";
    grouptype = "datetime";
}
set userpics_style_group = ["entry_userpic_style", "comment_userpic_style"];
property string entry_userpic_style {
    des = "In entries";
    doc = "Either '' for 100%, 'small' for 75% and 'smaller' for 50%";
    doc_flags = "[construct]";
    values = "|Full|small|Small|smaller|Smaller";
    grouped = 1;
}
property string comment_userpic_style {
    des = "In comments";
    doc = "Either '' for 100%, 'small' for 75% and 'smaller' for 50%";
    doc_flags = "[construct]";
    values = "|Full|small|Small|smaller|Smaller";
    grouped = 1;
}

property string userpics_position {
    des = "Place of icons in entries and comments";
    values = "none|None (don't show)|left|Left|right|Right";
}

property string entry_metadata_position {
    des = "Place of metadata in entries";
    values = "top|Before entry text|bottom|After entry text";
}

property string userlite_interaction_links {
    des = "Select whether user interaction links are printed as text or using the available icons";
    values = "icons|icons|text|text-only|";
}
property string entry_management_links {
    des = "Select whether entry management links are printed as text or using the available icons";
    values = "icons|icons|text|text-only|";
}
property string entry_interaction_links {
    des = "Select whether entry interaction links are printed as text or using the available icons";
    values = "icons|icons|text|text-only|";
    noui = 1; # FIXME: no icons made yet
}
property string comment_management_links {
    des = "Select whether comment management links are printed as text or using the available icons";
    values = "icons|icons|text|text-only|";
}
property string comment_interaction_links {
    des = "Select whether comment interaction links are printed as text or using the available icons";
    value = "icons|icons|text|text-only|";
    noui = 1; # FIXME: no icons made yet
}

property bool all_entrysubjects {
    des = "Display the replacement text when no subject is specified in entries";
    note = "If disabled, the replacement text will still appear where a subject is required, such as on month views.";
}

property bool all_commentsubjects {
    des = "Display the replacement text when no subject is specified in comments";
    note = "If disabled, the replacement text will still appear where a subject is required, such as in collapsed comments.";
}

set entry_date_format = "med";
set entry_time_format = "short";
set comment_date_format = "iso";
set comment_time_format = "short";
set entry_userpic_style = "";
set comment_userpic_style = "";
set userpics_position = "left";
set entry_metadata_position = "bottom";
set userlite_interaction_links = "icons";
set entry_management_links = "icons";
set entry_interaction_links = "text";
set comment_management_links = "icons";
set comment_interaction_links = "text";
set all_entrysubjects = true;
set all_commentsubjects = false;

##=======================================
## Display properties - Presentation
## Misc. options - not exposed
##=======================================

property bool linklist_support {
    des = "Display link list";
}

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

property string tags_page_count_type {
    des = "Determines how the tag count is displayed on the tags page";
    values = "|(default for this tag display)|title|Title|number|Number|text|Text";
}
property string module_tags_opts_count_type {
    des = "Determines how the tag count is displayed in the module";
    values = "|(default for this tag display)|title|Title|number|Number|text|Text";
}

set linklist_support = true;
set tags_aware = true;
set tags_page_count_type = "";
set module_tags_opts_count_type = "";

##=======================================
## Display properties - Presentation
## Misc. options - not implemented yet
##=======================================

property bool reverse_sortorder_recent    { des = "Display entries in reverse-chronological order on my recent entries page"; }
property bool reverse_sortorder_reading   { des = "Display entries in reverse-chronological order on my reading page"; }

set reverse_sortorder_recent = true;
set reverse_sortorder_reading = true;

##=======================================
## Display properties - Basic colors
##=======================================

property Color color_page_background { des = "Page background color"; }
property Color color_page_text { des = "Page text color"; }
property Color color_page_link { des = "Page link color"; }
property Color color_page_link_active { des = "Page active link color"; }
property Color color_page_link_hover { des = "Page hover link color"; }
property Color color_page_link_visited { des = "Page visited link color"; }
property Color color_page_border { des = "Page border color"; }
property Color color_page_details_text { des = "Color for miscellaneous page details text"; }

property Color color_module_background { des = "Module background color"; }
property Color color_module_text { des = "Module text color"; }
property Color color_module_link { des = "Module link color"; }
property Color color_module_link_active { des = "Module active link color"; }
property Color color_module_link_hover { des = "Module hover link color"; }
property Color color_module_link_visited { des = "Module visited link color"; }
property Color color_module_title_background { des = "Module title background color"; }
property Color color_module_title { des = "Module title color"; }
property Color color_module_border { des = "Module border color"; }

property Color color_header_background { des = "Page header background color"; }
property Color color_page_title { des = "Page header title color"; }
property Color color_header_link { des = "Page header link color"; }
property Color color_header_link_active { des = "Page header active link color"; }
property Color color_header_link_hover { des = "Page header hover link color"; }
property Color color_header_link_visited { des = "Page header visited link color"; }
property Color color_footer_background { des = "Page footer background color"; }
property Color color_footer_link { des = "Page footer link color"; }
property Color color_footer_link_active { des = "Page footer active link color"; }
property Color color_footer_link_hover { des = "Page footer hover link color"; }
property Color color_footer_link_visited { des = "Page footer visited link color"; }

property Color color_entry_background { des = "Entry background color"; }
property Color color_entry_text { des = "Entry text color"; }
property Color color_entry_link { des = "Entry link color"; }
property Color color_entry_link_active  { des = "Entry active link color"; }
property Color color_entry_link_hover   { des = "Entry hover link color"; }
property Color color_entry_link_visited { des = "Entry visited link color"; }
property Color color_entry_title_background { des = "Entry title background color"; }
property Color color_entry_title { des = "Entry title color"; }
property Color color_entry_interaction_links_background { des = "Entry interaction links background color"; }
property Color color_entry_interaction_links { des = "Entry footer links color"; }
property Color color_entry_interaction_links_active { des = "Entry active footer links color"; }
property Color color_entry_interaction_links_hover { des = "Entry hover footer links color"; }
property Color color_entry_interaction_links_visited { des = "Entry visited footer links color"; }
property Color color_entry_border { des = "Entry border color"; }

property Color color_comment_title_background { des = "Comment title background color"; }
property Color color_comment_title { des = "Comment title color"; }

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

set color_comment_bar = "#d0d0ff";

##=======================================
## Display properties - Custom colors
##=======================================

# Initialize custom colors, these properties can be selected or overwritten by layout layers, but should not normally be exposed in the wizard.

property string custom_foreground_element {
    des = "Part of entry that will display foreground color, if you have custom colors enabled";
    values = "subject|Entry title|userpic_border|Icon border|postername|Posted by|entry|Entry text|metadata|Mood/music/location|bottom_links|Comment links|custom|Custom template";
    noui = 1;
    }
property string custom_background_element {
    des = "Part of entry that will display background color, if you have custom colors enabled";
    values = "subject|Entry header background|userpic_background|Icon background|entry_border|Entry border|metadata_background|Mood/music/location section|footer_border|Entry footer|custom|Custom template";
    noui = 1;
    }
property string custom_colors_template {
    des = "Your own CSS to use for custom color";
    doc = "Enter a CSS statement with the class or id of CSS elements you want to use for foreground and background, and any CSS instructions.
           Use %%foreground%% for the custom foreground color, and %%background%% for the custom background color
           Each CSS rule should begin with %%new%%";
    example = "%%new%% div.special {border: 1px dotted %%background%%; font-size: 120%; color: %%foreground%%}";
    noui = 1;
    string_mode = "css";
    }

set custom_foreground_element = "";
set custom_background_element = "";
set custom_colors_template = "";

##=======================================
## Display properties - Nav strip colors
##=======================================

property string custom_control_strip_colors {
    des = "Navigation strip colors";
    values = "off|Do not use custom colors|on_gradient|Use custom colors with a background gradient|on_no_gradient|Use custom colors without a background gradient";
    note = "Custom colors can be set in the \"Colors\" section.";
}
property Color control_strip_bgcolor {
    des = "Background color of navigation strip";
    note = "If you don't enable custom navigation strip colors in the \"Presentation\" section, this won't have any effect.";
}
property Color control_strip_fgcolor {
    des = "Text color of navigation strip";
    note = "If you don't enable custom navigation strip colors in the \"Presentation\" section, this won't have any effect.";
}
property Color control_strip_bordercolor {
    des = "Border color of navigation strip";
    note = "If you don't enable custom navigation strip colors in the \"Presentation\" section, this won't have any effect.";
}
property Color control_strip_linkcolor {
    des = "Link color of navigation strip";
    note = "If you don't enable custom navigation strip colors in the \"Presentation\" section, this won't have any effect.";
}

set custom_control_strip_colors = "off";

##=======================================
## Display properties - Fonts
##=======================================

property string font_base {
    des = "Preferred journal font";
    maxlength = 100;
    size = 25;
    note = "For example: Arial or \"Times New Roman\". Leave blank to use the default.";
}
property string font_fallback {
    des = "Alternative journal font";
    values = "sans-serif|Sans-serif|serif|Serif|cursive|Cursive|monospace|Monospaced||Use browser's default";
    note = "This general style will serve as a fallback if your preferred font is unavailable.";
}
property string font_base_size {
    des = "Size of base font";
    size = 3;
}
property string font_base_units {
    des = "Units for base font size";
    values = "em|em|ex|ex|%|%|pt|pt|px|px";
}

property string font_module_heading {
    des = "Preferred font for module headings";
    maxlength = 100;
    size = 25;
    note = "For example: Arial or \"Times New Roman\". Leave blank to use the default.";
}
property string font_module_heading_size {
    des = "Size of module heading font";
    size = 3;
}
property string font_module_heading_units {
    des = "Units for module heading size";
    values = "em|em|ex|ex|%|%|pt|pt|px|px";
}

property string font_module_text {
    des = "Preferred font for module text";
    maxlength = 100;
    size = 25;
    note = "For example: Arial or \"Times New Roman\". Leave blank to use the default.";
}
property string font_module_text_size {
    des = "Size of module text font";
    size = 3;
}
property string font_module_text_units {
    des = "Units for module text size";
    values = "em|em|ex|ex|%|%|pt|pt|px|px";
}

property string font_journal_title {
    des = "Preferred font for journal titles";
    maxlength = 100;
    size = 25;
    note = "For example: Arial or \"Times New Roman\". Leave blank to use the default.";
}
property string font_journal_title_size {
    des = "Size of title font";
    size = 3;
}
property string font_journal_title_units {
    des = "Units for title size";
    values = "em|em|ex|ex|%|%|pt|pt|px|px";
}

property string font_journal_subtitle {
    des = "Preferred font for journal subtitles";
    maxlength = 100;
    size = 25;
    note = "For example: Arial or \"Times New Roman\". Leave blank to use the default.";
}
property string font_journal_subtitle_size {
    des = "Size of subtitle font";
    size = 3;
}
property string font_journal_subtitle_units {
    des = "Units for subtitle size";
    values = "em|em|ex|ex|%|%|pt|pt|px|px";
}

property string font_entry_title {
    des = "Preferred font for entry titles";
    maxlength = 100;
    size = 25;
    note = "For example: Arial or \"Times New Roman\". Leave blank to use the default.";
}
property string font_entry_title_size {
    des = "Size of entry title font";
    size = 3;
}
property string font_entry_title_units {
    des = "Units for entry title size";
    values = "em|em|ex|ex|%|%|pt|pt|px|px";
}

property string font_comment_title {
    des = "Preferred font for comment titles";
    maxlength = 100;
    size = 25;
    note = "For example: Arial or \"Times New Roman\". Leave blank to use the default.";
}
property string font_comment_title_size {
    des = "Size of comment title font";
    size = 3;
}
property string font_comment_title_units {
    des = "Units for comment title size";
    values = "em|em|ex|ex|%|%|pt|pt|px|px";
}

property string font_sources {
    des = "Embedded fonts stylesheet URL";
    note = "The URL must point to a font stylesheet such as 'https://fonts.googleapis.com/css?family=Average+Sans' and not directly at the font file. Embedded fonts can slow page loading time. Blank the field to stop using them. The URL should be a secure URL and begin with 'https://'.";
}

set font_base = "";     # In core, default is not to care. Layouts will probably specify fonts the author likes instead.
set font_fallback = ""; # Default in core is to let the browser handle it.

##=======================================
## Display properties - Images
##=======================================

property string[] image_background_page_group {
    des = "Background image";
    grouptype = "image";
}
set image_background_page_group = [ "image_background_page_url", "image_background_page_repeat", "image_background_page_position" ];
property string image_background_page_url {
    grouped = 1;
}
property string image_background_page_repeat {
    values = "repeat|tile image|no-repeat|don't tile|repeat-x|tile horizontally|repeat-y|tile vertically";
    grouped = 1;
}
property string image_background_page_position {
    values = "top left|top left|top center|top center|top right|top right|center left|center left|center center|center|center right|center right|bottom left|bottom left|bottom center|bottom center|bottom right|bottom right";
    grouped = 1;
    allow_other = 1;
}

property string[] image_background_module_group {
    des = "Module background image";
    grouptype = "image";
}
set image_background_module_group = [ "image_background_module_url", "image_background_module_repeat", "image_background_module_position" ];
property string image_background_module_url {
    grouped = 1;
}
property string image_background_module_repeat {
    values = "repeat|tile image|no-repeat|don't tile|repeat-x|tile horizontally|repeat-y|tile vertically";
    grouped = 1;
}
property string image_background_module_position {
    values = "top left|top left|top center|top center|top right|top right|center left|center left|center center|center|center right|center right|bottom left|bottom left|bottom center|bottom center|bottom right|bottom right";
    grouped = 1;
    allow_other = 1;
}

property string[] image_background_header_group {
    des = "Header background image";
    grouptype = "image";
}
set image_background_header_group = [ "image_background_header_url", "image_background_header_repeat", "image_background_header_position" ];
property string image_background_header_url {
    grouped = 1;
}
property string image_background_header_repeat {
    values = "repeat|tile image|no-repeat|don't tile|repeat-x|tile horizontally|repeat-y|tile vertically";
    grouped = 1;
}
property string image_background_header_position {
    values = "top left|top left|top center|top center|top right|top right|center left|center left|center center|center|center right|center right|bottom left|bottom left|bottom center|bottom center|bottom right|bottom right";
    grouped = 1;
    allow_other = 1;
}
# FIXME: This should be grouped, but then it doesn't get a label.
property int image_background_header_height {
    des = "The height of your header, in pixels.  Use 0 for default.";
    example = "50";
    size = 6;
}
property string[] image_background_entry_group {
    des = "Entry background image";
    grouptype = "image";
}
set image_background_entry_group = [ "image_background_entry_url", "image_background_entry_repeat", "image_background_entry_position" ];
property string image_background_entry_url {
    grouped = 1;
}
property string image_background_entry_repeat {
    values = "repeat|tile image|no-repeat|don't tile|repeat-x|tile horizontally|repeat-y|tile vertically";
    grouped = 1;
}
property string image_background_entry_position {
    values = "top left|top left|top center|top center|top right|top right|center left|center left|center center|center|center right|center right|bottom left|bottom left|bottom center|bottom center|bottom right|bottom right";
    grouped = 1;
    allow_other = 1;
}

property string[] separator_image_group {
    des = "Separator";
    grouptype = "image";
}
set separator_image_group = [ "separator_image_url", "separator_image_repeat", "separator_image_position" ];
property string separator_image_url {
    grouped = 1;
}
property string separator_image_repeat {
    values = "repeat|tile image|no-repeat|don't tile|repeat-x|tile horizontally|repeat-y|tile vertically";
    grouped = 1;
}
property string separator_image_position {
    values = "top left|top left|top center|top center|top right|top right|center left|center left|center center|center|center right|center right|bottom left|bottom left|bottom center|bottom center|bottom right|bottom right";
    grouped = 1;
    allow_other = 1;
}

##=======================================
## Display properties - Modules
##=======================================

property string module_layout_sections {
    des = "Map module sections to user-friendly names for the wizard. Designers may want to override this.";
    example = "none|(none)|one|Main Module Section|two|Secondary Module Section";
    noui = 1;
}

property string[][]{} module_sections {
    des = "Build up a list of module sections and the ordering within them";
    noui = 1;
}

property string{} grouped_property_override {
    des = "Use to override a specific property in a group of properties (for example, which sections a module should show up in), by mapping the default property to a new property you create";
    example = """{ "module_something_section" => "module_something_section_override" } (module_something_section_override is a new property created in your layout layer) """;
    noui = 1;
}

property string[] module_userprofile_group {
    des = "Profile";
    grouptype = "module";
}
set module_userprofile_group = ["module_userprofile_show", "module_userprofile_order", "module_userprofile_section", "module_userprofile_opts_group"];
property bool module_userprofile_show { grouped = 1; }
property int  module_userprofile_order { grouped = 1; }
property string module_userprofile_section { grouped = 1; }

property string[] module_userprofile_opts_group {
    grouptype = "moduleopts";
    grouped = 1;
}
set module_userprofile_opts_group = ["module_userprofile_opts_userpic", "module_userprofile_opts_name", "module_userprofile_opts_website"];
property bool module_userprofile_opts_userpic { grouped = 1; label = "Display Default Icon"; }
property bool module_userprofile_opts_name { grouped = 1; label = "Display Name"; }
property bool module_userprofile_opts_website { grouped = 1; label = "Display Website"; }

property string[] module_navlinks_group {
    des = "Navigation";
    grouptype = "module";
}
set module_navlinks_group = ["module_navlinks_show", "module_navlinks_order", "module_navlinks_section"];
property bool module_navlinks_show { grouped = 1; }
property int  module_navlinks_order { grouped = 1; }
property string module_navlinks_section { grouped = 1; }

property string[] module_calendar_group {
    des = "Calendar";
    grouptype = "module";
}
set module_calendar_group = ["module_calendar_show", "module_calendar_order", "module_calendar_section", "module_calendar_opts_group"];
property bool module_calendar_show { grouped = 1; }
property int  module_calendar_order { grouped = 1; }
property string module_calendar_section { grouped = 1; }
property string[] module_calendar_opts_group {
    grouptype = "moduleopts";
    grouped = 1;
}
set module_calendar_opts_group = ["module_calendar_opts_type"];
property string module_calendar_opts_type { grouped = 1; values = "|Table|horizontal|Horizontal"; des = "Type of calendar display"; }

property string[] module_links_group {
    des = "Link List";
    grouptype = "module";
}
set module_links_group = ["module_links_show", "module_links_order", "module_links_section"];
property bool module_links_show { grouped = 1; }
property int  module_links_order { grouped = 1; }
property string module_links_section { grouped = 1; }

property string[] module_syndicate_group {
    des = "Syndication";
    grouptype = "module";
}
set module_syndicate_group = ["module_syndicate_show", "module_syndicate_order", "module_syndicate_section"];
property bool module_syndicate_show { grouped = 1; }
property int  module_syndicate_order { grouped = 1; }
property string module_syndicate_section { grouped = 1; }

property string[] module_tags_group {
    des = "Tags";
    grouptype = "module";
}
set module_tags_group = ["module_tags_show", "module_tags_order", "module_tags_section", "module_tags_opts_group"];
property bool module_tags_show { grouped = 1; }
property int  module_tags_order { grouped = 1; }
property string module_tags_section { grouped = 1; }

property string[] module_tags_opts_group {
    grouptype = "moduleopts";
    grouped = 1;
}
set module_tags_opts_group = ["module_tags_opts_limit","module_tags_opts_type"];
property string module_tags_opts_type { grouped = 1; des = "Type of tags display"; values="|List|cloud|Cloud|multi|Multilevel"; }
property int module_tags_opts_limit { grouped = 1; des = "Number of most popular tags to display. Type '0' to display all tags."; }

property string[] module_pagesummary_group {
    des = "Page Summary";
    grouptype = "module";
}
set module_pagesummary_group = ["module_pagesummary_show", "module_pagesummary_order", "module_pagesummary_section", "module_pagesummary_opts_group"];
property bool module_pagesummary_show { grouped = 1; }
property int  module_pagesummary_order { grouped = 1; }
property string module_pagesummary_section { grouped = 1; }
property string[] module_pagesummary_opts_group { grouptype="moduleopts"; grouped = 1; }
set module_pagesummary_opts_group = ["module_pagesummary_opts_comments_tooltip"];
property bool module_pagesummary_opts_comments_tooltip {
    grouped = 1;
    des = "Display comment counts in page summary as tooltip (otherwise the number will appear next to the entry title)";
    label = "Display the comment count as a tooltip";
}

property string[] module_active_group {
    des = "Recent active entries";
    note = "This is only visible on paid journals.";
    grouptype = "module";
}
set module_active_group = ["module_active_show", "module_active_order", "module_active_section"];
property bool module_active_show { grouped = 1; }
property int  module_active_order { grouped = 1; }
property string module_active_section { grouped = 1; }

property string[] module_time_group {
    des = "Time Loaded";
    grouptype = "module";
}
set module_time_group = ["module_time_show", "module_time_order", "module_time_section"];
property bool module_time_show { grouped = 1; }
property int  module_time_order { grouped = 1; }
property string module_time_section { grouped = 1; }

property string[] module_poweredby_group {
    des = "Powered By";
    grouptype = "module";
}
set module_poweredby_group = ["module_poweredby_show", "module_poweredby_order", "module_poweredby_section"];
property bool module_poweredby_show { grouped = 1; }
property int  module_poweredby_order { grouped = 1; }
property string module_poweredby_section { grouped = 1; }

property string[] module_customtext_group {
    des = "Custom Text";
    grouptype = "module";
}
set module_customtext_group = ["module_customtext_show", "module_customtext_order", "module_customtext_section"];
property bool module_customtext_show { grouped = 1; }
property int  module_customtext_order { grouped = 1; }
property string module_customtext_section { grouped = 1; }

property string[] module_credit_group {
    des = "Style Credit";
    grouptype = "module";
}
set module_credit_group = ["module_credit_show", "module_credit_order", "module_credit_section"];
property bool module_credit_show { grouped = 1; }
property int module_credit_order { grouped = 1; }
property string module_credit_section { grouped = 1; }
property string{}[] layout_authors { noui = 1;  des = "The style author (or authors). Accepts 'name', 'url', 'type'"; }
property string{}[] theme_authors { noui = 1;  des = "The theme author (or authors). Accepts 'name', 'url', 'type'"; }
property string{}[] layout_resources { noui = 1; des = "Resources used in the layout. Accepts 'name', 'url'"; }

property string[] module_search_group {
    des = "Search";
    note = "This is only visible on paid journals.";
    grouptype = "module";
}
set module_search_group = ["module_search_show", "module_search_order", "module_search_section"];
property bool module_search_show { grouped = 1; }
property int module_search_order { grouped = 1; }
property string module_search_section { grouped = 1; }

property string[] module_cuttagcontrols_group {
    des = "Cut Tag Controls";
    grouptype = "module";
}
set module_cuttagcontrols_group = ["module_cuttagcontrols_show", "module_cuttagcontrols_order", "module_cuttagcontrols_section"];
property bool module_cuttagcontrols_show { grouped = 1; }
property int  module_cuttagcontrols_order { grouped = 1; }
property string module_cuttagcontrols_section { grouped = 1; }

property string[] module_subscriptionfilters_group {
    des = "Subscription Filters";
    grouptype = "module";
}
set module_subscriptionfilters_group = ["module_subscriptionfilters_show", "module_subscriptionfilters_order", "module_subscriptionfilters_section"];
property bool module_subscriptionfilters_show { grouped = 1; }
property int module_subscriptionfilters_order { grouped = 1; }
property string module_subscriptionfilters_section { grouped = 1; }

set module_layout_sections = "none|(none)|one|Main Module Section|two|Secondary Module Section";
set module_userprofile_show = true;
set module_userprofile_order = 1;
set module_userprofile_section = "one";
set module_userprofile_opts_userpic = true;
set module_userprofile_opts_name = true;
set module_userprofile_opts_website = true;
set module_navlinks_show = true;
set module_navlinks_order = 2;
set module_navlinks_section = "one";
set module_calendar_show = true;
set module_calendar_order = 3;
set module_calendar_section = "one";
set module_links_show = true;
set module_links_order = 4;
set module_links_section = "one";
set module_syndicate_show = true;
set module_syndicate_order = 5;
set module_syndicate_section = "one";
set module_tags_show = true;
set module_tags_order = 6;
set module_tags_section = "one";
set module_tags_opts_type = "";
set module_tags_opts_limit = 50;
set module_pagesummary_show = true;
set module_pagesummary_order = 7;
set module_pagesummary_section = "one";
set module_pagesummary_opts_comments_tooltip = true;
set module_active_show = true;
set module_active_order = 8;
set module_active_section = "one";
set module_time_show = true;
set module_time_order = 11;
set module_time_section = "two";
set module_poweredby_show = true;
set module_poweredby_order = 12;
set module_poweredby_section = "two";
set module_customtext_show = false;
set module_customtext_order = 13;
set module_customtext_section = "one";
set module_credit_show = true;
set module_credit_order = 14;
set module_credit_section = "one";
set layout_authors = [];
set theme_authors = [];
set layout_resources = [];
set module_search_show = true;
set module_search_order = 15;
set module_search_section = "one";
set module_cuttagcontrols_show = true;
set module_cuttagcontrols_order = 16;
set module_cuttagcontrols_section = "one";
set module_subscriptionfilters_show = false;
set module_subscriptionfilters_order = 17;
set module_subscriptionfilters_section = "one";

##=======================================
## Display properties - Text
## Navigation
##=======================================

property string text_view_recent {
    des = "Text used to link to the 'Recent Entries' page";
    maxlength = 40;
    "size" = 15;
    example = "Recent Entries";
}
property string text_view_recent_tagged {
    des = "Text used to link to a tag-filtered version of the 'Recent Entries' page";
    maxlength = 40;
    "size" = 15;
    example = "Entries tagged with";
}
property string text_view_archive {
    des = "Text used to link to the 'Archive' page";
    maxlength = 40;
    "size" = 15;
    example = "Archive";
}
property string text_view_friends {
    des = "Text used to link to the 'Reading' page";
    maxlength = 40;
    "size" = 15;
    example = "Reading";
}
property string text_view_friends_comm {
    des = "Text used to link to the 'Reading' page for a community";
    maxlength = 40;
    "size" = 15;
    example = "Member Posts";
}
property string text_view_friends_filter {
    des = "Text used to link to the 'Reading' page with an unnamed filter in effect";
    maxlength = 40;
    "size" = 15;
    example = "Reading (Custom filter)";
}
property string text_view_network {
    des = "Text used to link to the 'Network' page";
    maxlength = 40;
    "size" = 15;
    example = "Network";
}
property string text_view_network_filter {
    des = "Text used to link to the 'Network' page with an unnamed filter in effect";
    maxlength = 40;
    "size" = 15;
    example = "Network (Custom filter)";
}
property string text_view_tags {
    des = "Text used to link to the 'Tags' page";
    maxlength = 40;
    "size" = 15;
    example = "Tags";
}
property string text_view_memories {
    des = "Text used to link to the 'Memories' page";
    maxlength = 40;
    "size" = 15;
    example = "Memories";
}
property string text_view_monthpage {
    des = "Text used to link to a 'Month' page";
    maxlength = 40;
    "size" = 15;
    example = "Month";
    "note" = "\$*text_view_month is already used";
}
property string text_view_day {
    des = "Text used to link to a 'Day' page";
    maxlength = 40;
    "size" = 15;
    example = "Day";
}
property string text_view_entry {
    des = "Text used to link to an 'Entry' page";
    maxlength = 40;
    "size" = 15;
    example = "Entry";
}
property string text_view_userinfo {
    des = "Text used to link to the 'Profile' page";
    maxlength = 40;
    "size" = 15;
    example = "Profile";
}
property string text_view_icons {
    des = "Text used to link to the 'Icons' page";
    maxlength = 40;
    "size" = 15;
    example = "Icons";
}
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_page_top {
    des = "Text to show in a link which takes you back to the top of the page";
    maxlength = 20;
    "size" = 15;
    example = "Top of page";
}

set text_view_recent = "Recent Entries";
set text_view_recent_tagged = "Entries tagged with";
set text_view_archive = "Archive";
set text_view_friends = "Reading";
set text_view_friends_comm = "Member Posts";
set text_view_friends_filter = "Reading (Custom filter)";
set text_view_network = "Network";
set text_view_network_filter = "Network (Custom filter)";
set text_view_tags = "Tags";
set text_view_memories = "Memories";
set text_view_monthpage = "Month";
set text_view_day = "Day";
set text_view_entry = "Entry";
set text_view_userinfo = "Profile";
set text_view_icons = "Icons";
set text_skiplinks_back = "Previous #";
set text_skiplinks_forward = "Next #";
set text_page_top = "Top of page";

##=======================================
## Display properties - Text
## Module headings
##=======================================

property string text_module_userprofile {
    des = "Text for the 'Profile' heading";
    maxlength = 50;
    size = 20;
    example = "Profile";
}
property string text_module_links {
    des = "Text for the 'Links' heading";
    maxlength = 50;
    size = 20;
    example = "Links";
}
property string text_module_syndicate {
    des = "Text for the 'Syndicate' heading";
    maxlength = 50;
    size = 20;
    example = "Syndicate";
}
property string text_module_tags {
    des = "Text for the 'Tags' heading";
    maxlength = 50;
    size = 20;
    example = "Tags";
}
property string text_module_popular_tags {
    des = "Text for the 'Most Popular Tags' heading";
    maxlength = 50;
    size = 20;
    example = "Most Popular Tags";
    note = "Used when there are more tags than shown.";
}
property string text_module_pagesummary {
    des = "Text for the 'Page Summary' heading";
    maxlength = 50;
    size = 20;
    example = "Page Summary";
}
property string text_module_active_entries {
    des = "Text for the 'Active Entries' heading";
    note = "This is only visible on paid journals.";
    maxlength = 50;
    size = 20;
    example = "Active Entries";
}
property string text_module_customtext {
    noui = 1;
    des = "Text for the 'Custom Text' heading";
    maxlength = 50;
    size = 20;
    example = "Custom Text";
}
property string text_module_customtext_url {
    noui = 1;
    des = "URL for the 'Custom Text' heading link";
    maxlength = 100;
    size = 30;
    example = "https://www.dreamwidth.org";
}
property string text_module_customtext_content {
    noui = 1;
    des = "Text for the 'Custom Text' box";
    cols = 50;
    rows = 10;
    string_mode = "html";
}
property string text_module_credit {
    des = "Text for the 'Style Credit' heading";
    maxlength = 50;
    size = 20;
    example = "Style Credit";
}
property string text_module_cuttagcontrols {
    des = "Text for the 'Expand Cut Tags' heading";
    maxlength = 50;
    size = 20;
    example = "Expand Cut Tags";
}
property string text_module_search {
    des = "Text for the 'Search' heading";
    note = "This is only visible on paid journals.";
    maxlength = 50;
    size = 20;
    example = "Search";
}
property string text_module_search_btn {
    des = "Text for the 'Search' button";
    maxlength = 50;
    size = 20;
    example = "Search";
}
property string text_module_subscriptionfilters {
    des = "Text for the 'Subscription Filters' box";
    maxlength = 50;
    size = 20;
    example = "Subscription Filters";
}

# Modules without titles: navlinks, time, poweredby
# Calendar uses month and year

set text_module_userprofile = "Profile";
set text_module_links = "Links";
set text_module_syndicate = "Syndicate";
set text_module_tags = "Tags";
set text_module_popular_tags = "Most Popular Tags";
set text_module_pagesummary = "Page Summary";
set text_module_active_entries = "Active Entries";
set text_module_customtext = "Custom Text";
set text_module_customtext_url = "";
set text_module_customtext_content = "";
set text_module_credit = "Style Credit";
set text_module_cuttagcontrols = "Expand Cut Tags";
set text_module_search = "Search";
set text_module_search_btn = "Search";
set text_module_subscriptionfilters = "Subscription Filters";

##=======================================
## Display properties - Text
## Metadata
##=======================================

property string text_meta_location {
    des = "Text for 'Current Location'";
}
property string text_meta_music {
    des = "Text for 'Current Music'";
}
property string text_meta_mood {
    des = "Text for 'Current Mood'";
}
property string text_meta_groups {
    des = "Text for 'Custom Access Groups'";
}
property string text_meta_xpost {
    des = "Text for 'Crossposts'";
}
property string text_tags {
    des = "Text for 'Tags' header for an entry";
    example = "Tags:";
}

property string text_stickyentry_subject { des = "Text that appears before the subject of the sticky entry"; }
property string text_admin_post_subject { des = "Text that appears before the subject of a administrator post"; }
property string text_admin_post_note { des = "Text that appears before the subject of a administrator post"; }

set text_meta_groups = "Custom Access Groups:";
set text_meta_location = "Current Location:";
set text_meta_mood = "Current Mood:";
set text_meta_music = "Current Music:";
set text_meta_xpost = "Crossposts:";
set text_tags = "Tags:";
set text_stickyentry_subject = "Sticky:";
set text_admin_post_subject = "Admin Post: ";
set text_admin_post_note = "as admin";

##=======================================
## Display properties - Text
## Entry management
##=======================================

property string text_entry_prev {
    des = "Text to link to the previous entry";
    example = "\"Previous Entry\" or \"Previous\"";
}

property string text_entry_next {
    des = "Text to link to the next entry";
    example = "\"Next Entry\" or \"Next\"";
}

property string text_edit_entry { des = "Text to edit an entry"; }

property string text_edit_tags { des = "Text to edit tags for an entry"; }

property string text_mem_add {
    des = "Text to add an entry into memories";
    example = "\"Add Memory\" or \"Memory\"";
}

property string text_tell_friend {
    des = "Text to tell someone about an entry";
    example = "\"Share This Entry\" or \"Share\"";
}
property string text_watch_comments { des = "Text to track events on an entry"; }

property string text_unwatch_comments { des = "Text to stop tracking events on an entry"; }

set text_entry_prev = "Previous Entry";
set text_entry_next = "Next Entry";
set text_edit_entry = "Edit Entry";
set text_edit_tags = "Edit Tags";
set text_mem_add = "Add Memory";
set text_tell_friend = "Share This Entry";
set text_watch_comments = "Track This";
set text_unwatch_comments = "Untrack This";

##=======================================
## Display properties - Text
## Entry interaction
##=======================================

property string text_read_comments {
    des = "Text to read comments";
    format = "plurals";
    example = "1 comment // # comments";
}

property string text_read_comments_friends {
    des = "Text to read comments from your Reading page";
    format = "plurals";
    example = "1 comment // # comments";
}

property string text_read_comments_screened_visible {
    des = "Text for number of unscreened comments on an entry with screened comments";
    example = "1 visible // # visible";
}

property string text_read_comments_screened {
    des = "Text for number of screened comments on an entry";
    format = "plurals";
    example = "1 screened comment // # screened comments";
}

property string text_post_comment {
    des = "Text to leave a comment";
    example = "Reply";
}
property string text_post_comment_friends {
    des = "Text to leave a comment from your Reading page";
    example = "Reply";
}
property string text_max_comments {
    des = "Text when entry has reached a comment maximum";
    example = "Maximum comments reached";
}

property string text_permalink {
    des = "Text for an entry's permanent link";
    note = "This is displayed when the entry subject contains links";
    maxlength = 50;
    size = 20;
}

set text_read_comments = "1 comment // # comments";
set text_read_comments_friends = "1 comment // # comments";
set text_read_comments_screened_visible = "1 visible // # visible";
set text_read_comments_screened = "1 screened comment // # screened comments";
set text_post_comment = "Reply";
set text_post_comment_friends = "Reply";
set text_max_comments = "Maximum comments reached";
set text_permalink = "Link";

##=======================================
## Display properties - Text
## Comment interaction
##=======================================

property string text_comment_link {
    des = "Text to link to the comment";
    example = "Link";
    maxlength = "50";
}
property string text_comment_reply {
    des = "Text to link to reply 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_threadroot {
    des = "Text to link to the entire thread this comment is part of";
    example = "Thread from start";
    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";
}
property string text_comment_expand {
    des = "Text to expand a collapsed comment thread";
    example = "Expand";
    maxlength = "50";
}
property string text_comment_hide {
    des = "Text to hide a comment thread";
    example = "Hide 1 comment // Hide # comments";
    maxlength = "50";
}
property string text_comment_unhide {
    des = "Text to unhide a comment thread";
    example = "Show 1 comment // Show # comments";
    maxlength = "50";
}
property string text_commentview_flat {
   des = "Text to show comments in flat view";
   example = "Flat";
   maxlength = "50";
}
property string text_commentview_threaded {
   des = "Text to show comments in threaded view";
   example = "Threaded";
   maxlength = "50";
}
property string text_commentview_toponly {
   des = "Text to show only top-level comments";
   example = "Top-Level Comments Only";
   maxlength = "50";
}
set text_comment_link = "Link";
set text_comment_reply = "Reply";
set text_comment_frozen = "Frozen";
set text_comment_threadroot = "Thread from start";
set text_comment_parent = "Parent";
set text_comment_thread = "Thread";
set text_comment_expand = "Expand";
set text_comment_hide = "Hide 1 comment // Hide # comments";
set text_comment_unhide = "Show 1 comment // Show # comments";
set text_commentview_flat = "Flat";
set text_commentview_threaded = "Threaded";
set text_commentview_toponly = "Top-Level Comments Only";

##=======================================
## Display properties - Text
## Tags page & Tags module
##=======================================

property string text_tags_page_header {
    des = "Text for the header of the Tags page";
    example = "Visible Tags";
}
property string text_tags_item_sep {
    des = "Text to separate items in a list of tags";
    example = ",";
}
property string text_tagsmultilevel_delimiter {
    des = "Text to use as a delimiter for multi-level tags";
    example = ":";
}
property string text_tag_uses {
    des = "Text to show how many times a tag has been used";
    format = "plurals";
    example = "1 use // # uses";
}

set text_tags_page_header = "Visible Tags";
set text_tags_item_sep = ",";
set text_tagsmultilevel_delimiter = ":";
set text_tag_uses = "1 use // # uses";

##=======================================
## Display properties - Text
## Icons page
##=======================================

property string text_icons_page_header {
    des = "Text for the header of the Icons page";
    example = "Current Icons";
}
property string text_icons_page_empty_header {
    des = "Text for the header of the Icons page when it is empty";
    example = "No Icons";
}
property string text_icons_sort_upload {
    des = "Text for sort by upload order link";
    example = "Upload Order";
}
property string text_icons_sort_keyword {
    des = "Text for sort by keyword link";
    example = "Keyword Order";
}
property string text_icons_default {
    des = "Text for the default icon";
    example = "Default";
}
property string text_icons_inactive {
    des = "Text for inactive icons";
    example = "Inactive";
}
property string text_icons_keywords {
    des = "Text for the list of keywords";
    example = "Keywords:";
}
property string text_icons_keyword_sep {
    des = "Text to separate items in a list of keywords";
    example = ",";
}
property string text_icons_comment {
    des = "Text for the icon comment";
    example = "Comment:";
}
property string text_icons_description {
    des = "Text for the icon description";
    example = "Description:";
}

property string text_icon_manage {
    des = "Text to use to link to this journal's icon management page.";
    example = "Manage Icons";
}

set text_icons_page_header = "Current Icons";
set text_icons_page_empty_header = "No Icons";
set text_icons_sort_upload = "Upload Order";
set text_icons_sort_keyword = "Keyword Order";
set text_icons_default = "Default";
set text_icons_inactive = "Inactive";
set text_icons_keywords = "Keywords:";
set text_icons_keyword_sep = ",";
set text_icons_comment = "Comment:";
set text_icons_description = "Description:";
set text_icon_manage = "Manage Icons";

##=======================================
## Display properties - Text
## Misc strings - Modules
## Not popular enough to expose in wizard
##=======================================

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

property string text_calendar_num_entries {
    des = "Text to show how many entries were made on a particular day";
    format = "plurals";
    example = "1 entry // # entries";
}

property string text_linklist_manage {
    des = "Text to use to link to this journal's links list management page.";
    example = "Manage Links";
}
property string text_tags_manage {
    des = "Text to use to link to this journal's tags management page.";
    example = "Manage Tags";
}

property string text_subscriptionfilters_manage {
    des = "Text to use to link to this journal's subscription filter management page.";
    example = "Manage Filters";
}

property string text_read_comments_threads {
    des = "Link text to read comment threads from an entry";
    format = "plurals";
    example = "1 response // # responses";
}

property string text_generated_on {
    noui = 1;
    des = "Text used to describe at what time the page was generated";
    example = "Generated on";
}

property string text_powered_by { noui = 1; des = "Text to describe what site hosts the journal"; }

property string text_layout_authors { noui = 1; des = "Text label for the style author"; }
property string text_base_layout_authors {
    noui = 1;
    des = "Text label for the base style author";
    note = "Used when style and theme have different authors";
}
property string text_theme_authors { noui = 1; des = "Text label for the theme author"; }
property string text_layout_resources { noui = 1; des = "Text label for the layout resources"; }

property string text_cuttagcontrols_nocuttags {
    des = "Text to show when there are no cuts on the page";
    example = "No cut tags";
}

set text_website_default_name = "My Website";
set text_calendar_num_entries = "1 entry // # entries";
set text_linklist_manage = "Manage Links";
set text_tags_manage = "Manage Tags";
set text_subscriptionfilters_manage = "Manage Filters";
set text_read_comments_threads = "1 response // # responses";
set text_generated_on = "Page generated";
set text_powered_by = "Powered by";
set text_layout_authors = "Style:";
set text_base_layout_authors = "Base style:";
set text_theme_authors = "Theme:";
set text_layout_resources = "Resources:";
set text_cuttagcontrols_nocuttags = "No cut tags";

##=======================================
## Display properties - Text
## Misc strings - Navigation
## Not popular enough to expose in wizard
##=======================================

property string text_default_separator {
    des = "Text used to separate items";
    maxlength = 5;
    "size" = 5;
    example = " | ";
}

property string text_noentries_recent {
    des = "Text to display when there are no entries on the recent page";
    maxlength = 255;
    size = 50;
    noui = 1;
}
property string text_noentries_read {
    des = "Text to display when there are no entries on the reading page";
    maxlength = 255;
    size = 50;
    noui = 1;
}
property string text_noentries_day {
    des = "Text to display when there are no entries on the day view";
    maxlength = 255;
    size = 50;
    noui = 1;
}
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_default_separator = " | ";
set text_noentries_recent = "There are no entries to display.";
set text_noentries_read = "There are no earlier entries to display. This page displays only the most recent 1000 entries posted within the last 14 days.";
set text_noentries_day = "There were no entries on this day.";
set text_day_prev = "Previous Day";
set text_day_next = "Next Day";

##=======================================
## Display properties - Text
## Misc strings - Entries & Comments
## Not popular enough to expose in wizard
##=======================================

property string text_nosubject {
    des = "Text to replace a subject line when no subject is specified";
    maxlength = 20;
    size = 10;
    example = "No Subject";
}

property string text_nosubject_screenreader {
    des = "Text to replace a subject line in screenreaders when no subject is specified";
    maxlength = 20;
    size = 10;
    example = "No Subject";
    note = "This only is visible to screenreaders or with CSS disabled. Do not set this to empty.";
    noui = 1;
}

property string text_icon_alt_protected { noui = 1; des = "Alternative text for icons of friends-only entries"; }
property string text_icon_alt_private   { noui = 1; des = "Alternative text for icons of private entries"; }
property string text_icon_alt_groups    { noui = 1; des = "Alternative text for icons of custom friends group entries"; }
property string text_icon_alt_nsfw      { noui = 1; des = "Alternative text for icons of NSFW entries"; }
property string text_icon_alt_18        { noui = 1; des = "Alternative text for icons of 18+ entries"; }
property string text_icon_alt_sticky_entry        { noui = 1; des = "Alternative text for icons of sticky entries"; }
property string text_icon_alt_admin_post        { noui = 1; des = "Alternative text for icons of administrator posts"; }

property string text_posting_in {
    des = "Text when user is posting in a community";
    example = " posting in ";
}

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";
}

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";
}
property string text_reply_nocomments_header {
    des = "Heading text that explains that comments are disabled";
    example = "Comments Disabled:";
    maxlength = "50";
}
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";
}
property string text_comments_disabled_maintainer {
    des = "Text shown instead of comments if a community administrator has disabled comments";
    example = "Comments disabled by an administrator of this community";
}

property string comment_page_prev {
    des = "Text to link the previous comment page";
    example = "Previous Comment Page";
}
property string comment_page_next {
    des = "Text to link the previous comment page";
    example = "Previous Comment Page";
}

property string text_screened {
   des = "The text used when a comment is screened";
   example = "(screened comment)";
}
property string text_deleted {
   des = "The text used when a comment has been deleted";
   example = "(deleted comment)";
}
property string text_fromsuspended {
   des = "The text used when the comment's author has been suspended";
   example = "(reply from suspended user)";
}
property string text_frozen {
   des = "The text used when a comment is frozen";
   example = "(frozen)";
}
property string text_poster_anonymous {
    des = "The placeholder used when something is posted by an anonymous user";
    example = "(Anonymous)";
}

property string text_comment_posted { des = "Text to display when a comment has just been posted by the user"; }

property string text_comment_from {
    des = "Text of the 'from' header in comments";
    example = "From:";
    maxlength = "20";
}
property string text_openid_from {
  des = "Text to indicate which site this OpenID account originally posted a comment on";
  example = "from";
  noui = 1;
}
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";
}
property string text_comment_edittime {
    des = "Text of the 'Edited' string at the bottom of edited comments";
    example = "Edited";
    maxlength = "20";
}
property string time_ago_seconds {
    des = "# seconds";
    noui = 1;
}
property string time_ago_minutes {
    des = "# minutes";
    noui = 1;
}
property string time_ago_hours {
    des = "# hours";
    noui = 1;
}
property string time_ago_days {
    des = "# seconds";
    noui = 1;
}

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

property string text_multiform_des { des = "Text on the multiform action line."; }

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

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

property string text_multiform_opt_unscreen_to_reply { des = "Text for the comment unscreening action it's showed instead of reply-link for screened comments"; }

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

property string text_multiform_opt_unscreen_to_reply { des = "Text for the comment unscreening action it's showed instead of reply-link for screened comments"; }

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

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

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

property string text_multiform_opt_deletespam { des = "Text for the comment delete and mark as spam action"; }

property string text_multiform_conf_delete { des = "Text for the confirming mass-delete action"; }

property string text_multiform_opt_track { des = "Text for the comment tracking action"; }

property string text_multiform_opt_untrack { des = "Text for the comment untracking action"; }

property string text_multiform_opt_edit { des = "Text for the comment editing action"; }

property string text_replyform_header { des = "Text for the heading above the form on the reply page"; }

property string text_new_comment_title { des = "Text for the page title when writing a new comment"; }

property string text_edit_comment_title { des = "Text for the page title when editing an existing comment"; }

set text_nosubject = "(no subject)";
set text_nosubject_screenreader = "no subject";

set text_icon_alt_protected = "[protected post]";
set text_icon_alt_private = "[private post]";
set text_icon_alt_groups = "[custom friends groups post]";
set text_icon_alt_nsfw = "[NSFW]";
set text_icon_alt_18 = "[18+]";
set text_icon_alt_sticky_entry = "[sticky entry]";
set text_icon_alt_admin_post = "[admin post]";

set text_posting_in = " posting in ";

set text_reply_back = "Read Comments";
set text_reply_nocomments_header = "Comments Disabled:";
set text_reply_nocomments = "Comments have been disabled for this post.";
set text_comments_disabled_maintainer = "Comments disabled by an administrator of this community";

set comment_page_prev = "&lt;&lt";
set comment_page_next = "&gt;&gt";

set text_screened = "(screened comment)";
set text_deleted = "(deleted comment)";
set text_fromsuspended = "(reply from suspended user)";
set text_frozen = "(frozen)";
set text_poster_anonymous = "(Anonymous)";

set text_comment_posted = "Comment successfully posted.";

set text_comment_from = "From:";
set text_openid_from = "from";
set text_comment_date = "Date:";
set text_comment_ipaddr = "IP Address:";
set text_comment_edittime = "Edited";

set time_ago_seconds = "1 second // # seconds";
set time_ago_minutes = "1 minute // # minutes";
set time_ago_hours = "1 hour // # hours";
set time_ago_days = "1 day // # days";

set text_multiform_conf_delete = "Delete selected comments?";
set text_multiform_opt_track = "Track This";
set text_multiform_opt_untrack = "Untrack This";
set text_multiform_opt_edit = "Edit";
set text_multiform_check = "Select:";
set text_multiform_des = "Mass action on selected comments:";
set text_multiform_btn = "Perform Action";
set text_multiform_opt_unscreen = "Unscreen";
set text_multiform_opt_unscreen_to_reply = "Unscreen to reply";
set text_multiform_opt_screen = "Screen";
set text_multiform_opt_unscreen_to_reply = "Unscreen to reply";
set text_multiform_opt_unfreeze = "Unfreeze";
set text_multiform_opt_freeze = "Freeze";
set text_multiform_opt_delete = "Delete";
set text_multiform_opt_deletespam = "Delete as Spam";

set text_replyform_header = "Comment Form";

set text_new_comment_title = "Reply";
set text_edit_comment_title = "Edit reply";

##=======================================
## Display properties - Text
## Misc strings - Archive pages
## Not popular enough to expose in wizard
##=======================================

property string text_month_screened_comments { des = "Text to indicate there are screened comments"; }

property string text_month_form_btn { des = "Text used for Submit button in MonthPage selector"; }

set text_view_month = "View Subjects";
set text_month_screened_comments = "w/ Screened";
set text_month_form_btn = "View";

##=======================================
## Properties - Custom CSS
##=======================================

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

property bool include_default_stylesheet {
    des = "Use layout's stylesheet(s)";
    note = "Disable this only if you want to re-style this layout completely from scratch using a custom stylesheet.";
}

property string linked_stylesheet {
    des = "Custom stylesheet URL";
    note = "If you have a custom external stylesheet that you'd like to use, enter its URL here.";
}

property string custom_css {
    des = "Use embedded CSS";
    note = "If you'd like to add custom CSS to this style, enter it here.";
    cols = 60;
    rows = 40;
    string_mode = "css";
}

set external_stylesheet = true;
set include_default_stylesheet = true;
set linked_stylesheet = "";
set custom_css = "";

##=======================================
## Language defaults (noui)
##=======================================

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_iso { noui = 1; des = "ISO date format."; }

property string lang_fmt_date_short { noui = 1; des = "Short date format. All numeric; no leading zeroes; month before day."; }

property string lang_fmt_date_short_dayfirst { noui = 1; des = "Short date format. All numeric; no leading zeroes; day before month."; }

property string lang_fmt_date_short_dayfirst_full { noui = 1; des = "Short date format. All numeric; with leading zeroes; day before month."; }

property string lang_fmt_date_med { noui = 1; des = "Medium date format. Abbreviated month name; ordinal number; no day of the week; month before day."; }

property string lang_fmt_date_med_dayfirst { noui = 1; des = "Medium date format. Abbreviated month name; no day of the week; day before month."; }

property string lang_fmt_date_med_day_iso { noui = 1; des = "ISO date format and full day of the week."; }

property string lang_fmt_date_med_day { noui = 1; des = "Medium date format with day of the week. Abbreviated month name; abbreviated day of the week; ordinal number; month before day."; }

property string lang_fmt_date_long { noui = 1; des = "Long date format. Full month name; ordinal number; no day of the week; month before day."; }

property string lang_fmt_date_long_dayfirst { noui = 1; des = "Long date format. Full month name; no day of the week; day before month."; }

property string lang_fmt_date_long_day { noui = 1; des = "Long date format. Full month name; ordinal number; full day of the week; month before day."; }

property string lang_fmt_date_long_day_dayfirst { noui = 1; des = "Long date format. Full month name; full day of the week; day before month."; }

property string lang_fmt_time_short { noui = 1; des = "12 hours time format."; }

property string lang_fmt_time_short_24 { noui = 1; des = "24 hours time format."; }

property string lang_fmt_month_short { noui = 1; des = "Short month format."; }

property string lang_fmt_month_med { noui = 1; des = "Medium month format."; }

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

set lang_fmt_date_iso = "%%yyyy%%-%%mm%%-%%dd%%";
set lang_fmt_date_short = "%%m%%/%%d%%/%%yy%%";
set lang_fmt_date_short_dayfirst = "%%d%%/%%m%%/%%yy%%";
set lang_fmt_date_short_dayfirst_full = "%%dd%%/%%mm%%/%%yyyy%%";
set lang_fmt_date_med = "%%mon%%. %%dayord%%, %%yyyy%%";
set lang_fmt_date_med_dayfirst = "%%d%% %%mon%% %%yyyy%%";
set lang_fmt_date_med_day_iso = "%%yyyy%%-%%mon%%-%%dd%%, %%day%%";
set lang_fmt_date_med_day = "%%da%%, %%mon%%. %%dayord%%, %%yyyy%%";
set lang_fmt_date_long = "%%month%% %%dayord%%, %%yyyy%%";
set lang_fmt_date_long_dayfirst = "%%d%% %%month%% %%yyyy%%";
set lang_fmt_date_long_day = "%%day%%, %%month%% %%dayord%%, %%yyyy%%";
set lang_fmt_date_long_day_dayfirst = "%%day%%, %%d%% %%month%% %%yyyy%%";

set lang_fmt_time_short = "%%hh%%:%%min%% %%a%%m";
set lang_fmt_time_short_24 = "%%HH%%:%%min%%";

set lang_fmt_month_short = "%%m%%/%%yy%%";
set lang_fmt_month_med = "%%mon%% %%yyyy%%";
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)."; }

property string[] lang_monthname_short { noui = 1; des = "Months of the year, in their short forms.  Indexed from 1 (Jan) to 12 (Dec)."; }

property string[] lang_dayname_long { noui = 1; des = "Days of the week.  Indexed from 1 (Sunday) to 7 (Saturday)."; }

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

property string[] lang_dayname_shorter { noui = 1; des = "Days of the week, in their one letter forms.  Indexed from 1 (S) to 7 (S)."; }

set lang_monthname_long = [ "", "January",  "February", "March",
                            "April", "May", "June",
                            "July", "August", "September",
                            "October", "November", "December" ];
set lang_monthname_short = [ "", "Jan",  "Feb", "Mar",
                             "Apr", "May", "Jun",
                             "Jul", "Aug", "Sep",
                             "Oct", "Nov", "Dec" ];
set lang_dayname_long = [ "", "Sunday", "Monday",  "Tuesday", "Wednesday",
                          "Thursday", "Friday", "Saturday" ];
set lang_dayname_short = [ "", "Sun", "Mon",  "Tue", "Wed",
                           "Thu", "Fri", "Sat" ];
set lang_dayname_shorter = [ "", "S", "M",  "T", "W",
                             "T", "F", "S" ];

###[ 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."
{
}

function modules_init()
"This function sets up the modules. It is called immediately after prop_init."
{
    if ( $*module_userprofile_show ) { $*module_sections{$*module_userprofile_section}[$*module_userprofile_order]=["userprofile"]; }
    if ( $*module_navlinks_show ) { $*module_sections{$*module_navlinks_section}[$*module_navlinks_order]=["navlinks"]; }
    if ( $*module_calendar_show ) { $*module_sections{$*module_calendar_section}[$*module_calendar_order]=["calendar"]; }
    if ( $*module_links_show ) { $*module_sections{$*module_links_section}[$*module_links_order]=["links"]; }
    if ( $*module_active_show ) { $*module_sections{$*module_active_section}[$*module_active_order]=["active"]; }
    if ( $*module_syndicate_show ) { $*module_sections{$*module_syndicate_section}[$*module_syndicate_order]=["syndicate"]; }
    if ( $*module_tags_show ) { $*module_sections{$*module_tags_section}[$*module_tags_order]=["tags"]; }
    if ( $*module_pagesummary_show ) { $*module_sections{$*module_pagesummary_section}[$*module_pagesummary_order]=["pagesummary"]; }
    if ( $*module_time_show ) { $*module_sections{$*module_time_section}[$*module_time_order]=["time"]; }
    if ( $*module_poweredby_show ) { $*module_sections{$*module_poweredby_section}[$*module_poweredby_order]=["poweredby"]; }
    if ( $*module_customtext_show ) { $*module_sections{$*module_customtext_section}[$*module_customtext_order]=["customtext"]; }
    if ( $*module_credit_show ) { $*module_sections{$*module_credit_section}[$*module_credit_order]=["credit"]; }
    if ( $*module_search_show ) { $*module_sections{$*module_search_section}[$*module_search_order]=["search"]; }
    if ( $*module_cuttagcontrols_show ) { $*module_sections{$*module_cuttagcontrols_section}[$*module_cuttagcontrols_order]=["cuttagcontrols"]; }
    if ( $*module_subscriptionfilters_show ) { $*module_sections{$*module_subscriptionfilters_section}[$*module_subscriptionfilters_order]=["subscriptionfilters"]; }

    #if ( $*module_%%mod%%_show ) { $*module_sections{$*module_%%mod%%_section }[$*module_%%mod%%_order]=["%%mod%%"]; }
}

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()]]. If you want to hardcode a stylesheet into your layout, override this function."
{
}

function print_custom_control_strip_css ()
  "Prints the CSS for custom control strip colors, if the option is enabled. This should be called by print_stylesheet().  If you override this function, you will be changing the design  of what is effectively site navigation, so proceed with caution."
{
    if ($*custom_control_strip_colors != "off") {
        var bool bgcolor = $*control_strip_bgcolor.as_string != "";
        var bool fgcolor = $*control_strip_fgcolor.as_string != "";
        var bool bordercolor = $*control_strip_bordercolor.as_string != "";
        var bool linkcolor = $*control_strip_linkcolor.as_string != "";
        var string bgimage = "";

        if ($*custom_control_strip_colors == "on_gradient" and $bgcolor and $fgcolor) {
            var Color blended_color = $*control_strip_bgcolor->blend($*control_strip_fgcolor, 25); # Make the background color 25% like the text color
            $bgimage = palimg_tint("controlstrip/bg.gif", $*control_strip_bgcolor, $blended_color);
        }

        if ($bgcolor) {
"""

#lj_controlstrip {
    background-color: $*control_strip_bgcolor;
""";
if ($bgimage == "") {
    """    background-image: none;""";
} else {
    """    background-image: url($bgimage);""";
}
"""
}""";
        }

        if ($fgcolor or $bordercolor) {
"""

#lj_controlstrip td {
""";
if ($fgcolor) {
    """    color: $*control_strip_fgcolor;""";
}
if ($fgcolor and $bordercolor) {
    "\n";
}
if ($bordercolor) {
    """    border-bottom: 1px solid $*control_strip_bordercolor;""";
}
"""
}""";
        }

        if ($fgcolor) {
"""

#lj_controlstrip_statustext {
    color: $*control_strip_fgcolor;
}""";
        }

        if ($linkcolor) {
"""

#lj_controlstrip a {
    color: $*control_strip_linkcolor;
}""";
        }

        if ($bordercolor) {
"""

#lj_controlstrip_user,
#lj_controlstrip_actionlinks,
#lj_controlstrip_search,
#lj_controlstrip_login,
#lj_controlstrip_loggedout_userpic {
    border-right: 1px solid $*control_strip_bordercolor;
}

#lj_controlstrip_login td {
    border-bottom: 0;
}

#lj_controlstrip td td {
    border-bottom: 0;
}""";
        }

        if ($bgcolor and $fgcolor) {
            print control_strip_logged_out_userpic_css();
            print control_strip_logged_out_full_userpic_css();
        }
    }
}


### Language

function lang_map_plural (int n) : int
"This function defines plurals for different languages. Use this if you are writing a language layer that needs customization for its plurals."
{
    if ($n == 1) { return 0; } # singular
    return 1;             # plural
}

function lang_page_of_pages (int pg, int pgs) [notags] : string
"This function returns the current page number as well as number of available pages. Override for language layers."
{
    return "Page $pg of $pgs";
}

function lang_user_wrote(UserLite u) : string
"Returns text describing that the user wrote something. This function is deprecated in favor of print_poster. Override for # language layers, but even better, use print_poster."
{
    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. Override only for language layers."
{
    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 [function[lang_ordinal(int)]], which i18nc layers should override."
{
    return lang_ordinal(int($num));
}


function lang_viewname(string viewid) [notags] : string
"Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout. Sets strings according to layer or wizard supplied string properties."
{
    var Page p = get_page();
    if ($viewid == "recent") { return $*text_view_recent; }
    if ($viewid == "archive") { return $*text_view_archive; }
    if ($viewid == "read") {
        if ( $p.journal.journal_type == "C" ) {
            return $*text_view_friends_comm;
        } else {
            return $*text_view_friends;
        }
    }
    if ($viewid == "icons") { return $*text_view_icons; }
    if ($viewid == "network") { return $*text_view_network; }
    if ($viewid == "day") { return $*text_view_day; }
    if ($viewid == "month") { return $*text_view_monthpage; }
    if ($viewid == "userinfo") { return $*text_view_userinfo; }
    if ($viewid == "entry") { return $*text_read_comments; }
    if ($viewid == "reply") { return $*text_post_comment; }
    if ($viewid == "tags") { return $*text_view_tags; }
    if ($viewid == "memories") { return $*text_view_memories; }
    return "Unknown View";
}

function lang_metadata_title(string which) [fixed] : string
"Get a human-readable caption for a metadata key. Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout. Sets strings according to layer or wizard supplied string properties."
{
    if ($which == "music") {
        return $*text_meta_music;
    }
    elseif ($which == "mood") {
        return $*text_meta_mood;
    }
    elseif ($which == "location") {
        return $*text_meta_location;
    }
    elseif ($which == "groups") {
        return $*text_meta_groups;
    }
    elseif ($which == "xpost") {
        return $*text_meta_xpost;
    }
    else {
        return $which;
    }
}

function lang_icon_sortorder_title(string which) [fixed] : string
    "Get a human-readable caption for an icons sort order key. Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout. Sets strings according to layer or wizard supplied string properties."
    {
        if ($which == "upload") {
            return $*text_icons_sort_upload;
        }
        elseif ($which == "keyword") {
            return $*text_icons_sort_keyword;
        }
        else {
            return $which;
        }
    }

### Navigation


### Image Manipulation

function Image::as_string(string{} opts) [fixed] : string
"You most likely want to use Image::print instead.  Prints HTML for the image. Using meaningful alttext is strongly recommended to improve the accessibility of your layout. Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout."
{
    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
"You most likely want to use Image::print instead.  Prints HTML for the image with a given alttext. Using meaningful alttext is strongly recommended to improve the accessibility of your layout. Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout."
{
    return "<img src=\"$.url\" title=\"$.extra{"title"}\" alt=\""
        + ehtml( $alttext )
        + "\" "
        + htmlattr("height", $.height)
        + htmlattr("width", $.width)
        + " />";
}

function Image::as_string() [fixed] : string
"You most likely want to use Image::print instead.  Prints HTML for the image. Using meaningful alttext is strongly recommended to improve the accessibility of your layout. Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout."
{
    # If the image has an attribute "alttext", the alt text has already been escaped, eg. in S2.pm
     return "<img src=\"$.url\" title=\"$.extra{"title"}\" alt=\"$this.alttext\" "
        + htmlattr("height", $.height)
        + htmlattr("width", $.width)
        + " />";
}

function Image::print (string{} opts)
"Prints HTML for the image. Using meaningful alttext is strongly recommended to improve the accessibility of your layout. Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout."
{
    # must do safe here because opts could have the 'a_attr' key set
    print safe $this->as_string($opts);
}

function Image::print (string alttext)
"Prints HTML for the image. Using meaningful alttext is strongly recommended to improve the accessibility of your layout. Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout."
{
    print $this->as_string($alttext);
}

function Image::print
"Prints HTML for the image. Using meaningful alttext is strongly recommended to improve the accessibility of your layout. Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout."
{
    print $this->as_string();
}

function userinfoicon(UserLite user) : Image
"Prints the user icon identifying an account link's type. Be aware that overriding this function will allow you to change an image which contains semantically meaningful information used for site navigation, so override with caution."
{
    var Image uimage;
    $uimage.width = 16;
    $uimage.height = 16;

    if ($user.journal_type == "C") {
        $uimage->set_url("$*IMGDIR/silk/identity/community.png");
    } elseif ($user.journal_type == "Y") {
        $uimage->set_url("$*IMGDIR/silk/identity/feed.png");
    } elseif ($user.journal_type == "I") {
        $uimage->set_url("$*IMGDIR/silk/identity/openid.png");
    } else {
        $uimage->set_url("$*IMGDIR/silk/identity/user.png");
        $uimage.width = 17;
        $uimage.height = 17;
    }
    return $uimage;
}

### UserLite functions

function UserLite::base_url() [fixed] : string
"Returns a link to the user's journal."
{
    return userlite_base_url($this);
}

function UserLite::linklist_manage_url() [fixed] : string
"Returns a link to the user's links list management page. Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout."
{
    return "$*SITEROOT/customize/options?group=linkslist&authas=$.username";
}

function UserLite::tag_manage_url() [fixed] : string
"Returns a link to the user's tag management page. Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout."
{
    return "$*SITEROOT/manage/tags.bml?authas=$.username";
}

function UserLite::icon_manage_url() [fixed] : string
"Returns a link to the user's icon management page. Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout."
{
    return "$*SITEROOT/editicons?authas=$.username";
}

function UserLite::subscriptionfilters_manage_url() [fixed] : string
"Returns a link to the user's subscription filter management page.  Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout."
{
    return "$*SITEROOT/manage/subscriptions/filters";
}

function UserLite::as_string() [fixed] : string
"Use UserLite::print instead in most cases. Returns a DW user tag for the user. Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout."
{
    return $this->ljuser();
}

function UserLite::print()
"Print a DW user tag for the user. Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout."
{
    print $this->as_string();
}

function UserLite::print_interaction_links()
"This function prints the \"subscribe to journal, add user to access list, post to community, track journal, etc, etc\" items, and wraps them in a standarized class."
{

    var string display_type = ($*userlite_interaction_links == "text") ? " text-links" : " icon-links";
    var Link link;
    var int count;
    $count = 0;
    foreach var string k ($.link_keyseq) {
        $link = $this->get_link($k);
        if ($link.url) {
            $count ++;
            if ($count == 1) {
                """<ul class="userlite-interaction-links$display_type">""";
            }
            if ($*userlite_interaction_links == "text") {
                """<li class="link $k""" + ( $count == 1 ? " first-item" : "" ) + """"><a href="$link.url">$link.caption</a></li>\n""";
            }
            else { ## if ($*userlite_interaction_links == "icon")
                """<li class="link $k""" + ( $count == 1 ? " first-item" : "" ) + """">$link</li>\n""";
            }
        }
    }
    if ($count > 0) {
        """</ul>""";
    }
}

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

function User::print_userpic() {
  print """<a href="$this.userpic_listing_url">""";
  if ( defined $this.default_pic ) {
      $this.default_pic->print();
  }
  print "</a>";
}


### Generic Page Functions

function Page::print_default_stylesheet()
"Adds CSS to all themes on your layout. Very likely to be overridden for most layouts. Style tags will be automatically added around it."
{

}

function Page::print_contextual_stylesheet()
"Adds CSS for contextual popups to ensure readability. Style tags will be automatically added around it. Overriding this function is NOT RECOMMENDED. Use Page::print_default_stylesheet or print_stylesheet instead."
{

var Color background = isnull $*color_entry_background ? $*color_page_background : $*color_entry_background;
var Color text = isnull $*color_entry_text ? $*color_page_text : $*color_entry_text;
var Color link = isnull $*color_entry_link ? $*color_page_link : $*color_entry_link;
var Color visited = isnull $*color_entry_link_visited ? $*color_page_link_visited : $*color_entry_link_visited;
var Color hover = isnull $*color_entry_link_hover ? $*color_page_link_hover : $*color_entry_link_hover;
var Color active = isnull $*color_entry_link_active ? $*color_page_link_active : $*color_entry_link_active;

"""
.ContextualPopup {
    background: $background;
    color: $text;
    }

.ContextualPopup a { color: $link; }
.ContextualPopup a:visited { color: $visited; }
.ContextualPopup a:hover { color: $hover; }
.ContextualPopup a:active { color: $active; }
""";
}

function Page::print_theme_stylesheet()
"Prints theme-specific CSS. Should be overwritten by themes that include CSS not part of the layout's default stylesheet."
{

}

function Page::print_default_stylesheets()
"Contains the layout and theme default stylesheets. Themes and child layouts may use this to disable the printing of the layout's base default stylesheet, or to add theme-specific CSS bits. May be overriden on a per-layout/theme basis"
{
    if ($*external_stylesheet) {
        # prints out the stylesheets for $this->print_default_stylesheet, print_stylesheet(), and $this->print_theme_stylesheet
        println safe """<link rel="stylesheet" href="$.stylesheet_url" type="text/css" />""";
        if ($*font_sources != "") {
            println safe """<link rel="stylesheet" href="$*font_sources" type="text/css" />""";
        }
    }
    else {
        println """<style type="text/css">""";
        start_css();
        $this->print_contextual_stylesheet();
        $this->print_default_stylesheet();
        print_stylesheet();
        $this->print_theme_stylesheet();
        end_css();
        println """</style>""";
    }
}

function Page::print_stylesheets()
"If you have specific CSS to code onto all themes using this layout, override print_default_stylesheet.  If you have specific CSS to code into this theme, set the external_stylesheet property or override the Page::print_theme_stylesheet function. An end user can choose to link to an off-site stylesheet using the linked_stylesheet property and/or use the custom_css property.  Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout."
{
    if ($*include_default_stylesheet) {
        $this->print_default_stylesheets();
    }

    if ($*linked_stylesheet != "") {
        println safe """<link rel="stylesheet" href="$*linked_stylesheet" type="text/css" />""";
    }

    if ($*custom_css != "") {
        println """<style type="text/css">""";
        start_css();
        println safe $*custom_css;
        end_css();
        println """</style>""";
    }
}
function EntryPreviewPage::print_standout_box(string message)
"Prints a standout box for warnings, etc "
{
    println "<div class='highlight-box'><p> $message </p></div>";
}

function FriendsPage::print_stylesheets() {
#This creates entry-specific CSS to allow custom colors on reading page. Rather than override this, layouts should change custom_foreground_element, custom_background_element (and custom_colors_template for advanced options) instead
    $super->print_stylesheets();

    if ($*use_custom_friend_colors) {
        println """<style type="text/css">""";
        start_css();

        foreach var Entry e ($this.entries) {
            var Color bg;
            var Color fg;

            $bg = $.friends{$e.journal.username}.bgcolor;
            $fg = $.friends{$e.journal.username}.fgcolor;

            var string{} custom_foreground_location;
            var string{} custom_background_location;

            $custom_foreground_location = {
                "subject" => ".entry h3.entry-title a {color:",
                "userpic_border" => ".entry .userpic a img {border: solid !important; border-color:",
                "postername" => ".entry .entry-poster a {color:",
                "entry" => ".entry .entry-content {color:",
                "metadata" => ".entry .metadata {color:",
                "bottom_links" => ".entry .footer a {color:"
                };

            $custom_background_location = {
                "subject" => ".entry h3.entry-title {background-color:",
                "userpic_background" => ".entry .userpic a img {padding: 2px !important; background-color:",
                "entry_border" => ".entry .entry-content {border: solid !important; border-color:",
                "metadata_background" => ".entry .metadata {background-color:",
                "footer_border" => ".entry .footer {border: solid !important; border-color:"
                };

            var string custom_foreground_css = "";
            var string custom_background_css = "";
            $custom_foreground_css = """.journal-$e.journal.username $custom_foreground_location{$*custom_foreground_element} $fg !important; } \n""";
            $custom_background_css = """.journal-$e.journal.username $custom_background_location{$*custom_background_element} $bg !important; } \n\n""";

            var string custom_colors_css;
            if ($*custom_colors_template != ""){
            var string foreground = $fg;
            var string background = $bg;
            $custom_colors_css = $*custom_colors_template->replace("%%foreground%%", $foreground);
            $custom_colors_css = $custom_colors_css->replace("%%background%%", $background);
            $custom_colors_css = $custom_colors_css->replace("%%new%%", ".journal-$e.journal.username");
            $custom_colors_css = $custom_colors_css + "\n\n";
            }
            else {$custom_colors_css = $custom_foreground_css + $custom_background_css;}
            print $custom_colors_css;
            }

        end_css();
        println """</style>""";
        }
}

function generate_image_url ( string image_path ) : string
"Take an image property and append style image directory path if not given absolute path"
{
    if ( $image_path != "" and not ( $image_path->starts_with("http://") or $image_path->starts_with("https://") ) ) {
        $image_path = "$*STYLES_IMGDIR/$image_path";
    }

    return $image_path;
}

function generate_background_css (
    string background_image,
    string background_image_repeat,
    string background_image_position,
    Color background_color
) : string
"Take the values for the properties in a background image group and output the appropriate
line of CSS for inclusion in your stylesheet."
{
    var string color = ($background_color.as_string != "") ? $background_color.as_string : "transparent";
    var string background_css = "background: $color";

    $background_image = generate_image_url ($background_image);
    if ($background_image != "") {
        $background_css = $background_css + " url($background_image) $background_image_repeat $background_image_position";
    }

    $background_css = $background_css + ";";

    return $background_css;
}

function generate_color_css (Color text_color, Color background_color, Color border_color) : string
"Take the color property values and return the appropriate lines of CSS for your style sheet."
{
    var string color_css = "";
    if ($text_color.as_string != "") {
        $color_css = $color_css + "color: $text_color;\n";
    }
    if ($background_color.as_string != "") {
        $color_css = $color_css + "background-color: $background_color;\n";
    }
    if ($border_color.as_string != "") {
        $color_css = $color_css + "border: solid 1px $border_color;\n";
    }
    return $color_css;
}

function generate_font_css(string font_specific, string font_base, string font_fallback, string font_size, string font_unit) : string
{
    var string font_css = "";


    if ($font_specific != "" or $font_base != "" or $font_fallback != "") {
        $font_css = "font-family: ";

        if ($font_specific != "") {
            $font_css = $font_css + "$font_specific";
            if ($font_base != "") {
                $font_css = $font_css + ", $font_base";
            }

            if ($font_fallback != "") {
                $font_css = $font_css + ", $font_fallback";
            }

        } elseif ($font_specific == "" and ($font_base != "" or $font_fallback != "")) {
            if ($font_base != "") {
                $font_css = $font_css + "$font_base";

                if ($font_fallback != "") {
                    $font_css = $font_css + ", $font_fallback";
                }
            } else {
                $font_css = $font_css + "$font_fallback";
            }
        }

        $font_css = $font_css + "; ";
    }


    if ($font_size != "" and $font_unit != "") {
        $font_css = $font_css + "font-size: $font_size$font_unit;";
    }
    return $font_css;
}

function generate_font_css(string font_base, string font_fallback, string font_size, string font_unit) : string {
    return generate_font_css("", $font_base, $font_fallback, $font_size, $font_unit);
}

function generate_media_query(string user_breakpoint, int sidebar_width_multiplier, string default_min_width): string {
    # use the user-provided breakpoint width
    var string min_width = $user_breakpoint;

    # no user-provided breakpoint width, base our min-width as a multiplier on the sidebar width
    if ($min_width == "" and $*sidebar_width != "") {
        # set the min width in ems
        $min_width = $*sidebar_width->css_multiply_length($sidebar_width_multiplier);
    }

    # $*sidebar_width wasn't in a format we expected, fall back to a default
    if ($min_width == "") {
        $min_width = $default_min_width;
    }

    return "only screen and (min-width: $min_width)";
}

function generate_medium_media_query(): string {
    return generate_media_query($*medium_breakpoint_width, 3, "40em");
}

function generate_large_media_query(): string {
    return generate_media_query($*large_breakpoint_width, 4, "64em");
}

function Page::print_meta_tags() {
    # they're using their own stylesheets which probably won't work with the initial scale
    # (things will just look smooshed together)
    if ($this.include_meta_viewport and $*include_default_stylesheet) {
        print """<meta name="viewport" content="width=device-width, initial-scale=1.0"/>""";
    }

    print """<meta http-equiv="X-UA-Compatible" content="IE=edge" />""";
}

function Page::print_header()
{
    $this->print_global_title();
    $this->print_global_subtitle();
    $this->print_title();
}

function Page::print_title()
"Using standard CSS, print the page title. If you want to change the way this looks, modify the CSS."
{
    print """<h2 id="pagetitle"><span>""" + $this->view_title() + """</span></h2>\n""";
}

function Page::print_head_title()
"Using standard CSS, print the journal title. If you want to change the way this looks, modify the CSS."
{
    if ($this.journal.journal_type == "I") {
        print """<title>""" + $this.journal.name + $*text_default_separator + $this->view_title() + """</title>\n""";
    }
    else {
        print """<title>""" + $this.journal.username + $*text_default_separator + $this->view_title() + """</title>\n""";
    }
}
function Page::print_global_title() {
    if ($.global_title) {
        """<h1 id="title"><span>""" + $.global_title + """</span></h1>""";
    }
}
function FriendsPage::print_global_title() {
    if ($.friends_title) {
        """<h1 id="title"><span>""" + $.friends_title + """</span></h1>""";
    }
    else {
        """<h1 id="title"><span>""" + $.global_title + """</span></h1>""";
    }
}
function Page::print_global_subtitle() {
    if ($.global_subtitle) {
        """<h2 id="subtitle"><span>""" + $.global_subtitle + """</span></h2>""";
    }
}
function FriendsPage::print_global_subtitle(){
    if ($.friends_subtitle){
        """<h2 id="subtitle"><span>""" + $.friends_subtitle + """</span></h2>""";
    } elseif ($.friends_title) {
    } elseif ($.global_subtitle) {
        """<h2 id="subtitle"><span>""" + $.global_subtitle + """</span></h2>""";
    }
}
function Page::view_title() [notags] : string {
    return lang_viewname($.view);
}
function RecentPage::view_title() : string {
    if ($.filter_active) {
        if ($.filter_tags) {
            return $*text_view_recent_tagged + " " + $.filter_name;
        } else {
            return "(" + $.filter_name + ")";
        }
    }
    else {
        return $*text_view_recent;
    }
}
function FriendsPage::view_title() : string {
    if ($.friends_mode == "") {
        if ($.filter_active) {
            if ($.filter_name != "") {
                return $*text_view_friends+" ("+$.filter_name+")";
            } else {
                return $*text_view_friends_filter;
            }
        } else {
            if ($.journal.journal_type == "C") {
                return $*text_view_friends_comm;
            } else {
                return $*text_view_friends;
            }
        }
    }
    elseif ($.friends_mode == "network") {
        if ($.filter_active) {
            if ($.filter_name != "") {
                return $*text_view_network+" ("+$.filter_name+")";
            }
            else {
                return $*text_view_network_filter;
            }
        }
        else {
            return $*text_view_network;
        }
    }
    else {
        return "Unknown Friends View";
    }
}
function DayPage::view_title : string {
    return $.date->date_format($*entry_date_format);
}
function YearPage::view_title() : string {
    return string($.year);
}
function EntryPage::view_title() : string {
    return $.entry.subject ? $.entry->get_plain_subject() : $*text_nosubject;
}
function ReplyPage::view_title() : string {
var string suffix = $.isedit ?
    " (" + $*text_edit_comment_title + ")" :
    " (" + $*text_new_comment_title + ")";
if ($.replyto isa Comment) {
        var Comment c = $.replyto as Comment;
        return $c->get_plain_subject() + $suffix;
    }
    else {
        var Entry e = $.replyto as Entry;
        return $e.subject ?
          $e->get_plain_subject() + $suffix :
          $*text_nosubject + $suffix;
    }
}
function Page::title() [notags] : string {
    return $this->view_title();
}

function Page::print_module_jump_link() {
    if ($*layout_type == "one-column-split") {
        """<div id="module-jump-link"><a href="#tertiary" title="Jump to modules below the entries">Modules</a></div>""";
    } else {
        """<div id="module-jump-link"><a href="#secondary" title="Jump to modules below the entries">Modules</a></div>""";
    }
}

function Page::print_wrapper_start()
"This function adds standard classes to the body of a page.  Pass extra options to it if necessary.  Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout."
{
    $this->print_wrapper_start( { "" => "" } );
}

function Page::print_wrapper_start(string{} opts)
"This function adds standard classes to the body of a page.  Pass extra options to it if necessary.  Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout."
{
    var string control_strip = viewer_sees_control_strip() ? "has-navstrip" : "no-navstrip";
    var string logged_in = viewer_logged_in() ? "logged-in" : "logged-out";
    var string owner = viewer_is_owner() ? "my-journal" : "";
    var string subscribed = viewer_is_subscribed() ? "subscribed" : "not-subscribed";
    var string access = viewer_has_access() ? "has-access" : "no-access";

    var string member = viewer_is_member() ? "is-member" : "non-member";
    var string admin = viewer_is_admin() ? "is-admin" : "";
    var string moderator = viewer_is_moderator() ? "is-moderator" : "";

    var string column_count = "";
    var string column_side = "";
    var string column_multiple = "";

    if ( $*layout_type->contains("split") ) { $column_count = "one-column"; }
    if ( $*layout_type->contains("two") ) { $column_count = "two-columns"; }
    if ( $*layout_type->contains("three") ) { $column_count = "three-columns"; }

    if ( $*layout_type->contains("right") ) { $column_side = " column-right"; }
    if ( $*layout_type->contains("left") ) { $column_side = " column-left"; }
    if ( $*layout_type->contains("sides") ) { $column_side = " column-left column-right"; }

    if ( $*layout_type->contains("one-column") ) { $column_multiple = "any-column"; }
    else                                         { $column_multiple = "any-column multiple-columns"; }

    var string community_options = "";
    if ( $this.journal.journal_type == "C" ) {
         $community_options = "$member $admin $moderator";
    }

    """<body class="page-$.view $column_count$column_side $column_multiple $*layout_type $opts{"class"} $logged_in $owner $subscribed $access $community_options $control_strip">\n""";
}

function Page::print_wrapper_end()
"This function concludes the wrapper to the page.  Overriding this function is NOT RECOMMENDED. Overriding this function could prevent sitewide improvements to styles, accessibility, or other functionality from operating in your layout."
{
    """</body>""";
}

function server_sig()
"Provides the site branding for each page."
{
    """<span id="site-branding">$*text_powered_by <a href="$*SITEROOT/">$*SITENAME</a></span>""";
}

function print_linklist_manage_link() "Prints out a link to the links list management page, if the viewer can manage the list."
{
    var Page p = get_page();
    print (viewer_is_owner() or viewer_is_admin()) ? "<div class=\"manage-link\"><a href=\"" + $p.journal->linklist_manage_url() + "\">$*text_linklist_manage</a></div>" : "";
}

function print_tag_manage_link() "Prints out a link to the tag management page, if the viewer can manage tags."
{
    var Page p = get_page();
    print viewer_can_manage_tags() ? "<div class=\"manage-link manage-tags-link\"><a href=\"" + $p.journal->tag_manage_url() + "\">$*text_tags_manage</a></div>" : "";
}

function IconsPage::print_icon_manage_link() "Prints out a link to the icon management page, if the viewer can manage icons."
{
    print ($.can_manage) ? "<div class=\"manage-link manage-icons-link\"><a href=\"" + $.journal->icon_manage_url() + "\">$*text_icon_manage</a></div>\n" : "";
}

function print_subscriptionfilters_manage_link() "Prints out a link to the subscription filter management page, if the viewer can manage them."
{
    var Page p = get_page();
    print viewer_is_owner() ? "<div class=\"manage-link manage-subfilters-link\"><a href=\"" + $p.journal->subscriptionfilters_manage_url() + "\">$*text_subscriptionfilters_manage</a></div>" : "";
}

function print_list_tags(TagDetail[] tagslist, string{} opts) "Prints out a list of tags. Takes as arguments the taglist and a hash with optional arguments 'list-class' and 'item-class'. 'print_uses' option can be 'number', 'text' or 'title' to determine whether to display the uses as a number, as full text, or in the link title. It defaults to full text"
{
    var string list_class = $opts{"list-class"} ? """class="$opts{"list-class"}" """ : "";
    var string item_class = $opts{"item-class"} ? $opts{"item-class"} : "";
    var string print_uses = $opts{"print_uses"} ? $opts{"print_uses"} : "text";
    if (size $tagslist) {
        println """<ul $list_class>""";

        foreach var TagDetail t ($tagslist) {
            var string tag_class = """class="visibility-$t.visibility $item_class" """;
            if ($print_uses == "title") {
                var string uses = get_plural_phrase($t.use_count, "text_tag_uses");
                println """<li $tag_class><a href="$t.url" title="$uses">$t.name</a></li>\n""";
            } elseif ($print_uses == "number") {
                println """<li $tag_class><a href="$t.url">$t.name</a> [$t.use_count]</li>\n""";
            } else {
                var string uses = get_plural_phrase($t.use_count, "text_tag_uses");
                println """<li $tag_class><a href="$t.url">$t.name</a> - $uses</li>\n""";
            }
        }
        println """</ul>""";
    }
}

function print_cloud_tags(TagDetail[] tagslist, string{} opts) "Prints out a list of tags in a cloud. Takes as arguments the taglist and a hash with optional arguments 'min_size' (minimum size in ems, times 10), 'maz_size' (maximum size in ems, times 10), 'list-class' and 'item-class'. 'print_uses' option can be 'number', 'text' or 'title' to determine whether to display the uses as a number, as full text, or in the link title. It defaults to the link title"
{
    var string list_class = $opts{"list-class"} ? """class="$opts{"list-class"}" """ : "";
    var string item_class = $opts{"item-class"} ? $opts{"item-class"}: "";
    var string print_uses = $opts{"print_uses"} ? $opts{"print_uses"} : "title";

    var int min_size = $opts{"min_size"} ? int($opts{"maz_size"}) : 9;
    var int maz_size = $opts{"maz_size"} ? int($opts{"maz_size"}) : 20;
    var int high_count = 1;

    println """<div $list_class>""";
    println """<ul>""";

    foreach var TagDetail t ($tagslist)
    {
        if ($t.use_count > $high_count) { $high_count = $t.use_count; }
    }

    foreach var TagDetail t ($tagslist)
    {
        var string tag_size = string($min_size);
        var string tag_class = """class="visibility-$t.visibility $item_class" """;
        if ($t.use_count > 1)
        {
            $tag_size = string((($maz_size-$min_size)*$t.use_count)/$high_count + $min_size);
        }
        var string tag_size_em = $tag_size->substr(0,($tag_size->length())-1) + "." + $tag_size->substr(($tag_size->length())-1,1) + "em";
        if ($print_uses == "number") {
            println """<li $tag_class style="font-size: $tag_size_em;"><a rel="tag" href="$t.url">$t.name</a> [$t.use_count]</li>\n""";
        } elseif ($print_uses == "text") {
            var string uses = get_plural_phrase($t.use_count, "text_tag_uses");
            println """<li $tag_class style="font-size: $tag_size_em;"><a rel="tag" href="$t.url">$t.name</a> - $uses</li>\n""";
        } else {
            var string uses = get_plural_phrase($t.use_count, "text_tag_uses");
            # if the style hides the number of uses, put it both
            # in the title tag and offscreen via CSS to maximize
            # the chance of it being available to
            # screenreader/lynx users
            println """<li $tag_class style="font-size: $tag_size_em;">""";
            println """<a rel="tag" href="$t.url" title="$uses">$t.name</a> """;
            println """<span class ="invisible">$uses</span></li>\n""";
        }
    }

    println """</ul>""";
    println """</div>""";
}

function print_multilevel_tags(TagDetail[] tagslist, string{} opts) "Prints out a list of multilevel tags. Takes as arguments the taglist and a hash with optional arguments 'list-class' and 'item-class'. 'print_uses' option can be 'number', 'text' or 'title' to determine whether to display the uses as a number, as full text, or in the link title. It defaults to uses as a number"
{
    if (size($tagslist) < 1) { return; }

    var string list_class = $opts{"list-class"} ? """class="$opts{"list-class"}" """ : "";
    var string item_class = $opts{"item-class"} ? $opts{"item-class"} : "";
    var string print_uses = $opts{"print_uses"} ? $opts{"print_uses"} : "number";

    var string[] prev_tags = [];
    var int tag_list_pos = 0;
    var string tier_code = "";
    var int levels_to_close = 0;

    foreach var TagDetail t ($tagslist) {
        var string[] tags = $t.name->split($*text_tagsmultilevel_delimiter);
        var string tag_class = """class="visibility-$t.visibility $item_class" """;

        var int pos = 0;
        var bool show_lower_tiers = false;
        foreach var string tier($tags) {

            # If we're on a tag's last tier, we need to return a link to the tag,
            # otherwise plain text is returned.
            if (size $tags == ($pos + 1)) {
                if ($print_uses == "title") {
                    var string uses = get_plural_phrase($t.use_count, "text_tag_uses");
                    $tier_code = """<a rel="tag" href="$t.url" title="$uses">$tier</a>""";
                } elseif ($print_uses == "text") {
                    var string uses = get_plural_phrase($t.use_count, "text_tag_uses");
                    $tier_code = """<a rel="tag" href="$t.url">$tier</a> - $uses""";
                } else {
                    $tier_code = """<a rel="tag" href="$t.url">$tier</a> [${t.use_count}]""";
                }
            }
            else {
                $tier_code = """<span class="non-link-tag">$tier</span>""";
            }

            # $prev_tags has fewer tiers than current tag.
            if (size $prev_tags < $pos + 1) {
                print """\n<ul $list_class><li $tag_class>$tier_code""";
                $levels_to_close++;
            }
            elseif (($tags[$pos] != $prev_tags[$pos]) or ($show_lower_tiers)) {
                if ($tags[$pos] != $prev_tags[$pos]) {

                    # The current tag's tier is not the same as the previous tag's tier of
                    # the same level.  This means we may need to close some lists.
                    var int i = $levels_to_close;
                    foreach var string html ($prev_tags) {
                        if ($i > $pos + 1) {
                            print "</li></ul>";
                            $levels_to_close--;
                        }
                        $i--;
                    }

                    # If we just closed some lists, that means that any lower tiers in this tag need to
                    # be explicitly displayed, even if they match the same-level tier of the previous tag
                    $show_lower_tiers = true;
                }

                if ($levels_to_close <= $pos) {
                    # This is the first tier at this level, so open list.
                    print """\n<ul $list_class><li $tag_class>$tier_code""";
                    $levels_to_close++;
                }
                else {
                    # There have already been tiers added at this level
                    print """</li>\n<li $tag_class>$tier_code""";
                }
            }
            else {
                # The current tag's tier is exactly the same as the previous tag's tier at
                # this same level.  It has already been included in the list, so do nothing.
            }

            # Moving on to next tier in this tag!
            $pos++;
        }
        $prev_tags = $tags;
        $show_lower_tiers = false;
    }
    # Next tag in the list!
    $tag_list_pos++;

    # All the tags have been added so close all outstanding lists.
    foreach var string html ($prev_tags) {
        if ($levels_to_close > 0) {
            print "</li></ul>";
            $levels_to_close--;
        }
    }

}

function print_multilevel_tags(TagDetail[] tagslist) "Prints out a list of multilevel tags. Takes as arguments the taglist."
{
    print_multilevel_tags($tagslist, { "" => "" });
}


function open_module(string intname, string title, string headlink_url, bool nocontent)
"Opens up a module for printing, using the appropriate HTML/CSS."
{
    print safe """<div class="module-$intname module">""";
    if ($title != "") {
        """<h2 class="module-header">""";
        if ($headlink_url != "") { print safe """<a href="$headlink_url">"""; }
        print safe $title;
        if ($headlink_url != "") { """</a>"""; }
        "</h2>\n";
    }
    if (not $nocontent) {
        println """<div class="module-content">""";
    }
}

function close_module(bool nocontent) {
    println "</div>";
    if (not $nocontent) { println "</div>"; }
}

function open_module(string intname, string title, string headlink_url)
"Opens up a module for printing, using the appropriate HTML/CSS."
{
    open_module($intname, $title, $headlink_url, false);
}
function close_module() {
    close_module(false);
}

function print_module_list(string[] list)
"Takes a straight list of strings, prints them out as a list for a module"
{
    if (size $list) {
        println """<ul class="module-list">""";
        foreach var string s ($list) {
            print safe """<li class="module-list-item">$s</li>\n""";
        }
        println """</ul>""";
    }
}

function print_module_list(string{}[] list)
"Takes a list of hashes in the form of [ { class => list_of_classes, item => list_item_contents }], prints them out as a list for a module"
{
    if (size $list) {
        println """<ul class="module-list">""";
        foreach var string{} s ($list) {
            print safe """<li class="module-list-item $s{"class"}">$s{"item"}</li>\n""";
        }
        println """</ul>""";
    }
}

function print_module_search() {
    # only print this module if the viewer can search this journal
    if (viewer_can_search()) {
        var Page p = get_page();

        open_module("search", $*text_module_search, "$*SITEROOT/search?user=" + $p.journal.username);
        print_search_form($*text_module_search_btn);
        close_module();
    }
}

function print_module_userprofile() {
    var Page p = get_page();

    open_module("userprofile", $*text_module_userprofile, $p.view_url{"userinfo"});

    if ($*module_userprofile_opts_userpic) {
        if (defined $p.journal.default_pic) {
            """<div class="userpic">""";
            $p.journal->print_userpic();
             "</div>";
        }
    }

    if ($*module_userprofile_opts_name) {
        println "<div class='journal-name'>" + $p.journal.name + "</div>";
    }

    if ($*module_userprofile_opts_website and $p.journal.website_url != "") {
        var string website_name = ( $p.journal.website_name != "" ) ? $p.journal.website_name : $*text_website_default_name;
        println "<div class='journal-website-name'><a href='$p.journal.website_url'>$website_name</a></div>";
    }

    $p.journal->print_interaction_links();
    close_module();
}

function print_module_navlinks( bool apply_class_to_link ) {
    var Page p = get_page();
    open_module("navlinks", "", "");

    if ( $apply_class_to_link ) {
        var string[] links = [];
        foreach var string k ($p.views_order) {
            var string css = """ class="$k" """;
            if ($p.view == $k) { $css = """ class="current $k" """; }
            $links[size $links] = """<a href="$p.view_url{$k}"$css>"""+lang_viewname($k)+"""</a>""";

        }
        print_module_list($links);
    } else {
        var string{}[] links = [];

        foreach var string k ($p.views_order) {
            var string class = $k;
            if ($p.view == $k) { $class = "current $k"; }
            $links[size $links] = { "class" => $class, "item" => """<a href="$p.view_url{$k}">"""+lang_viewname($k)+"""</a>""" };
        }
        print_module_list($links);
    }

    close_module();

}

function print_module_navlinks() {
    # prints as   <li><a class="current view...">
    # styles may override and change to print_module_navlinks( false )
    # to print as <li class="current view...">
    print_module_navlinks( true );
}

function print_module_time() {
    var Page p = get_page();
    open_module("time", "", "");
    $p->print_time();
    close_module();
}

function print_module_poweredby() {
    var Page p = get_page();
    open_module("powered", "", "");
    server_sig();
    close_module();
}

function print_module_pagesummary_comment_count(Comment comment) : int {
    var int count = 0;
    var Comment c;

    # copy over to avoid munging the existing array for other callers
    var Comment[] replies_stack = [];
    push $replies_stack, $comment.replies;

    while (size $replies_stack) {
      $c = pop $replies_stack;
      push $replies_stack, $c.replies;

      $count = $count + 1;
    }

    return $count;
}

function print_module_pagesummary_comments(string esubject, int count, string prop, string read_url) : string {
    var string subject = ($esubject != "" ? striphtml($esubject) : "<em>$*text_nosubject</em>");
    var string module_comment_text = get_plural_phrase($count, $prop);

    if ($*module_pagesummary_opts_comments_tooltip) {
        return "title='$module_comment_text'>$subject</a></span>";
    }
    elseif ($count != 0) {
        if ($read_url == "") {
            return ">$subject</a></span> <span class='pagesummary-commentcount'>+$module_comment_text</span>";
        } else {
            return ">$subject</a></span> <span class='pagesummary-commentcount'><a href='$read_url'>+$module_comment_text</a></span>";
        }
    }
    else {
        return ">$subject</a></span>";
    }
}

function print_module_pagesummary() {
    var Page p = get_page();
    var string[] links = [];
    var Image admin_post_image = get_image("admin-post");

    if ( $p isa EntryPage ) {
        var EntryPage cp = $p as EntryPage;
        foreach var Comment c ( $cp.comments ) {
            if (not $c.deleted and not $c.fromsuspended and not $c.screened_noshow) {
                var int count = print_module_pagesummary_comment_count($c);
                var string comment_display = print_module_pagesummary_comments($c.subject, $count, "text_read_comments_threads", "");
                var string poster = isnull $c.poster ? $*text_poster_anonymous : $c.poster->ljuser();
                var string prefix_icon = $c.admin_post ? $admin_post_image->as_string($*text_icon_alt_admin_post) : "";

                $links[size $links] = """<span class="pagesummary-poster">$poster</span> - <span class="pagesummary-subject">$prefix_icon<a href="#$c.anchor" $comment_display""";
            }
        }
    }

    elseif ( $p isa FriendsPage ) {
        var FriendsPage cp = $p as FriendsPage;
        foreach var Entry e ( $cp.entries ) {
            var string comment_display =
                print_module_pagesummary_comments($e.subject, $e.comments.count, "text_read_comments_friends", $e.comments.read_url + "#comments");
            var string poster = "<span class='pagesummary-poster'>" + $e.poster->ljuser() + "</span>";

            if ( not $e.poster->equals($e.journal) ) {
                $poster = "$poster in <span class='pagesummary-community'>$e.journal</span>";
            }

            var string prefix_icon = $e.admin_post ? $admin_post_image->as_string($*text_icon_alt_admin_post) : "";

            $links[size $links] = """$poster - <span class="pagesummary-subject">$prefix_icon<a href="#entry-$e.itemid" $comment_display""";
        }
    }

    elseif ( $p isa RecentPage ) {
        var RecentPage cp = $p as RecentPage;
        foreach var Entry e ( $cp.entries ) {
            var string comment_display =
                print_module_pagesummary_comments($e.subject, $e.comments.count, "text_read_comments", $e.comments.read_url + "#comments");
            var string poster = "<span class='pagesummary-poster'>" + $e.poster->ljuser() + "</span>";

            var string prefix_icon = $e.admin_post ? $admin_post_image->as_string($*text_icon_alt_admin_post) : "";

            if ( not $e.poster->equals($e.journal) ) {
                $links[size $links] = """$poster - <span class="pagesummary-subject">$prefix_icon<a href="#entry-$e.itemid" $comment_display""";
            }
            else {
                $links[size $links] = """<span class="pagesummary-subject">$prefix_icon<a href="#entry-$e.itemid" $comment_display""";
            }
        }
    }

    elseif ( $p isa DayPage ) {
        var DayPage cp = $p as DayPage;
        foreach var Entry e ( $cp.entries ) {
            var string comment_display =
                print_module_pagesummary_comments($e.subject, $e.comments.count, "text_read_comments", $e.comments.read_url + "#comments");
            var string poster = "<span class='pagesummary-poster'>" + $e.poster->ljuser() + "</span>";
            var string prefix_icon = $e.admin_post ? $admin_post_image->as_string($*text_icon_alt_admin_post) : "";

            if ( not $e.poster->equals($e.journal) ) {
                $links[size $links] = """$poster - <span class="pagesummary-subject">$prefix_icon<a href="#entry-$e.itemid" $comment_display""";
            }
            else {
                $links[size $links] = """<span class="pagesummary-subject">$prefix_icon<a href="#entry-$e.itemid" $comment_display""";
            }
        }
    }

    if ( size($links) < 1 ) { return; }
    open_module("pagesummary", $*text_module_pagesummary, "");
    print_module_list($links);
    close_module();
}

function print_module_tags() {
    var Page p = get_page();
    var string title = "";
    var TagDetail[] tags = $p->visible_tag_list($*module_tags_opts_limit);
    if ($*module_tags_opts_limit == 0) {
       $title = $*text_module_tags;
    }
    else {
       $title = $*text_module_popular_tags;
    }
    if (size($tags) < 1) {
        return;
    }
    elseif ($*module_tags_opts_type == "multi") {
        open_module("tags_multilevel", $title, $p.view_url{"tags"});
        print_multilevel_tags($tags, { "list-class" => "module-list", "item-class" => "module-list-item", "print_uses" => $*module_tags_opts_count_type });
        print_tag_manage_link();
        close_module();
    }
    elseif ($*module_tags_opts_type == "cloud") {
        open_module("tags_cloud", $title, $p.view_url{"tags"});
        print_cloud_tags($tags, { "list-class" => "module-list", "item-class" => "module-list-item", "print_uses" => $*module_tags_opts_count_type });
        print_tag_manage_link();
        close_module();
    }
    else {
        open_module("tags_list", $title, $p.view_url{"tags"});
        print_list_tags($tags, { "list-class" => "module-list", "item-class" => "module-list-item", "print_uses" => $*module_tags_opts_count_type });
        print_tag_manage_link();
        close_module();
    }
}

function print_module_active() {
    var Page p = get_page();
    # only print this module if the viewer can have active entries
    if ( $p.has_activeentries and not $p isa FriendsPage ) {
        var Entry[] activeentries = $p.activeentries;
        var string[] subjects;
        open_module( "active", $*text_module_active_entries, "" );
        var int i = 1;
                foreach var Entry activeentry ( $activeentries ) {
                    var string subject = ( $activeentry.subject != "" ? striphtml($activeentry.subject) : "$*text_nosubject" );
                    $subjects[size $subjects] = """<span class='active-entry-label active-entry-$i'>$i:</span> <a href="$activeentry.permalink_url">$subject</a>""";
                    $i++;
                 }
        print_module_list( $subjects );
        close_module();
    }
}

function print_module_calendar() {
    var Page p = get_page();
    var YearMonth mon = $p->get_latest_month();

    if ($*module_calendar_opts_type=="horizontal") {
        open_module("calendar calendar-horizontal","", "");
        print $mon->month_format("%%month%%", true);
        foreach var YearWeek week ($mon.weeks) {
            foreach var YearDay day ($week.days) {
                if ($day.num_entries > 0) {
                    var string entries = get_plural_phrase($day.num_entries, "text_calendar_num_entries");
                    print """<span class="entry-day"> <a href="$day.url" title="$entries">$day.day</a></span>""";
                }
                else {
                    print """<span class="empty-day"> $day.day </span>""";
                }
            }
        }
        print $mon->month_format("%%yyyy%%", true);
        close_module();
    }
    else {
        open_module("calendar", $mon->month_format("", true), "");
        println """<table summary="Monthly calendar with links to each day's entries">""";

        println "<tr>";
        foreach var int d (weekdays()) {
            "<th>"+$*lang_dayname_shorter[$d]+"</th>\n";
        }
       println "</tr>";

        foreach var YearWeek week ($mon.weeks) {
            println "<tr>";
            foreach var int i (1 .. $week.pre_empty) {
                print "<td>&nbsp;</td>";
            }

            foreach var YearDay day ($week.days) {
                if ( $day.num_entries > 0) {
                    var string entries = get_plural_phrase($day.num_entries, "text_calendar_num_entries");
                    print """<td class="entry-day"> <a href="$day.url" title="$entries">$day.day</a></td>""";
                }
                else {
                    print """<td class="empty-day">$day.day</td>""";
                }
            }

            foreach var int i (1 .. $week.post_empty) {
                print "<td>&nbsp;</td>";
            }
            println "</tr>";
        }

        println """</table>""";
        close_module();
    }
}
function print_module_syndicate() {
    var Page p = get_page();

    if ((size $p.data_links_order) < 1) { return; }
    open_module("syndicate", $*text_module_syndicate, "");

    foreach var string k ($p.data_links_order) {
        print " $p.data_link{$k} ";
    }

    close_module();
}
function print_module_customtext() {
    var Page p = get_page();
    open_module("customtext", $p.customtext_title, $p.customtext_url);
    print safe $p.customtext_content;
    close_module();
}
function print_module_links() {
    var Page p = get_page();
    var UserLink[] links = $p.linklist;
    if (size($links) < 1 or not $*linklist_support) { return; }

    var bool box_open = false;
    if (not $links[0].is_heading) {
        open_module("typelist", $*text_module_links, "");
        $box_open = true;
    }

    var string[] items = [];
    foreach var UserLink link ($links) {
        if ($link.is_heading and $link.title != "") {
            if ($box_open) {
                print_module_list($items);
                close_module();
                $items = [];
            }
            open_module("typelist", $link.title, "");
            $box_open = true;
        }
        if ($link.title == "") {
             print_module_list($items);
             $items = [];
        }
        if (not $link.is_heading and $link.title != "") {
            $items[size $items] = """<a href="$link.url" title="$link.hover">$link.title</a>""";
        }
    }
    if ($box_open) {
        print_module_list($items);
        print_linklist_manage_link();
        close_module();
    }
}

function print_module_credit() {
    var Page p = get_page();
    var string layoutname = $p.layout_name;
    var string themename = $p.theme_name;
    var string styleurl = $p.layout_url;
    var string ret = "";
    var int layout_authors_size = size $*layout_authors;
    var int theme_authors_size = size $*theme_authors;
    var string text_style_authors = ($theme_authors_size > 0) ? $*text_base_layout_authors : $*text_layout_authors;

    if ( ($layoutname != "") or ($layout_authors_size > 0) ) {

        # Print category title.

        $ret = $ret + "<li class='module-list-item'><span class='category-title'>$text_style_authors</span> ";

        # If theme name exists and style author and theme author are the same print it first.

        if ( ($themename != "") and ($theme_authors_size == 0) ) {
            $ret = $ret + "<span class='theme-name'>$themename</span> for ";
        }

        # If layout name exists print it.

        if ($layoutname != "") {
            $ret = $ret + "<span class='style-name'>";
            if ($styleurl != "") { $ret = $ret + """<a href="$styleurl">"""; }
            $ret = $ret + "$layoutname";
            if ($styleurl !="") { $ret = $ret + "</a>"; }
            $ret = $ret + "</span>";
        }


        if ($layout_authors_size > 0) {
            $ret = $ret + " by ";

            var int count = 0;

            foreach var string{} author ( $*layout_authors ) {
                var string item;
                if ( $author{"url"} != "" ) {
                        $item = "<a href='" + $author{"url"} + "'>" + $author{"name"} + "</a>";
                } elseif ( $author{"type"} == "user" ) {
                    var UserLite u = UserLite( $author{"name"} );
                    if ( defined $u ) {
                        $item = $u->ljuser();
                    }
                }
                if ( $item == "" ) {
                    $item = $author{"name"};
                }

                $ret = $ret + "<span class='style-author'>$item</span>";
                $count++;
                if ( $count < $layout_authors_size ) {
                    if ( $count == ($layout_authors_size -1) ) { $ret = $ret + " and "; }
                    else { $ret = $ret + ", "; }
                }
            }
        }

        $ret = $ret + "</li>\n";
    }

    if ($theme_authors_size > 0) {

        # Print category title.

        $ret = $ret  + "<li class='module-list-item'><span class='category-title'>$*text_theme_authors</span> ";

        # Print theme name if it exists.

        if ($themename != "") {
            $ret = $ret + "<span class='theme-name'>$themename</span> by ";
        }

        var int count = 0;

        foreach var string{} author ( $*theme_authors ) {
            var string item;
            if ( $author{"url"} != "" ) {
                $item = "<a href='" + $author{"url"} + "'>" + $author{"name"} + "</a>";
            } elseif ( $author{"type"} == "user" ) {
                var UserLite u = UserLite( $author{"name"} );
                if ( defined $u ) {
                    $item = $u->ljuser();
                }
            }
            if ( $item == "" ) {
                $item = $author{"name"};
            }

            $ret = $ret + "<span class='style-author'>$item</span>";
            $count++;
            if ( $count < $theme_authors_size ) {
                if ( $count == ($theme_authors_size -1) ) { $ret = $ret + " and "; }
                else { $ret = $ret + ", "; }
            }
        }

        $ret = $ret + "</li>\n";
    }

    var int resources_size = size $*layout_resources;
    if ( $resources_size > 0 ) {
        $ret = $ret + "<li class='module-list-item'><span class='category-title'>$*text_layout_resources</span> ";

        var int count = 0;
        foreach var string{} resource ( $*layout_resources ) {
            var string item;
            if ( $resource{"url"} != "" ) {
                $item = "<a href='" + $resource{"url"} + "'>" + $resource{"name"} + "</a>";
            } else {
                $item = $resource{"name"};
            }

            $ret = $ret + "<span class='resource-name'>$item</span>";
            $count++;
            if ( $count < $resources_size ) {
                if ( $count == ($resources_size -1) ) { $ret = $ret + " and "; }
                else { $ret = $ret + ", "; }
            }
        }
        $ret = $ret + "</li>\n";
    }

    if ( $ret != "" ) {
        open_module("credit", $*text_module_credit, "");
        print safe "<ul class='module-list'>$ret</ul>";
        close_module();
    }
}

function print_module_cuttagcontrols() {
    var Page p = get_page();

    open_module("cuttagcontrols", $*text_module_cuttagcontrols, "");

    print safe """<span class=\"cutTagControls\"><span style=\"font-size: smaller;\">$*text_cuttagcontrols_nocuttags</span></span>""";

    close_module();

}

function print_module_subscriptionfilters() {
    var Page p = get_page();

    if ( $p.journal.journal_type == "C" or $p.journal.journal_type == "Y" ) {
        return;
    }

    var SubscriptionFilter[] filters = journal_subscription_filters();

    if ( ( size $filters ) < 1 ) { return; }

    open_module("subscriptionfilters", $*text_module_subscriptionfilters, "");

    var string{}[] filter_list;

    var int pos = 0;
    foreach var SubscriptionFilter filter ( $filters ) {
        var string filter_class = $filter.public ? "public" : "private";
        $filter_list[$pos] = { "class" => "$filter_class", "item" => """<a href="$filter.url">$filter.name</a>""" };
        $pos++;
    }

    print_module_list($filter_list);

    print_subscriptionfilters_manage_link();

    close_module();
}

function handle_module_group_array(string[][] list) {
    foreach var string[] item ($list) {
        var string module = $item[0];

        # we can have blank modulenames because the ordering will not necessarily be consecutive
        if ($module == "") {
            continue;
        }
        elseif ($module == "userprofile") {
            print_module_userprofile();
        }
        elseif ($module == "navlinks") {
            print_module_navlinks();
        }
        elseif ($module == "time") {
            print_module_time();
        }
        elseif ($module == "poweredby") {
            print_module_poweredby();
        }
        elseif ($module == "pagesummary") {
            print_module_pagesummary();
        }
        elseif ($module == "tags") {
            print_module_tags();
        }
        elseif ($module == "active") {
            print_module_active();
        }
        elseif ($module == "calendar") {
            print_module_calendar();
        }
        elseif ($module == "syndicate") {
            print_module_syndicate();
        }
        elseif ($module == "customtext") {
            print_module_customtext();
        }
        elseif ($module == "links") {
            print_module_links();
        }
        elseif ($module == "credit") {
            print_module_credit();
        }
        elseif ($module == "search") {
            print_module_search();
        }
        elseif ($module == "cuttagcontrols") {
            print_module_cuttagcontrols();
        }
        elseif ($module == "subscriptionfilters") {
            print_module_subscriptionfilters();
        }
    }
}


function Page::print_module_section ( string section_name ) {
    """<div class="module-wrapper"><div class="separator separator-before"><div class="inner"></div></div>\n<div class="module-section-$section_name">\n<div class="inner">""";
    handle_module_group_array( $*module_sections{$section_name} );
    """</div>\n</div><div class="separator separator-after"><div class="inner"></div></div>\n</div>""";
}

function Page::print()
"The meat of each new layout. Describes how each page will look. In nearly all cases, the logic and decision-making processes should come from pre-existing functions in core2, and should not get written here. If you limit the structure of the page to HTML, function calls, and attached CSS, then you will be able to pick up all of the enhancements  and accessibility requirements managed by core2."
{
    """<!DOCTYPE html>\n<html lang="en">\n<head profile="http://www.w3.org/2006/03/hcard http://purl.org/uF/hAtom/0.1/ http://gmpg.org/xfn/11">\n""";
    $this->print_meta_tags();
    $this->print_head();
    $this->print_stylesheets();
    $this->print_head_title();
    """</head>""";
    $this->print_wrapper_start();
    $this->print_control_strip();
    """
    <div id="canvas">
        <div class="inner">
            <div id="header">
                <div class="inner">
                    """;
                    $this->print_header();
    """
                </div><!-- end header>inner -->
            </div><!-- end header -->
            <div id="content">
                <div class="inner">
    """;
                if ($*layout_type == "one-column-split") {
    """
                    <div id="secondary"><div class="inner">
    """;
                        $this->print_module_section("one");
    """
                    </div></div><!--  end secondary and secondary>inner -->
    """;
                }
    """
                    <div id="primary"><div class="inner">
                        """;
                        $this->print_body();
    """
                    </div></div><!-- end primary and primary>inner -->
    """;
                if ($*layout_type != "one-column-split") {
    """
                    <div id="secondary"><div class="inner">
    """;
                        $this->print_module_section("one");
    """
                    </div></div><!--  end secondary and secondary>inner -->
    """;
                }
    """
                    <div id="invisible-separator" style="float: left; width: 1px;"></div> <!-- this is a hack for IE7 + two-columns-right -->
                    <div id="tertiary"><div class="inner">
                        """;
                        $this->print_module_section("two");
    """
                    </div></div><!-- end tertiary and tertiary>inner -->
                    <div id="content-footer"></div>
                </div><!-- end content>inner -->
            </div> <!-- end content -->
        </div> <!-- end canvas>inner -->
    """;

    """
    <div id="footer">
        <div class="inner">
            """;
            print safe """
                <div class="page-top"><a href="#">$*text_page_top</a></div>
        </div><!-- end footer>inner -->
    </div><!-- end footer -->

    </div> <!-- end canvas -->
    """;
    $this->print_wrapper_end();
    """</html>""";
}

function Page::print_time() {
    $this->print_time("","");
}

function Page::print_time(string datefmt, string timefmt) {

    if ($datefmt == "") { $datefmt = $*entry_date_format; }

    if ($timefmt == "") {

        if (viewer_logged_in()) {
            if (viewer_is_owner()) {
                $timefmt = $*entry_time_format;
            } else {
                if ($this.timeformat24) {
                    $timefmt = "short_24";
                } else {
                    $timefmt = "short";
                }
            }
        } else {
            $timefmt = $*entry_time_format;
        }

    }

    var string ret;
    if ($datefmt != "none") { $ret = $ret + $this.local_time->date_format($datefmt); }
    if ($datefmt != "none" and $timefmt != "none") { $ret = $ret + " "; }
    if ($timefmt != "none") { $ret = $ret + $this.local_time->time_format($timefmt); }

    print safe """<span id="load-time">$*text_generated_on $ret</span>""";
}

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_navigation() { $this->print_navigation( { "" => "" } ); }
function Page::print_navigation( string{} opts ) {}

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 />";
    }
}

# Prints the name of the account which posted the entry
function Entry::print_poster {
    var Page p = get_page();
    var string emptyclass = ((($p isa RecentPage and not $p isa FriendsPage) or $p isa MonthPage or $p isa DayPage) and $p.journal.journal_type != "C") ? "empty" : "";
    print safe "<span class=\"poster entry-poster $emptyclass\">";
    if ($p isa FriendsPage or $p isa EntryPage or $p isa ReplyPage) {
        $this.poster->print();
        if (not $this.poster->equals($this.journal)) {
            print $*text_posting_in;
            $this.journal->print();
        }
    }
    else {
        if (not $this.poster->equals($this.journal)) {
          $this.poster->print();
        }
    }
    print safe "</span>";
}

# For any given comment, print the commentor's name (local, anonymous, or openid)
function Comment::print_poster() {
    var string poster = defined $this.poster ? $this.poster->as_string() : "<span class=\"anonymous\">$*text_poster_anonymous</span>";
    if ($this.metadata{"imported_from"}) { $poster = "<span class=\"imported-from\">$poster ($*text_openid_from " + $this.metadata{"imported_from"} + ")</span>"; }

    print safe "<span class=\"poster comment-poster\"><span class=\"comment-from-text\">$*text_comment_from</span> $poster";
    if ( $this.admin_post ) {
        """ <span class="admin-poster">(""";
        $this->print_admin_post_icon();
        print $*text_admin_post_note;
        """)</span>""";
    }
    """</span>\n""";
}


function Page::print_entry(Entry e)
"The meat of each new layout. Describes how each page will look. In nearly all cases, the logic and decision-making processes should come from pre-existing functions in core2, and should not get written here. If you limit the structure of the page to HTML, function calls, and attached CSS, then you will be able to pick up all of the enhancements  and accessibility requirements managed by core2."
{
    ## For most styles, this will be overridden by FriendsPage::print_entry and such.
    $e->print_wrapper_start();
    """<div class="header">\n""";
    """<div class="inner">\n""";
    $e->print_subject();
    $e->print_metatypes();
    $e->print_time();
    """</div>\n""";
    """</div>\n""";
    """<div>\n""";
    """<div class="contents">\n""";
    """<div class="inner">\n""";
    $e->print_userpic();
    $e->print_poster();
    if ($*entry_metadata_position == "top") { $e->print_metadata(); }
    $e->print_text();
    if ($*entry_metadata_position == "bottom") { $e->print_metadata(); }
    """</div>\n""";
    """</div>\n""";
    """</div>\n""";

    """<div class="footer">\n""";
    """<div class="inner">\n""";
    $e->print_tags();
    $this->print_entry_footer($e);
    "</div>\n</div>\n";

    $e->print_wrapper_end();

}
function EntryPreviewPage::print_entry_footer(Entry e)
"Disable links to avoid people trying to edit from a preview"
{
    $this->print_standout_box($this.preview_warn_text);
}

function EntryPage::print_entry_footer(Entry e) {
    $e->print_management_links();
    """<hr class="above-entry-interaction-links" />""";
    $e->print_interaction_links("topcomment");
    $this->print_reply_container({ "target" => "topcomment" });
    """<hr class="below-reply-container" />""";
}

function Page::print_entry_footer(Entry e) {
    $e->print_management_links();
    $e->print_interaction_links();
    $e->print_reply_container({ "target" => $e.dom_id + "-reply" });
}
function RecentPage::print_sticky_entry(StickyEntry s)
"function to print the sticky entry. can be overrised by styles to print it differently than other entries."
{
    $this->print_entry( $s );
}

function Comment::print_edit_text() {
    if ($this.edited) {
        var string editreason = $this.editreason == "" ? "" : "($this.editreason) ";
        print "<div class='edittime'><em>$*text_comment_edittime $editreason";
        $this->print_edittime();
        print "</em></div>";
    }
}

function EntryLite::print_text() [fixed] {
    if ($this isa Comment) {
        """<div class="comment-content">""";
        print $.text;
        var Comment c = $this as Comment;
        $c->print_edit_text();
        """</div>""";
    }
    else {
        """<div class="entry-content">""";
        print $.text;
        """</div>""";
    }
}

function EntryLite::print_subject() [fixed] {
    $this->print_subject({ "" => "" });
}

function EntryLite::print_subject( string{} opts ) [fixed] {
    if ($this isa Comment) {
        var Comment c = $this as Comment;
        """<h4 class="comment-title">""";
        if ($c.screened) {
            print $*text_screened + " ";
        }
        if ($c.frozen) {
            print $*text_frozen + " ";
        }
        if ($c.echi) {
            print "<span class='echi'>${c.echi}.</span> ";
        }
        print $this->formatted_subject( $opts );
        "</h4>";
    }
    else {
        if ($this isa StickyEntry) {
            var StickyEntry s = $this as StickyEntry;
            """<h3 class="sticky-entry-title entry-title">""";
            if ( $this.admin_post ) {
                $this->print_admin_post_icon();
            }
            $s->print_sticky_icon();
            if ( $this.admin_post ) {
                print $*text_admin_post_subject;
            }
            if ($*text_stickyentry_subject != "") { print $*text_stickyentry_subject + " "; }
            print $this->formatted_subject( $opts );
            "</h3>";
        }
        else {
            """<h3 class="entry-title">""";
            if ( $this.admin_post ) {
                $this->print_admin_post_icon();
                print $*text_admin_post_subject;
            }
            print $this->formatted_subject( $opts );
            "</h3>";
        }
    }
}

function StickyEntry::print_sticky_icon() {
    """<span class="sticky-entry-icon">""";
    $this.sticky_entry_icon->print($*text_icon_alt_sticky_entry);
    """</span>\n""";
}

function EntryLite::print_admin_post_icon() {
    var Image img = get_image("admin-post");

    """<span class="admin-post-icon">""";
    $img->print($*text_icon_alt_admin_post);
    """</span>\n""";
}

function EntryLite::print_wrapper_start() {
}
function Entry::print_wrapper_start() {
# FIXME: need to ensure that calling the alternate function doesn't interfere with the printing of comments or the printing of module groups.
    var Page p = get_page();
    var string alternate = alternate ("entry-wrapper-odd", "entry-wrapper-even");
    var string security = $this.security ? $this.security : "public";
    var string adult_content_level = $this.adult_content_level ? $this.adult_content_level : "none";
    var string journal_type = "journal-type-$this.journal.journal_type";
    var string poster;
    var string journal;
    var string mod_post = $this.admin_post ? "admin-post" : "";

    if ($this.journal.journal_type != "I") {
        $poster = clean_css_classname( "poster-$this.poster.username" );
        $journal = clean_css_classname( "journal-$this.journal.username" );
    }
    var string userpic = $this.userpic ? "has-userpic" : "no-userpic";

    var string entrysubject = (not $*all_entrysubjects and $.subject == "" and $p.view != "month") ? "no-subject" : "has-subject";

    #additional classes for sticky entries added
    if ($this isa StickyEntry) {
        """<div class="sticky-entry-wrapper entry-wrapper $alternate security-$security restrictions-$adult_content_level $journal_type $poster $journal $userpic $mod_post $entrysubject" id="sticky-entry-wrapper-$this.itemid">\n""";
        """<div class="separator separator-before"><div class="inner"></div></div>\n""";
        """<div class="sticky-entry entry" id="sticky-entry-$this.itemid">\n""";
    }
    else {
        """<div class="entry-wrapper $alternate security-$security restrictions-$adult_content_level $journal_type $poster $journal $userpic $entrysubject" id="entry-wrapper-$this.itemid">\n""";
        """<div class="separator separator-before"><div class="inner"></div></div>\n""";
        """<div class="entry" id="entry-$this.itemid">\n""";
    }
    """<div class="inner">\n""";
}

function Comment::print_wrapper_start() {
    var EntryPage ep = get_page() as EntryPage;
    var string alternate = alternate ("comment-wrapper-odd", "comment-wrapper-even");
    var string screened = ($this.screened and not $this.fromsuspended) ? "screened" : "visible";
    var string deletedstate = $this.deleted ? "deleted" : "";
    var string suspendedstate = $this.fromsuspended ? "from-suspended" : "";
    var string frozen = $this.frozen ? "frozen" : "";
    var string poster = defined $this.poster ? "poster-" + $this.poster.user : "poster-anonymous";
    if ($this.deleted or $this.fromsuspended or $this.screened_noshow) { $poster =  "poster-hidden"; }
    var string full = $this.full ? "full" : "partial";
    var string mod_post = $this.admin_post ? "admin-post" : "";

    var string userpic = $this.userpic ? "has-userpic" : "no-userpic";
    var bool commenthassubject = ($.screened or $.frozen or $.echi != "" or $.subject != "") ? true : false;
    var string commentsubject = (not $*all_commentsubjects and not $commenthassubject and $.full) ? "no-subject" : "has-subject";
    var string entryauthor = "";
    if (not isnull $ep and defined $this.poster and $this.poster.username == $ep.entry.poster.username) {
        $entryauthor = "entry-author";
    }
    """<div class="comment-wrapper $alternate $screened $frozen $deletedstate $suspendedstate $poster $entryauthor $full $userpic $mod_post $commentsubject">\n""";
    """<div class="separator separator-before"><div class="inner"></div></div>\n""";
    """<div class="comment" id="comment-$this.dom_id">\n""";
    """<div class="inner">\n""";
}

function EntryLite::print_wrapper_end() {
}
function Entry::print_wrapper_end() {
    "</div>\n</div>\n";
    """<div class="separator separator-after"><div class="inner"></div></div>\n""";
     "</div>\n";
}
function Comment::print_wrapper_end() {
    "</div>\n</div>\n";
    """<div class="separator separator-after"><div class="inner"></div></div>\n""";
    "</div>\n";
}

function EntryLite::print_metadata() {
}
function Entry::print_metadata() {
# If new entry metadata types are added, they must be explicitly called by key here

    if (size $.metadata) {
        var string position = ($*entry_metadata_position == "top") ? " top-metadata" : " bottom-metadata";
        """<div class="metadata$position">\n<ul>\n""";
        foreach var string m ( [ "mood", "location", "music", "groups", "xpost" ] ) {
                if ($.metadata{$m}) {
                var string metadata_name = lang_metadata_title($m);
                """<li id="metadata-$m"><span class="metadata-label metadata-label-$m">$metadata_name</span> """;
                if ($m == "mood") {
                    " $.mood_icon ";
                }
                """<span class="metadata-item metadata-item-$m">$.metadata{$m}</span></li>\n""";
            }
         }
        "</ul>\n</div>\n";
    }
}
function Comment::print_metadata() {
    if ($.metadata{"poster_ip"}) {
        print safe "<span class=\"poster-ip\"><span class=\"comment-ip-text\">$*text_comment_ipaddr</span> (" + $.metadata{"poster_ip"} + ")</span>";
    }
}

function EntryLite::print_metatypes() {
}
function Entry::print_metatypes(bool icon, bool info) {
    if ($this.security) {
        """<span class="access-filter">""";
        if ($icon) {
            $this.security_icon->print($this.security);
        }
        if ($info) {
            print $this.security;
        }
        """</span>\n""";
    }
    if ($this.adult_content_level) {
        """<span class="restrictions">""";
        if ($icon) {
            $this.adult_content_icon->print($this.adult_content_level);
        }
        if ($info) {
            print $this.adult_content_level;
        }
        """</span>\n""";
    }
}

function Entry::print_metatypes() {
    $this->print_metatypes(true, false);
}

function Comment::print_metatypes() {
    if (defined $this.subject_icon) {
        """<div class="comment-subjecticon">$this.subject_icon</div>""";
    }
}

function Entry::print_tags() [fixed] {
    if ($this.tags) {
        var Page p = get_page();
        var bool show_tagnav_links;
        if ($p isa RecentPage) {
            var RecentPage rp = $p as RecentPage;
            $show_tagnav_links = ($rp.filter_active and $rp.filter_tags);
        } else {
            $show_tagnav_links = ($p.view == "entry" or $p.view == "reply");
        }
        var int tag_count = 0;

        var string dataAttributes = "";
        if ( $show_tagnav_links ) {
          $dataAttributes = """ data-journal="$this.journal.username" data-ditemid="$this.itemid" """;
        }
        """<div class="tag"><span class="tag-text"$dataAttributes>$*text_tags</span><ul>\n""";

        foreach var Tag t ($this.tags) {
            "<li>";
            """<a rel="tag" href="$t.url">$t.name</a>""";
            $tag_count++;
            if ($tag_count < size $.tags) { print $*text_tags_item_sep; }
            "</li>\n";
        }
        "</ul></div>";
    }
}

function EntryLite::print_userpic() {
    print "<div class=\"userpic\">";
    if ( defined $this.userpic )
    {
        if ( $*use_shared_pic )
        {
            print """<a href="$this.journal.userpic_listing_url">""";
        }
        else
        {
            print """<a href="$this.poster.userpic_listing_url">""";
        }
        $this.userpic->print();
        print "</a>";
    }
    println "</div>";
}

function EntryLite::print_management_links() {}
function Comment::print_management_links()
"Prints comment management links, aka delete/screen/freeze/track."
{

    var string display_type = ($*comment_management_links == "text") ? " text-links" : " icon-links";
    var Link link;
    var int count;
    var string extras;
    $count = 0;
    foreach var string k ($.link_keyseq) {
        $link = $this->get_link($k);
        if ($link.url) {
            $count ++;
            if ($count == 1) {
                """<ul class="comment-management-links$display_type">""";
            }
            if ($*comment_management_links == "text") {
                foreach var string extra ( $link.extra ) {
                    var string value = $link.extra{$extra};
                    $extras = $extras + "$extra='$value' ";
                }
                """<li class="link $k""" + ( ($count == 1) ? " first-item" : "" ) + """"><a href="$link.url" $extras>$link.caption</a></li>\n""";
            }
            else { ## if ($*comment_management_links == "icon")
                """<li class="link $k""" + ( ($count == 1) ? " first-item" : "" ) + """">$link</li>\n""";
            }
        }
    }
    if ($count > 0) {
        """</ul>""";
    }
}

function EntryLite::print_interaction_links() {}
function Comment::print_interaction_links()
"Prints comment interaction links, aka reply/thread/parent/expand."
{
    var string display_type = ($*comment_interaction_links == "text") ? " text-links" : " icon-links";

    """<ul class="comment-interaction-links$display_type">""";
    print safe """<li class="link commentpermalink"><a href="$this.permalink_url">$*text_comment_link</a></li>\n""";
    if ($this.frozen) {
        print safe """<li class="frozen first-item">$*text_comment_frozen</li>\n""";
    } else {
        """<li class="link reply first-item">""";
        $this->print_reply_link({"linktext" => $*text_comment_reply});
        """</li>\n""";
    }
    if ($this.threadroot_url != "") { print safe """<li class="link threadroot"><a href="$this.threadroot_url">$*text_comment_threadroot</a></li>\n""";}
    if ($this.parent_url != "") { print safe """<li class="link commentparent"><a href="$this.parent_url">$*text_comment_parent</a></li>\n"""; }
    if ($this.thread_url != "") {
        print safe """<li class="link thread"><a href="$this.thread_url">$*text_comment_thread</a></li>\n""";
    }
    var Link expand_link = $this->get_link("expand_comments");
    if (defined $expand_link) {
        """<li class="link expand">""";
        $this->print_expand_link();
        """</li>\n""";
    }
    var Link hide_link = $this->get_link("hide_comments");
    if (defined $hide_link) {
         var string show_hide = "";
         if (not $this.hide_children) {
             $show_hide = " cmt_show_hide_default";
         }
        """<li class="link cmt_hide$show_hide" style="display:none;" id="cmt${this.talkid}_hide">""";
        $this->print_hide_link();
        """</li>\n""";
    }
    var Link unhide_link = $this->get_link("unhide_comments");
    if (defined $unhide_link) {
        var string show_hide = "";
        """<li class="link cmt_unhide$show_hide" style="display: none;" id="cmt${this.talkid}_unhide">""";
        $this->print_unhide_link();
        """</li>\n""";
    }
    """</ul>""";
}

function Entry::print_link_next() {
    var Link link = $this->get_link("nav_next");
    if ($*entry_management_links == "text") {
         """<a href="$link.url">$link.caption</a>""";
    }
    else {
        " $link";
    }
}
function Entry::print_link_prev() {
    var Link link = $this->get_link("nav_prev");
    if ($*entry_management_links == "text") {
         """<a href="$link.url">$link.caption</a>""";
    }
    else {
        " $link";
    }
}
function Entry::print_link_tag_next() {
    var Link link = $this->get_link("nav_tag_next");
    """<span class="tagnav"> <a href="$link.url">&rarr;</a></span>""";
}
function Entry::print_link_tag_prev() {
    var Link link = $this->get_link("nav_tag_prev");
    """<span class="tagnav"><a href="$link.url">&larr;</a> </span>""";
}
function Entry::print_management_links()
"Prints entry management links, aka prev/edit/tag/memory/tag/next, with standarized CSS."
{
    ## 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 string display_type = ($*entry_management_links == "text") ? " text-links" : " icon-links";

    var Page p = get_page();
    var int count;
    var string extras;
    $count = 0;
    var bool show_interentry = ($p.view == "entry" or $p.view == "reply");
    if ($show_interentry) {
        $count ++;
        if ($count == 1) {
            """<ul class="entry-management-links$display_type">""";
        }
        """<li class="link link_prev first-item">""";
        $this->print_link_prev();
        """</li>\n""";
    }
    var Link link;
    foreach var string k ($.link_keyseq) {
        $link = $this->get_link($k);
        if ($link.url) {
            $count ++;
            if ($count == 1) {
                """<ul class="entry-management-links$display_type">""";
            }
            if ($*entry_management_links == "text") {
                foreach var string extra ( $link.extra ) {
                    var string value = $link.extra{$extra};
                    $extras = $extras + "$extra='$value' ";
                }
                """<li class="link $k""" + ( $count == 1 ? " first-item" : "" ) + """"><a href="$link.url" $extras>$link.caption</a></li>\n""";
            }
            else { ## if ($*entry_management_links == "icon")
                """<li class="link $k""" + ( $count == 1 ? " first-item" : "" ) + """">$link</li>\n""";
            }
        }
    }
    if ($show_interentry) {
        $count ++;
        if ($count == 1) {
            """<ul class="entry-management-links$display_type">""";
        }
        """<li class="link link_next">""";
        $this->print_link_next();
        """</li>\n""";
    }
    if ($count > 0) {
        """</ul>""";
    }
}
function Entry::print_interaction_links() {
    $this->print_interaction_links( "" );
}

function Entry::print_interaction_links(string target)
"Prints entry interaction links, aka # comments/leave a comment, with standardized CSS."
{
    var string display_type = ($*entry_interaction_links == "text") ? " text-links" : " icon-links";

    var Page p = get_page();
    var int count;
    $count = 0;
    if ($p isa EntryPage) {
        var EntryPage ep = $p as EntryPage;
        if ($.comments.enabled and $ep.comment_pages.total_subitems > 0) {
            $count ++;
            if ($count == 1) {
                """<ul class="entry-interaction-links$display_type">""";
            }
            """<li class="entry-readlink first-item">""";
            $this.comments->print_readlink();
            "</li>\n";
        }
        if ($.comments.enabled) {
            if ($.comments.maxcomments) {
                print safe "$*text_max_comments";
            } else {
                $count ++;
                if ($count == 1) {
                    """<ul class="entry-interaction-links$display_type">""";
                }
                """<li class="entry-replylink""" + ( $count == 1 ? " first-item" : "" ) + """">""";
                $ep->print_reply_link({ "linktext" => $*text_post_comment, "target" => $target });
                "</li>\n";
            }
        }
        if ($count > 0) {
            "</ul>";
        }
    } elseif ($p.view == "read" or $p.view == "day" or $p.view == "recent" or $p.view == "network") {
        $this.comments->print($this, { "linktext" => $*text_post_comment_friends, "target" => $this.dom_id + "-reply" });
    } else {
        $this.comments->print();
    }
}

### 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.
    """
    <div id="entries" class="hfeed">
        <div class="inner">
            """;
        $this->print_navigation( { "class" => "topnav" } );

    foreach var Entry e ($.entries) {
        # Print the entry
        if ( $e isa StickyEntry ) {
            var StickyEntry s = $e as StickyEntry;
            $this->print_sticky_entry($s);
        }
        else {
            $this->print_entry($e);
        }
    }

            $this->print_navigation( { "class" => "bottomnav" } );
            """
        </div><!-- entries>inner-->
    </div><!-- entries -->
    """;


}

function RecentPage::print_navigation( string{} opts ) [fixed] {
    var bool noentries = $this isa FriendsPage and $.nav.backward_url == "" and $opts{"class"} == "bottomnav";
    # Navigation is empty if there are no links or text to show
    var bool empty = $.nav.backward_url == "" and $.nav.forward_url == "" and not $noentries;
    var string emptyclass = $empty ? " empty" : "";
    """
    <div class="navigation $opts{"class"}$emptyclass">
        <div class="inner">
    """;

    if (not $empty) {
        if ( $.nav.backward_url != "" or $.nav.forward_url != "" ) {
            "<ul>";
            if ( $.nav.backward_url != "" ) {
                print safe """<li class="page-back"><a href="$.nav.backward_url">""" + get_plural_phrase( $.nav.backward_count, "text_skiplinks_back" ) + "</a></li>\n";
            }
            if ( $.nav.backward_url != "" and $.nav.forward_url != "" ) {
                print safe """<li class="page-separator">$*text_default_separator</li>""";
            }
            if ( $.nav.forward_url != "" ) {
                print safe """<li class="page-forward"><a href="$.nav.forward_url">""" + get_plural_phrase( $.nav.forward_count, "text_skiplinks_forward" ) + "</a></li>\n";
            }
            "</ul>";
        }
    }
    if ($noentries) {
        print safe """<p class="noentries">$*text_noentries_read</p>""";
    }

    """
        </div><!-- navigation>inner -->
    </div><!-- navigation -->
    """;
}

function secs_to_string( int sec ) : string {
    var int retnum = $sec;
    var string rettype;
    if ( $sec < 60 ) {
        $rettype = "seconds";
    } elseif ( $sec < 3600 ) {
        $retnum = $retnum / 60;
        $rettype = "minutes";
    } elseif ( $sec < 86400 ) {
        $retnum =  $retnum / 3600;
        $rettype = "hours";
    } else {
        $retnum = $retnum / 86400;
        $rettype = "days";
    }
    return get_plural_phrase( $retnum, "time_ago_" + $rettype  );
}

function EntryLite::print_time(string datefmt, string timefmt) {
    print $this->time_display($datefmt, $timefmt);
}

function EntryLite::time_display(string datefmt, string timefmt) : string {

    if ($datefmt == "") { $datefmt = $*entry_date_format; }

    if ($timefmt == "") {

        if (viewer_logged_in()) {
            if (viewer_is_owner()) {
                $timefmt = $*entry_time_format;
            } else {
                if ($this.timeformat24) {
                    $timefmt = "short_24";
                } else {
                    $timefmt = "short";
                }
            }
        } else {
            $timefmt = $*entry_time_format;
        }

    }

    var string ret;
    if ($datefmt != "none") { $ret = $ret + $this.time->date_format($datefmt); }
    if ($datefmt != "none" and $timefmt != "none") { $ret = $ret + " "; }
    if ($timefmt != "none") { $ret = $ret + $this.time->time_format($timefmt); }

    return $ret;
}

function Entry::time_display(string datefmt, string timefmt) : string {
    var Page p = get_page();

    var bool linkify = ( $p.view == "recent" or $p.view == "entry" or $p.view == "reply" or $p.view == "day" );

    if ($datefmt == "") { $datefmt = $*entry_date_format; }

    if ($timefmt == "") {

        if (viewer_logged_in()) {
            if (viewer_is_owner()) {
                $timefmt = $*entry_time_format;
            } else {
                if ($this.timeformat24) {
                    $timefmt = "short_24";
                } else {
                    $timefmt = "short";
                }
            }
        } else {
            $timefmt = $*entry_time_format;
        }

    }

    var string ret;
    if ($datefmt != "none") { $ret = $ret + """<span class="date">""" + $this.time->date_format($datefmt, $linkify) + "</span>"; }
    if ($datefmt != "none" and $timefmt != "none") { $ret = $ret + " "; }
    if ($timefmt != "none") { $ret = $ret + """<span class="time">""" + $this.time->time_format($timefmt) + "</span>"; }

    if ($ret != "") { $ret = """<span class="datetime">$ret</span>"""; }

    return $ret;
}

# edittime argument is true if we want the get the edit time of the comment
function Comment::print_time (string datefmt, string timefmt, bool edittime) {
    print $this->time_display($datefmt, $timefmt, $edittime);
}
function Comment::time_display (string datefmt, string timefmt, bool edittime) : string {
    var DateTime time = ($edittime ? $this.edittime : $this.time);
    var DateTime time_remote = ($edittime ? $this.edittime_remote : $this.time_remote);
    var DateTime time_poster = ($edittime ? $this.edittime_poster : $this.time_poster);

    if ($datefmt == "") { $datefmt = $*comment_date_format; }

    if ($timefmt == "") {

        if (viewer_logged_in()) {
            if (viewer_is_owner()) {
                $timefmt = $*comment_time_format;
            } else {
                if ($this.timeformat24) {
                    $timefmt = "short_24";
                } else {
                    $timefmt = "short";
                }
            }
        } else {
            $timefmt = $*comment_time_format;
        }

    }

    var string tooltip = "";
    if ($edittime == false) {
        var string etime = secs_to_string($this.seconds_since_entry);
        $tooltip = $etime + " after journal entry";
    }

    var string main;

    var string display_date;
    var string display_time;

    if ($time_remote) {
        $display_date = $time_remote->date_format($datefmt);
        if ($timefmt == "none") { $display_date = $display_date + " (local)"; }
        $display_time = $time_remote->time_format($timefmt) + " (local)";
    } else {
        $display_date = $time->date_format($datefmt);
        if ($timefmt == "none") { $display_date = $display_date + " (UTC)"; }
        $display_time = $time->time_format($timefmt) + " (UTC)";
    }

    if (defined $time_poster and defined $this.poster)
    {
        var string poster_date = $time_poster->date_format($datefmt);
        if ($edittime == false) {
            $tooltip = $tooltip + ", ";
        }

        if ($poster_date == $display_date and $timefmt != "none") { $poster_date = ""; }
        else { $poster_date = $poster_date + " "; }

        if ($timefmt != "none") {
            $tooltip = $tooltip + $poster_date + $time_poster->time_format($timefmt) + " (" + $this.poster.username + "'s time)";
        } else {
            $tooltip = $tooltip + $poster_date + "(" + $this.poster.username + "'s time)";
        }
    }

    if ($datefmt != "none") { $main = $main + $display_date; }
    if ($datefmt != "none" and $timefmt != "none") { $main = $main + " "; }
    if ($timefmt != "none") { $main = $main + $display_time; }

    return "<span class=\"datetime\"><span class=\"comment-date-text\">" + $*text_comment_date + " </span> <span title=\"" + ehtml($tooltip) + "\">" + ehtml($main) + "</span></span>";
}

function Comment::print_time (string datefmt, string timefmt) {
    $this->print_time($datefmt, $timefmt, false);
}

function Comment::time_display (string datefmt, string timefmt) : string {
    return $this->time_display($datefmt, $timefmt, false);
}

function EntryLite::print_time() {
    print $this->time_display();
}

function EntryLite::time_display() : string {
    # Let the real function decide on some nice defaults
    return $this->time_display("", "");
}

function Comment::print_edittime() {
    print $this->edittime_display();
}

function Comment::edittime_display() : string {
    return $this->time_display("", "", true);
}

function Comment::print_edittime (string datefmt, string timefmt){
    print $this->edittime_display($datefmt, $timefmt);
}

function Comment::edittime_display (string datefmt, string timefmt) : string {
    return $this->time_display($datefmt, $timefmt, true);
}

### Year view

function YearPage::print_body {
var YearMonth[] months = $*reverse_sortorder_year ? reverse $.months : $.months;
    """
    <div id="archive-year">
        <div class="inner">
            """;
            $this->print_navigation( { "class" => "topnav" } );
            """
            <div class="year">
                <div class="inner">
                    """;
                    foreach var YearMonth m ($months) {
                        $this->print_month($m);
                    }
                    """
                </div><!-- year>inner -->
            </div><!-- year -->
            """;
            $this->print_navigation( { "class" => "bottomnav" } );
            """
        </div><!-- archive-year>inner -->
    </div><!-- archive-year -->
    """;
}

function YearPage::print_navigation( string{} opts ) [fixed] {
    """
    <div class="navigation $opts{"class"}">
        <div class="inner">
    """;

        $this->print_year_links();

    """
        </div><!-- navigation>inner -->
    </div><!-- navigation -->
    """;
}

function YearPage::print_year_links() {
    if (size $.years) {
        """<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; }

    var string month_label = $m->month_format();
    """
    <div class="month-wrapper">
        <div class="separator separator-before"><div class="inner"></div></div>
        <div class="month">
            <div class="inner">
                <div class="header">
                    <div class="inner">
                        <h3>$month_label</h3>
                    </div><!-- header>inner -->
                </div><!-- header -->
                <div class="contents">
                    <div class="inner">
                        <table summary="Monthly calendar with links to each day's entries" class="month" cellspacing="0" cellpadding="0">
                            <caption>$month_label</caption>
                            <thead>
                                <tr>""";
                                foreach var int d ( weekdays() ) {
                                    "<th>"+$*lang_dayname_short[$d]+"</th>\n";
                                }
                                """
                                </tr>
                            </thead>
                            <tbody>""";
                            foreach var YearWeek w ($m.weeks) {
                                $w->print();
                            }
                            """
                            </tbody>
                        </table>
                    </div><!-- contents>inner -->
                </div><!-- contents -->
                <div class="footer">
                    <div class="inner">
                        <a href="$m.url">$*text_view_month</a>
                    </div><!-- footer>inner -->
                </div><!-- footer -->
            </div><!-- month>inner -->
        </div><!-- month -->
        <div class="separator separator-after"><div class="inner"></div></div>
    </div><!-- month-wrapper -->
    """;
}

function YearWeek::print() {
   "<tr>";
   if ($.pre_empty > 0) {
      """<td class="day-empty" colspan="$.pre_empty">&nbsp;</td>\n""";
   }
   foreach var YearDay d ($.days) {
       if ($d.num_entries > 0) {
            """
            <td class="day day-has-entries">
                <span class="label">$d.day</span>
                <p><a href="$d.url">$d.num_entries</a></p>
            </td>
            """;
       } else {
            """
            <td class="day">
                <span class="label">$d.day</span>
            </td>
            """;
       }
   }
   if ($.post_empty > 0) {
      """<td class="day-empty" colspan="$.post_empty">&nbsp;</td>\n""";
   }

   "</tr>";
}

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

function MonthPage::print_body {
var MonthDay[] days = $*reverse_sortorder_month ? reverse $.days : $.days;
    """
    <div id="archive-month">
        <div class="inner">
    """;
    $this->print_navigation( { "class" => "topnav" } );

    """
            <div class="month">
                <div class="inner">
                    <dl>
                    """;

    foreach var MonthDay d ($days) {
        if ($d.has_entries) {
            """<dt><a href="$d.url">""";
                if ($*entry_date_format == "med" or $*entry_date_format == "med_day" or $*entry_date_format == "long" or $*entry_date_format == "long_day") {
                    print lang_ordinal($d.day);
                } else {
                    print "$d.day";
                }
            "</a></dt>";

            "\n<dd>";
            $d->print_subjectlist();
            "</dd>\n";
        }
    }
                """
                </dl>
            </div><!-- month>inner -->
        </div><!-- month -->
    """;

    $this->print_navigation( { "class" => "bottomnav" } );

    """
        </div><!-- archive-month>inner -->
    </div><!--archive-month -->
    """;
}

function MonthPage::print_navigation( string{} opts ) [fixed] {
    var bool empty = $.prev_url == "" and $.next_url == "";
    var string emptyclass = $empty ? "empty" : "";
    """
    <div class="navigation $opts{"class"} $emptyclass">
        <div class="inner">
            <ul>
            <form method='post' action='$.redir.url'>
            """;

            $.redir->print_hiddens();

            if ($.prev_url != "") { "<li class='month-back'>[<a href='$.prev_url'>&lt;&lt;&lt;</a>]</li>\n"; }

            if (size $.months > 1) {
                "<li><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' /></li>\n";
            }

            if ($.next_url != "") { "\n<li class='month-forward'>[<a href='$.next_url'>&gt;&gt;&gt;</a>]</li>\n"; }

            """
            </form>
            </ul>
        </div><!-- navigation>inner -->
    </div><!-- navigation -->
    """;
}

function MonthDay::print_subjectlist() {
var Entry[] entries = $*reverse_sortorder_month ? reverse $.entries : $.entries;
    # Too many tables...
    var Page p = get_page();

    foreach var Entry e ($entries) {
        $e->print_time("none", "");
        $e->print_poster();
        $e->print_metatypes();
        $e->print_subject();
        if ($e.comments.count > 0) {
            print safe " - " + get_plural_phrase($e.comments.count, "text_read_comments");
        }
        if ($e.comments.screened) {
            print safe " <b>$*text_month_screened_comments</b>";
        }
        "<br />\n";
        $e->print_tags();
        "<br />\n";
    }
}

### Day view

function DayPage::print_body() {
var Entry[] entries = $*reverse_sortorder_day ? reverse $.entries : $.entries;
    """
    <div id="archive-day">
        <div class="inner">
            <div id="entries">
                <div class="inner">
                    """;
                    $this->print_navigation( { "class" => "topnav" } );
                    """
                    <div class="day">
                        <div class="inner">
                    """;
                    print "<h3 class='day-date'>" + $.date->date_format($*entry_date_format) + "</h3>";
                    if ($.has_entries) {
                        foreach var Entry e ($entries) {
                            $this->print_entry($e);
                        }
                    } else {
                        print safe "<div class=\"text_noentries text_noentries_day\">$*text_noentries_day</div>";
                    }

                    """
                        </div><!-- day>inner -->
                    </div><!-- day -->
                    """;

                    $this->print_navigation( { "class" => "bottomnav" } );
                    """
                </div> <!-- entries>inner -->
            </div> <!-- entries -->
        </div> <!-- archive-day>inner -->
    </div><!-- archive-day -->
    """;
}

function DayPage::print_navigation( string{} opts ) [fixed] {
    var bool empty = $.prev_url == "" and $.next_url == "";
    var string emptyclass = $empty ? "empty" : "";
    """
    <div class="navigation $opts{"class"} $emptyclass">
        <div class="inner">
    """;

    if ( not $empty ) { "<ul>"; }
    if ( $.prev_url != "" ) {
        print safe """<li class="page-back"><a href="$.prev_url">$*text_day_prev</a></li>\n""";
    }
    if ( $.prev_url != "" and $.next_url != "" ) {
        print safe """<li class="page-separator">$*text_default_separator</li>""";
    }
    if ( $.next_url != "" ) {
        print safe """<li class="page-forward"><a href="$.next_url">$*text_day_next</a></li>\n""";
    }
    if ( not $empty ) { "</ul>"; }

    """
        </div><!-- navigation>inner -->
    </div><!-- navigation -->
    """;
}

### CommentInfo functions

function CommentInfo::print_readlink {
    var Page p = get_page();
    var string text_visible = get_plural_phrase($.count, "text_read_comments_screened_visible");
    var string text_screened = get_plural_phrase($.screened_count, "text_read_comments_screened");
    var string text_both = $text_visible + $*text_default_separator + $text_screened;
    var string text_total = get_plural_phrase($.count, $p.view == "read" ? "text_read_comments_friends" : "text_read_comments");
    var string text_sing = get_plural_phrase(1, $p.view == "read" ? "text_read_comments_friends" : "text_read_comments");
    var string text_dual = get_plural_phrase(2, $p.view == "read" ? "text_read_comments_friends" : "text_read_comments");
    var string text_plur = get_plural_phrase(3, $p.view == "read" ? "text_read_comments_friends" : "text_read_comments");

    if ($.screened) { # show visible count and screened count
        print safe """<a href="$.read_url#comments" data-sing="$text_sing" data-dual="$text_dual" data-plur="$text_plur">"""+ $text_both + """</a>""";
    } else {
        print safe """<a href="$.read_url#comments" data-sing="$text_sing" data-dual="$text_dual" data-plur="$text_plur">"""+ $text_total + """</a>""";
    }
}

function CommentInfo::print_postlink() {
    var Page p = get_page();
    if ($.maxcomments) {
        print safe "$*text_max_comments";
    } else {
        print safe "<a href=\"$.post_url\">"+($p.view == "read" or $p.view == "network" ? $*text_post_comment_friends : $*text_post_comment)+"</a>";
    }
}
function CommentInfo::print(Entry e, string{} opts) {
  var string display_type = ($*entry_interaction_links == "text") ? " text-links" : " icon-links";
  """<ul class="entry-interaction-links$display_type" data-quickreply-target="$opts{"target"}">\n""";
  """<li class="entry-permalink first-item"><a href="$.permalink_url">$*text_permalink</a></li>\n""";
  if ($.show_readlink) {
      """<li class="entry-readlink">""";
      $this->print_readlink();
      "</li>\n";
  } elseif ($.show_readlink_hidden) {
    """<li class="entry-readlink" style="display:none">""";
    $this->print_readlink();
    "</li>\n";
  }

  if ($.show_postlink and $.enabled) {
      """<li class="entry-replylink">""";
      if (isnull $e) {
        $this->print_postlink();
      } else {
        $e->print_reply_link({ "linktext" => $opts{"linktext"}, "target" => $opts{"target"} });
      }
      "</li>\n";
  }
  "</ul>";
}
function CommentInfo::print() {
  return $this->print(null Entry, { "" => "" });
}


### 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);
    var string{} extraLink = $.extra;
    var string{} extraImg = $.icon.extra;
    var string extraParamsLink = "";
    var string extraParamsImg = "";

    foreach var string extraKeyLink ($extraLink) {
        $extraParamsLink = $extraParamsLink + """$extraKeyLink="$extraLink{$extraKeyLink}" """;
    }

    foreach var string extraKeyImg ($extraImg) {
        $extraParamsImg = $extraParamsImg + """$extraKeyImg="$extraImg{$extraKeyImg}" """;
    }

    return """<a href="$.url" $extraParamsLink><img border='0' width="$.icon.width" height="$.icon.height" alt="$ealt" title="$ealt" src="$.icon.url" $extraParamsImg /></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_comment_section(Entry e) {
   "<div id='comments'><div class='inner'>";
   $.comment_pages->print({ "anchor" => "comments", "class" => "comment-pages toppages" });
   if ( $e.comments.comments_disabled_maintainer ) {
        """<div class='comments-message'>$*text_comments_disabled_maintainer</div>""";
   }
   if ($.comment_pages.total_subitems > 0) {
        $.comment_nav->print({ "class" => "comment-pages toppages" });
        $this->print_multiform_start();
   }
   $this->print_comments($.comments);
   if ($.comment_pages.total_subitems > 0) {
        "<div class='bottomcomment'>";
        $e->print_management_links();
        $e->print_interaction_links("bottomcomment");
        $this->print_reply_container({ "target" => "bottomcomment" });
        $this->print_multiform_actionline();
        $this->print_multiform_end();
        "</div>";
   }
   $.comment_pages->print({ "anchor" => "comments", "class" => "comment-pages bottompages" });
   if ($.comment_pages.total_subitems > 0) {
        $.comment_nav->print({ "class" => "comment-pages bottompages" });
   }
    "</div></div>";
}

function EntryPage::print_comments (Comment[] cs) {
    if (size $cs == 0) { return; }

    # Browsers stop applying styles to nested elements after a certain depth
    # Firefox stops at ~200
    # Chrome stops at ~500
    # So we stop nesting the .comment-thread element before we hit that point
    var int MAX_NESTED_DEPTH = 180;

    # Iterate over the comment tree structure
    var int prev_depth = -1;
    var int first_depth = -1;
    var Comment c;
    var Comment[] cs_stack = reverse $cs;
    while (size $cs_stack) {
        $c = pop $cs_stack;

        push $cs_stack, reverse $c.replies;

        # close some divs when not increasing depth
        if ($c.depth < $MAX_NESTED_DEPTH and $prev_depth >= 0) {
            for (var int i = $prev_depth; $i >= $c.depth; $i--) {
                "</div>\n";
            }
        }

        if ($c.depth < $MAX_NESTED_DEPTH) {
            $prev_depth = $c.depth;
        }

        if ($first_depth < 0) {
            $first_depth = $c.depth;
        }

        # actually print the comment
        var string parity = $c.depth % 2 ? "odd" : "even";
        var int indent = ($c.depth - 1) * 25;
        "<div class='comment-thread comment-depth-$parity comment-depth-$c.depth'>\n";
        "<div id='$c.dom_id' class='dwexpcomment' style='margin-left: ${indent}px; margin-top: 5px;";
        if ($c.hidden_child) {
            " display: none;";
        }
        "'>\n";
        if ($c.full) {
            $this->print_comment($c);
        } else {
            $this->print_comment_partial($c);
        }
        "</div>";

        if ($c.depth >= $MAX_NESTED_DEPTH) {
            print "</div>";
        }
    }

    if ($c.depth <= $MAX_NESTED_DEPTH) {
        # close any remaining divs
        for (var int i = $prev_depth; $i >= $first_depth; $i--) {
            "</div>\n";
        }
    }
}

function ReplyPage::print_comment (Comment c) {
# ReplyPage and EntryPage work nicest if they output the same structure for printing comments and entries, but this requires to manually change both ReplyPage::print_comment and EntryPage::print_comment.  Layout authors can also choose to override separately for different structures.
# Note that there is no multiform check on the ReplyPage.

    $c->print_wrapper_start();
    """<div class="header">\n""";
    """<div class="inner">\n""";
    $c->print_subject();
    $c->print_metatypes();
    $c->print_time();
    """</div>\n""";
    """</div>\n""";
    """<div class="contents">\n""";
    """<div class="inner">\n""";
    $c->print_userpic();
    """<div class="poster-information">\n""";
    $c->print_poster();
    $c->print_metadata();
    """</div>\n""";
    $c->print_text();
    """</div>\n""";
    """</div>\n""";
    """<div class="footer">\n""";
    """<div class="inner">\n""";
    $c->print_management_links();
    $c->print_interaction_links();
    $c->print_reply_container();
    "</div>\n</div>\n";
    $c->print_wrapper_end();
}

function EntryPage::print_comment (Comment c) {
# ReplyPage and EntryPage work nicest if they output the same structure for printing comments and entries, but this requires to manually change both ReplyPage::print_comment and EntryPage::print_entry.  Layout authors can also choose to override separately for different structures.

    $c->print_wrapper_start();
    """<div class="header">\n""";
    """<div class="inner">\n""";
    $c->print_subject();
    $c->print_metatypes();
    $c->print_time();
     if ( $c.comment_posted ) {
         print safe "<div class='comment-posted'>$*text_comment_posted</div>";
     }
    """</div>\n""";
    """</div>\n""";
    """<div class="contents">\n""";
    """<div class="inner">\n""";
    $c->print_userpic();
    $c->print_poster();
    $c->print_metadata();
    $c->print_text();
    """</div>\n""";
    """</div>\n""";
    """<div class="footer">\n""";
    """<div class="inner">\n""";
    if ($this.multiform_on) {
        """<span class="multiform-checkbox">""";
        print safe " <label for='ljcomsel_$c.talkid'>$*text_multiform_check</label> ";
        $c->print_multiform_check();
    "</span>";
    }
    $c->print_management_links();
    $c->print_interaction_links();
    $c->print_reply_container();
    "</div>\n</div>\n";
    $c->print_wrapper_end();
}

function EntryPage::print_comment_partial (Comment c) {
    $c->print_wrapper_start();
    if ($c.deleted) {
        print $*text_deleted;
        if ($c.hide_children) {
            var Link expand_link = $c->get_link("expand_comments");
            if (defined $expand_link) {
                print " (";
                $c->print_expand_link();
                print ")";
            }
        }
    }
    elseif ($c.fromsuspended) {
        print $*text_fromsuspended;
        if ($c.hide_children) {
            var Link expand_link = $c->get_link("expand_comments");
            if (defined $expand_link) {
                print " (";
                $c->print_expand_link();
                print ")";
            }
        }
    }
    elseif ($c.screened_noshow) {
        print $*text_screened;
        if ($c.hide_children) {
            var Link expand_link = $c->get_link("expand_comments");
            if (defined $expand_link) {
                print " (";
                $c->print_expand_link();
                print ")";
            }
        }
    }
    else {
        var string poster = defined $c.poster ? $c.poster->as_string() : "<i>$*text_poster_anonymous</i>";
        $c->print_subject();
        $c->print_poster(); " - ";
        $c->print_time();
        var Link expand_link = $c->get_link("expand_comments");
        if (defined $expand_link) {
            " - "; $c->print_expand_link();
    if ( $c.comment_posted ) {
         print safe " <span class='comment-posted'>$*text_comment_posted</span>";
     }
        }
    }
    $c->print_wrapper_end();
}

function ItemRange::print() {
    $this->print({ "anchor" => "", "class" => "" });
}

function ItemRange::print(string{} opts) {
    if ($.all_subitems_displayed) { return; }

    var string anchor = $opts{"anchor"} ? "#$opts{"anchor"}" : "";
    var string class = $opts{"class"} ? $opts{"class"} : "pages";

    """<div class="$class">""";
    print "<b>" + lang_page_of_pages($.current, $.total) + "</b>";
    var string url_prev = $this->url_of($.current - 1);
    if ($.current != 1) {
        print " <a href='$url_prev$anchor'>$*comment_page_prev</a> ";
    } else {
        print " $*comment_page_prev ";
    }
    foreach var int i (1..$.total) {
        if ($i == $.current) { "<span class=\"comment-page-$i comment-page-current\"><b>[$i]</b></span> "; }
        else {
            var string url_of = $this->url_of($i);
            "<a href='$url_of$anchor' class=\"comment-page\"><b>[$i]</b></a> ";
        }
    }
    var string url_next = $this->url_of($.current + 1);
    if ($.current != $.total) {
         print " <a href='$url_next$anchor'>$*comment_page_next</a> ";
    } else {
         print " $*comment_page_next ";
    }

    if ( $.url_all != "" ) {
        """<p><a href="$.url_all">View All</a></p>""";
    }

    "</div>";
}

function CommentNav::print() {
    $this->print({ "class" => "" });
}

function CommentNav::print(string{} opts) {

    var string class = $opts{"class"} ? $opts{"class"} : "pages";
    var string sep = $.url->contains( "?" ) ? "&" : "?";
    var string page_arg = $.current_page > 1 ? "&page=$.current_page" : "";

    print "<div class='$class'>";

    var string{} links = {
        "flat"      => """<span class="view-flat"><a href="$.url${sep}view=flat#comments">$*text_commentview_flat</a></span>""",
        "threaded"  => """<span class="view-threaded"><a href="$.url#comments">$*text_commentview_threaded</a></span>""",
        "top-only"  => """<span class="view-top-only"><a href="$.url${sep}view=top-only#comments">$*text_commentview_toponly</a></span>""",
        "expand_all"  => """<a href="$.url${sep}expand_all=1${page_arg}#comments" onClick="Expander.make(this,'$.url${sep}expand_all=1${page_arg}#comments',-1,false);return false;">Expand All</a>"""
    };

    if ( $.view_mode == "threaded" ) {
        print "$links{"flat"}${*text_default_separator}$links{"top-only"}";
    } elseif ( $.view_mode == "flat" ) {
        print "$links{"threaded"}${*text_default_separator}$links{"top-only"}";
    } elseif ( $.view_mode == "top-only" ) {
        print "$links{"threaded"}${*text_default_separator}$links{"flat"}";
    }

    if ( $.show_expand_all ) {
        print "${*text_default_separator}<span class='expand_all'>$links{"expand_all"}</span>";
    }
    print "</div>";
}

function EntryPage::print_body
{
    var Entry e = $.entry;
    $this->print_entry($e);
    $this->print_comment_section($e);
}

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

    if ($.replyto isa Entry) {
        var Entry e = $.replyto as Entry;
        $this->print_entry($e);
    }
    elseif ($.replyto isa Comment) {
        var Comment c = $.replyto as Comment;
        $this->print_comment($c);
    }
    $.form->print();
}

### IconsPage functions

function IconsPage::print_body {
    """<div class='icons-container'>\n<div class="inner">\n""";
    """<div class="header">\n<div class="inner">\n""";
    print safe "<h2>$*text_icons_page_header</h2>";
    "</div>\n</div>\n";
    """<div class="contents">\n<div class="inner">\n""";
    """<div class="sorting-options">\n<ul>\n""";
    var int sort_ct = 0;
    foreach var string k ($.sort_keyseq) {
        var string text = lang_icon_sortorder_title($k);
        if ( $k == $.sortorder ) {
            print safe """<li class='$k active'>$text""";
        } else {
            print safe """<li class='$k'><a href='$.sort_urls{$k}'>$text</a>""";
        }
        if ( (++$sort_ct) < size $.sort_keyseq) { print $*text_default_separator; }
        "</li>\n";
    }
    "</ul>\n</div>\n";
    $.pages->print({ "class" => "icon-pages toppages" });
    foreach var Icon i ($.icons) {
        $i->print();
    }
    $.pages->print({ "class" => "icon-pages bottompages" });
    "</div>\n</div>\n";
    """<div class="footer">\n<div class="inner">\n""";
    $this->print_icon_manage_link();
    "</div>\n</div>\n";
    "</div>\n</div>\n";
}

function Icon::print {
    var string default_class = $.default ? "icon-default" : "";
    var string inactive_class = $.active ? "" : "icon-inactive";

    """<div class="icon $default_class $inactive_class">\n""";
    """<div class="icon-image"><a href="$.link_url">"""; $.image->print(); """</a></div>\n""";
    """<div class="icon-info">\n""";
    if ($.default) {
        print safe "<div class='default label'>$*text_icons_default</div>\n";
    }
    if (not $.active) {
        print safe "<div class='inactive label'>$*text_icons_inactive</div>\n";
    }
    if ($.keywords) {
        """<div class="icon-keywords">""";
        var int keyword_count = 0;
        print safe "<span class='keywords-label'>$*text_icons_keywords</span>";
        """<ul>\n""";
        foreach var string kw ($.keywords) {
            $keyword_count++;
            """<li>""";
            print safe $kw;
            if ($keyword_count < size $.keywords) { print $*text_icons_keyword_sep; }
            """</li>\n""";
        }
        """</ul></div>\n""";
    }
    if ($.comment) {
        print safe "<div class='icon-comment'><span class='comment-text'>$*text_icons_comment</span> $.comment</div>\n";
    }
    if ($.description) {
        print safe "<div class='icon-description'><span class='description-text'>$*text_icons_description</span> $.description</div>\n";
    }
    """</div></div>\n""";
}

### TagsPage functions

function TagsPage::print_body
{
    """<div class='tags-container'>\n<div class="inner">\n""";
    """<div class="header">\n<div class="inner">\n""";
    print safe "<h2>$*text_tags_page_header</h2>";
    "</div>\n</div>\n";
    """<div class="contents">\n<div class="inner">\n""";

    if ($*tags_page_type == "multi") {
        print_multilevel_tags($.tags, { "list-class" => "ljtaglist tags_multilevel", "print_uses" => $*tags_page_count_type });
    }
    elseif ($*tags_page_type == "cloud") {
        print_cloud_tags($.tags, { "list-class" => "ljtaglist tags_cloud", "print_uses" => $*tags_page_count_type });
    }
    else {
        print_list_tags($.tags, { "list-class" => "ljtaglist tags_list", "print_uses" => $*tags_page_count_type });
    }

    "</div>\n</div>\n";
    """<div class="footer">\n<div class="inner">\n""";
    print_tag_manage_link();
    "</div>\n</div>\n";
    "</div>\n</div>\n";
}

### MessagePage functions

function MessagePage::view_title() : string {
    return $.title;
}

function MessagePage::print_body() {
    $this->print_links();
    $this->print_message();
}

function MessagePage::print_message() {
    print $.message;
}

function MessagePage::print_links() {
    println """<div style='text-align: center;'>""";
    foreach var string k ($.link_keyseq) {
        println """ $.links{$k} """;
    }
    println """</div>""";
}