Home Company Services Portfolio Contact us nav spacer

Skins

Skins

Jean Jordaan

In this section, we introduce the concepts of skins and layers used to collect and structure the templates that provide the UI of a Plone site, as well as customisation points for much of its functionality. We see how to use skins to customise the looks of a Plone site. We also look at some skin methods that modify the functionality of default Plone.

Skinning Plone

The term skinning is normally understood to mean changing the appearance of an application without affecting the underlying functionality. This is somewhat of a gray area: skins often shade over into the domain of functionality. Different Mozilla, WinAmp or GKrellm skins, for example, offer different widgets or amounts of information.

In the case of Plone, the portal_skins tool is also the first stop for visual customisation. However, it does a whole lot more than this. Besides all the templates for the rendering of Plone views and forms, the portal_skins also contain scripts that implement a lot of Plone's behaviour. Exposed as skin methods, they form a powerful customization layer.

The portal_skins tool makes use of Filesystem Directory Views, a mechanism introduced by the CMF to surface files on the filesystem as objects in a flat namespace in the ZODB. This enables different products to supply e.g. scripts, templates and properties without burdening the Plone developer with knowledge of how these products are organized internally. It also allows any product to supply alternate implementations of templates, stylesheets or scripts that may override existing implementations, depending on the configured precedence of skin layers.

Products conventionally package their skins in a directory called 'skins':


jean@klippie plone203 $ find Products/ -name skins 
Products/Epoz/skins
Products/PortalTransforms/skins
Products/PloneErrorReporting/skins
Products/Archetypes/skins
Products/CMFActionIcons/skins
Products/CMFCalendar/skins
Products/CMFPlone/skins
Products/CMFTopic/skins
Products/GroupUserFolder/skins
Products/CMFDefault/skins

Depending on the complexity of the product, the skins directory may itself contain a hierarchy of directories. CMFPlone contains 14 directories, organized in terms of different kinds of functionality. CMFCalendar contains two, but they are alternatives: a DTML and a ZPT implementation of the view and edit templates. This level of organization is transparent to the skin objects in the ZODB.

The portal_skins tool surfaces the filesystem objects in terms of layers, with a layer corresponding to a directory on the filesystem. The layers are combined to form skins. A skin is a list of layers that is searched sequentially. The first object found is returned.

The layers are combined into named skins on the Properties tab of the portal_skins tool. Here are the two default Plone skins:

Figure 2.2. Plone skin definitions

Plone skin definitions

They are identical, except for the plone_tableless layer which comes just before the plone_templates layer, and which provides three objects overriding the ones with the same names in plone_templates and plone_styles, namely colophon, main_template and ploneColumns.css

When new Plone products are installed via the Plone setup area, they normally add their skins to the top of the layer stack of all configured skins. We should have installed Epoz by now, so you should see the layer it provides just beneath the custom folder.

The portal_skins tool is not restricted to drawing its layers from Filesystem Directory Views. Any folders added under portal_skins take part in the skinning. custom is one such folder. By convention, it is always at the top of the layer stack, and it is the first location available for customization of skin objects. Since Filesystem Directory Views cannot be edited TTW, they are copied to the custom folder where they may be edited.

This is usually how the prototyping Plone looks or behaviour works --- the custom folder accumulates a selection of changed or rewritten objects from various other layers, sometimes differing only in their security settings or proxy roles. If the customization is meant to be deployed at different sites, or if different policies are possible, it makes sense to consolidate the customizations into a product of their own, and register a filesystem directory view for them.

Changing the Plone looks

The Plone templates have been designed to provide an easy global configuration route. Practically all the styling is done in CSS, and most of the elements of a Plone page have either class or id attributes, making it easy to address them directly in CSS.

Furthermore, the CSS is layered and parametricized. Different stylesheets are provided for different generations of browsers and for different media. The stylesheet parameters reflect the conceptualization of the Plone UI. We touched on that during our first encounter with Plone: actions, portlets, tabs and views.

In total, the look of a Plone page is derived as follows:

  • The header template (used from main_template) includes the following stylesheets, in sequence, and depending on their availability and the browser capabilities (various mechanisms are used to ensure that the right browsers get the right stylesheets): ploneNS4.css, ploneColumns.css, plone.css, ploneDeprecated.css, ploneTextSmall.css, ploneCustom.css, plonePrint.css, plonePresentation.css, ploneIEFixes.css.

    Finally, if the template being rendered provides CSS, it is included last of all.

    This translates to a fair number of browser requests per page view, and doesn't even include the Javascript and image files. All these files are relatively static, though, and are usually the first things that should be considered for caching when deploying a site.

  • As each of the stylesheets above is sourced, its values are filled in from the base_properties property sheet. These properties are documented in comments in ploneCustom.css.

    The stylesheet ploneCustom.css is always sourced, but originally consists of nothing but comment text. In order to keep the precedence and sourcing of the stylesheets predictable, you are meant to customize only this template, and to use the CSS cascade to override definitions in the other stylesheets.

Customizing base_properties and ploneCustom.css

The first stop when changing Plone looks is base_properties. It associates visual attributes such as colors, borders and font sizes with the meaningful elements of the Plone page. CSS rules to define the tabs in the header and above editable content, for example, need to be defined in various places, but they share concepts such as "selected tab". It makes sense to define the properties that relate them in one place, in base_properties, although the properties are used all over the place.

To see the impact that changes to base_properties can have, let's change the colour scheme of the site:


backgroundColor            #E6DFB3   /* was White       */
borderWidth                2px       /* was 1px         */
globalBorderColor          #F3EFD9   /* was #8cacbb     */
globalBackgroundColor      #CCBE99   /* was #dee7ec     */
contentViewBorderColor     #DAD094   /* was #74ae0b     */
contentViewBackgroundColor #DAD094   /* was #cde2a7     */
oddRowBackgroundColor      #e0d6aa   /* was transparent */
notifyBorderColor          #F3EFD9   /* was #ffa500     */
notifyBackgroundColor      #DAD094   /* was #ffce7b     */

Any designer could probably do better, but it serves to illustrate how all the Plone templates and content types now conform to the new scheme.

Figure 2.3. Beige colour scheme

Beige colour scheme

To understand how all the keys can be used, base_properties should be studied in conjunction with plone.css. For example, the CSS border-style statement can take up to four arguments to style the top, right, bottom and left edges of a box, and giving a value such as solid none none solid would work in most of the cases where borderStyle is used, but it would not make sense for the .contentActions .actionMenu, where it is repeated:


border-style: none &dtml-borderStyle; &dtml-borderStyle; &dtml-borderStyle;;

To change anything beyond the colours and fonts, e.g. the positioning of elements, it's necessary to write some CSS.

A word about the format of the stylesheets

You will have noticed that the stylesheets start out as Filesystem DTML Method instances. When you customise ploneCustom.css, it becomes a DTML Method. Why DTML?

Because DTML is not concerned with correct HTML structure, it is a good fit for templating non-HTML pages, such as emails and CSS. That's one of the reasons why it's worthwhile to have DTML in your armory even though it has been supplanted by ZPT for the creation of web pages.

The use of DTML in the Plone stylesheets is mostly limited to picking properties from base_properties with the dtml-var statement, mostly in the entity notation ( &dtml-) for the sake of compactness. There are one or two tricks to take note of, though.

The stylesheets start and end with some DTML in CSS comments (invisible to the DTML parser):


/* <dtml-with base_properties>  */
/* <dtml-call "REQUEST.set('portal_url', portal_url())">  */

...

/* </dtml-with> */

This just puts the base_properties propertysheet at the top of the namespace stack, and gets the absolute URL for the root of the Plone site from the portal_url tool.

There are some other interesting uses in plone.css. Wherever font names are included, the entity notation is not used:


body {
    font: &dtml-fontBaseSize; <dtml-var fontFamily>;
    background-color: &dtml-backgroundColor;;
    color: &dtml-fontColor;;
    margin: 0;
    padding: 0;
}

This is because fontFamily may include quotation marks ( "Lucida Grande", ...) that would be quoted as &quot;Lucida Grande&quot; by the entity notation, which quotes by default, and which would invalidate the CSS. Another interesting usage is:


#portal-logo a {
    display: block;
    text-decoration: none;
    overflow: hidden;
    border: 0;
    margin: 0;
    padding: 0;
    padding-top: <dtml-var "_[logoName].height">px;
    height: 0px !important;
    height /**/: <dtml-var "_[logoName].height">px;
    width: <dtml-var "_[logoName].width">px;
    cursor: pointer;
}

This one really isn't pretty. In this case, the logo image object needs to be looked up, and its height and width attributes. Within templates, the DTML namespace is available as a dictionary named _ (underscore). Since the logo's name is stored in the logoName property, we need to subscript the namespace with the value of logoName as key. This finds the logo image object by acquisition. Then we can use Python attribute access to get the height and width. The DTML parameter is qouted (equivalent to expr="quoted expression") to let the DTML parser know that we're working with a Python expression. This example does illustrate reaching out to the ZODB from the stylesheet.

The repeated height property and '!'/ /**/ tricks are browser kludges.

Changing the border styles

We've seen that the default stylesheet doesn't cater for varying the style of border edges. Let's change that, and give our portlets and content views 3d-style borders.

The quickest way to zoom in on the relevant CSS rules is by way of the Mozilla DOM Inspector. Zoom in on the main content block using the "Find a node" tool:

Figure 2.4. DOM Inspector find widget

DOM Inspector find widget

Click on the main content area, to orient yourself in the DOM, and browse the DOM treeview in the top left frame until the red highlight indicates the whole content area. At this point you should have the documentContent div selected. In the top right frame, select the Object - CSS Style Rules info from the dropdown. This is more or less what you'll see:

Figure 2.5. 'documentContent' division selected

'documentContent' division selected

Taking a closer look at the top right frame:

Figure 2.6. 'documentContent' CSS rules in the DOM Inspector

'documentContent' CSS rules in the DOM Inspector

This frame shows exactly where the styles that apply to the selected element come from, in order of precedence. In this case, the basic style (the browser's implementation of the standard) is indicated by a Mozilla resource:...URI, and the only style information that this contributes is the fact that div is a block-level element. There are two more matching roles, on lines 655 and 661 of plone.css respectively. The last one is the most specific, so that's where we should jump in. Lines 661 and following of plone.css look like this:


.documentEditable .documentContent {
    border: &dtml-borderWidth; &dtml-borderStyle; 
            &dtml-contentViewBorderColor;;
    padding: 0;
}

In order to intervene in its styling, add the following to 'ploneCustom.css':


.documentEditable .documentContent {
    border-color: &dtml-contentViewBorderColor; 
                  &dtml-globalBorderColor;
                  &dtml-globalBorderColor;
                  &dtml-contentViewBorderColor;;
}

(Note the last repeating semi-colon. The first one ends the DTML entity, and the second one terminates the CSS statement.) This refines the rules in plone.css, specifying the colors of each edge of the border.

We're making use of the existing globalBorderColor property. While this ensures that the borders will stay coordinated with the page colour scheme, it is questionable in the sense that we're using a global property to style content.

Following the same steps for the portlets, we'll find that the .portletBody rule (line 1052 of plone.css) is the one to augment. Add the following to 'ploneCustom.css':


.portletBody {
    border-color: &dtml-globalBorderColor; 
                  &dtml-contentViewBorderColor;
                  &dtml-contentViewBorderColor;
                  &dtml-globalBorderColor;;
}

I've reversed the occurrence of the global and content styles, in order to create an "inverted" effect with regard to the content area --- and also to prevent the borders from clashing with the borders of the portlet header. Here's the result:

Figure 2.7. Customised borders

Customised borders
Q:

Change the content tabs ( Contents, View, etc.) so that they don't break when they wrap. Currently, tabs that wrap look like untidily stacked boxes. Change them to look like an informal list, delimited by some character, on a common background, without so much padding.

A:

Look at .contentViews li and .contentViews li a. Redefine border and padding styles. The delimiter can be implemented as an image (cf. .link-external). If this won't do, you'll have to customize the template that renders the tabs. See skinning/creme-ploneCustom.css.dtml, specifically the rules on the content* classes.

Adding properties to base_properties

In default Plone, buttons are styled using the global, notify and background styles, or they are transparent. This makes for a consistent scheme, but it is somewhat lacking in variation. The style rules are grouped in plone.css under the header Widgets. It might make sense to add properties for e.g. standalone, context and destructive widgets to base_properties, and to apply them in ploneCustom.css. I'll leave that as an exercise.

Customising templates

Customising styles is relatively straightforward. It can be somewhat more involved to customise Plone templates. In order to find our way later, the first thing we need is a knowledge of the terrain. To find how everything fits together, we'll start with the master template, and progressively drill down.

The master template

The master macro is defined by the template .../portal_skins/plone_templates/main_template. Let's go through it step by step:


1  <metal:page define-macro="master"><metal:doctype
     define-slot="doctype"><!DOCTYPE
     html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"></metal:doctype>

2  <metal:block define-slot="top_slot" />
3  <metal:block use-macro="here/global_defines/macros/defines" />
4  <html xmlns="http://www.w3.org/1999/xhtml"
5        xml:lang="en"
6        lang="en"
7        tal:attributes="lang default_language|default;
8                        xml:lang default_language|default;">

The first four lines precede the html element. The master template starts immediately, and it immediately defines a doctype slot with a default of "XHTML Transitional". There are no intervening spaces, for reasons of validity. If templates want to supply a specific doctype, they can fill this slot.

Line 2 defines an empty top_slot, which may be used by templates that need to do stuff before any Plone templating has occurred. One use to which this slot may be put is to define additional global names, or to influence the definition of the globals that are defined on line 3. For example, global_defines contains the definition:


global object_title here/Title;

If top_slot redefined Title, for example by doing:


global Title string:Title: ${here/Title}

all titles in the site would be prepended by the string "Title: ". On line 8, the page proper begins. (I'm going to gloss over most of the detail for the moment, to get the bigger picture.)

The HEAD element

Lines 9 to line 33 [3] construct the head element. Here, main_template uses the html_header macro from the header template, filling and redefining the slots base, head_slot, css_slot and javascript_head_slot. The slots are redefined so that they remain available for all the templates that use the master macro.

On line 34 the body starts. It immediately opens the visual-portal-wrapper division, which wraps the entire visible part of the Plone page.

The page header

The next line opens the portal-top division. It corresponds to the whole header part of the page, up to the breadcrumbs:

Figure 2.8. Plone header

Plone header

A number of macros are used to construct the header. They are: portal_logo (from global_logo), skin_tabs (from global_skinswitcher), site_actions (from global_siteactions), quick_search (from global_searchbox), portal_tabs (from global_sections), personal_bar (from global_personalbar), and path_bar (from global_pathbar). Now you know where to look if you want to customise any aspect of the header.

Besides these, master supplies a navigation link for non-CSS browsers and sets some i18n properties.

The page body

On line 63, the body is sliced from the header, making sure that it starts as a new block. The body starts as a table which implements the default three-column layout. Here are just the bare bones:


 67 <table id="portal-columns">
 68   <tbody>

 69     <tr>
 70       <!-- start of the left (by default at least) column -->
 71       <td id="portal-column-one"
 72           metal:define-slot="column_one_slot"
 73           tal:condition="slots_mapping/left">
[...]
 82       </td>
 83       <!-- end of the left (by default at least) column --> 
 84       <!-- start of main content block -->

 85       <td id="portal-column-content"
 86           tal:define="tabindex python:Iterator(pos=0)">
[...] 
117       </td>
118       <!-- end of main content block --> 
119       <!-- start of right (by default at least) column -->
120       <td id="portal-column-two"
121           metal:define-slot="column_two_slot"
122           tal:condition="slots_mapping/right">

[...] 
131       </td>
132       <!-- end of the right (by default at least) column -->
133     </tr>
134   </tbody>
135 </table>

136 <!-- end column wrapper -->

Note that the left and right columns will only appear if there are portlets to be rendered in them (the tal:condition statements). The iterator defined in the portal-column-content column ensures that input fields in the content area get the initial focus when tabbing through the page, and that tabbing proceeds in an orderly fashion.

The page footer

Line 137 again separates the body from the footer. The footer area is filled by the portal_footer (from footer) and colophon (from colophon) macros.

Figure 2.9. Plone footer

Plone footer

The left and right columns

These two columns work identically. They use the left_column and right_column macros from portlets_fetcher. Here is the left column:


75 <metal:portlets define-slot="portlets_one_slot">
76   <metal:leftportlets use-macro="here/portlets_fetcher/macros/left_column">

77     This instruction gets the portlets (boxes) for the left column.
78   </metal:leftportlets>
79 </metal:portlets>

We can look at portlets_fetcher a bit later --- it uses some TAL magic, and there's a white rabbit to follow, starting at slots_mapping.

The centre column

This is where the content of the page is rendered. The whole content block is defined as a slot which may be overridden by any template using the master macro, but nothing in default Plone does this:


 87 <metal:block define-slot="content">
 88   <div id="content"
 89        metal:define-macro="content"
 90        tal:define="show_border is_editable;"
 91        tal:attributes="class python:test(show_border,'documentEditable','')">

 92     <metal:ifborder tal:condition="show_border" >
 93       <div metal:use-macro="here/global_contentviews/macros/content_views">
 94         The content views (View, Edit, Properties, Workflow)
 95       </div>
 96       <div metal:use-macro="here/global_contentviews/macros/content_actions">
 97         The content bar
 98       </div>

 99     </metal:ifborder>
100     <div class="documentContent" id="region-content">
101       <a name="documentContent"></a>
102       <div metal:use-macro="here/global_statusmessage/macros/portal_message">

103         Portal status message
104       </div>
105       <metal:header metal:define-slot="header" tal:content="nothing">
106         Visual Header
107       </metal:header>
108       <metal:bodytext metal:define-slot="main" tal:content="nothing">
109         Page body text
110       </metal:bodytext>

111       <metal:sub metal:define-slot="sub">
112         <metal:discussion use-macro="here/viewThreadsAtBottom/macros/discussionView" />
113       </metal:sub>
114     </div>
115   </div>

116 </metal:block>

It then starts the content division (line 88) and defines a macro content, in case a Plone application needs to create templates that render editable content. In default Plone, main_template is the only one that does this, so the macro isn't called.

On line 91 a reasonably involved test is done to determine whether the object being rendered is editable, and if so, the content_views and content_actions macros (both from global_contentviews) are used. They render these parts of the page, respectively:

Figure 2.10. Content views

Content views

Figure 2.11. Content actions

Content actions

Before we get to the content, the portal_message macro from global_statusmessage is used to render any status message that needs showing (on line 102).

Now, at last, we reach the core of the page. The slots header, main and sub are defined. The first two are empty. In default Plone, the header slot goes unused --- all the content templates fill just the main slot. The sub slot is filled by discussionView (from viewThreadsAtBottom), so if any template fills it, the discussion settings at that point will be ignored. (The discussion templates do fill it, in order to prevent recursion.)

The main slot renders the content:

Figure 2.12. Main content area

Main content area

If discussion has been enabled on the content type being rendered, the sub slot renders an Add Comment button together with any replies accumulated so far:

Figure 2.13. The 'sub' slot with a comment

The 'sub' slot with a comment

A content type template

Let's look at the newsitem_view template again. On its HTML element, it uses the master macro, and allows it to fill in the header, portlets, and footer, with all the navigation and global actions:


1  <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
2        lang="en"
3        metal:use-macro="here/main_template/macros/master"
4        i18n:domain="plone">

The rest of the template only fills the main slot. Like most of the other content type templates, it simultaneously defines the slot as a macro. None of these macros are ever used in default Plone, but I suppose they're useful in composite documents (where you need to incorporate a rendered news item in another document, for example):


 6  <div metal:fill-slot="main">

 7      <tal:main-macro metal:define-macro="main"
 8            tal:define="len_text python:len(here.text)">
[...]
36      </tal:main-macro>
37  </div> 

The template then renders a headline, the document actions (using the document_actions macro), the description, and the body text (catering for a blank item, and supplying an stx wrapper DIV element for styling the rendered StructuredText HTML):


21 <p tal:condition="python: not len_text and is_editable"
22    i18n:translate="no_body_text"
23    class="discreet">
24     This item does not have any body text, click the edit tab to change it.
25 </p>
26 
27 <div class="stx"
28      tal:condition="len_text"
29      tal:omit-tag="python:here.text_format != 'structured-text'">
30     <div tal:replace="structure python:here.CookedBody(stx_level=2)" />
31 </div>

Finally, the document byline is constructed and rendered by the byline macro from document_byline.

Customising functionality in skins

In this section, we aren't concerned with looks, but with the integration of I18NLayer instances in the Plone UI.

Fixing breadcrumbs

This is a one-liner: add a meta type to the dont_show_metatypes list:


dont_show_metatypes = ['TempFolder', # Metatypes of objects we wont show
                       'Plone Factory Tool',
                       'Plone Form Tool', 
                       # Upfront. (This will make breadcrumbs lazy if
                       # you change browser preference: the URL will
                       # still be pointing at a specific language. Maybe
                       # there's a setting on 'portal_languages' that
                       # will mitigate this)
                       'I18NLayer', 
                       ]

Fixing the News portlet

The crux is changing the items that are selected for display in the portlet. In the original, this is what we have:


<div metal:define-macro="portlet"
            tal:define="results python:request.get('news',
            here.portal_catalog.searchResults( portal_type='News Item',
            sort_on='Date',
            sort_order='reverse',
            review_state='published')[:5]);
            " 
        tal:condition="python:test(template.getId()!='news' and results, 1, 0)">

We'll change that tal:define, factoring out the selection into a Python script:


tal:define="results python:request.get('news', here.itemsForPortletNews());

Here is the script:


## Script (Python) "itemsForPortletNews"
##bind container=container
##bind context=context
##bind namespace=
##bind script=script
##bind subpath=traverse_subpath
##parameters=
##title=
##
""" Return a filtered list of objects for a portlet.
"""

proxies = context.portal_catalog.searchResults(
            portal_type='News Item',
            sort_on='Date',
            sort_order='reverse', review_state='published')[:5]

return context.filterI18NLayerItems(proxies)

We're delegating to another new script to be added to the custom layer:


## Script (Python) "filterI18NLayerItems"
##bind container=container
##bind context=context
##bind namespace=
##bind script=script
##bind subpath=traverse_subpath
##parameters=proxies
##title=
##
""" We don't want duplicates of I18Nized items to be returned. Return
    the I18NLayer representing the item instead of the item itself.
"""

filtered = []
for p in proxies:
    obj = p.getObject()
    if not obj:
        continue
    if obj.aq_parent.portal_type == 'I18NLayer':
        i18n_obj = obj.aq_parent
        if i18n_obj not in filtered:
            filtered.append(obj.aq_parent)
    else:
        filtered.append(obj)

return filtered

The most important difference between the new selection scripts and the original catalog query, is that the new scripts return objects instead of catalog proxies. To accommodate this, some more fixes need to be made in portlet_news. Here's the new one:


<html xmlns:tal="http://xml.zope.org/namespaces/tal"
    xmlns:metal="http://xml.zope.org/namespaces/metal"
    i18n:domain="plone">

<body>

<!-- The news box -->
<!-- Upfront: Use a custom filter to get items;
            adjust rendering to expect objects, not proxies (i.e.
            *call* stuff); use 'absolute_url', not getUrl.
-->

<div metal:define-macro="portlet"
    tal:define="results python:request.get('news', here.itemsForPortletNews());
        "
    tal:condition="python:test(
            template.getId()!='news' and results, 1, 0)">

    <div class="portlet" id="portlet-news">
        <h5 i18n:translate="box_news">News</h5>
        <div class="portletBody">
            <tal:block tal:repeat="obj results">
                <div tal:define="oddrow repeat/obj/odd"
                    tal:attributes="class python:test(oddrow, 'portletContent even', 'portletContent odd')">

                    <a href=""
                    tal:attributes="href obj/absolute_url;
                                    title obj/Description">
                    <tal:block replace="structure 
                                    here/newsitem_icon.gif"/>
                    <span tal:replace="python:test(
                        obj.Title(), obj.Title(), obj.getId())">
                        Extended Calendar Product </span>
                    </a>
                    <div class="portletDetails"
                        tal:content="python:here.toPortalTime(
                            obj.Date())">July 7, 08:11</div>

                </div>
            </tal:block>
            <div class="portletContent odd">
                <a href=""
                class="portletMore"
                tal:attributes="href string:${utool}/news"
                i18n:translate="box_morelink"
                >
                    More...
                </a>
            </div>

        </div>
    </div>
</div>
</body>
</html>
Q:

The same kinds of fixes are necessary in the other templates that render localised items. Do topic_view, portlet_navigation, portlet_events, i18ncontent_slot, itemsForPortletEvents. Other outstanding ones are the search results, and the News and Press Releases templates.

A:

See the skinning folder.



[3] counting only non-empty lines

Document Actions