changeset 1295:4237934df124

rework transclusion overlay patch - improve transcluded item visibility, fix HTML validation errors, fixes #132 and #164
author Roger Haase <crosseyedpenguin@yahoo.com>
date Sun, 18 Mar 2012 12:55:06 -0700
parents 74b0ae78492b
children 733cb34e4c50
files MoinMoin/config/default.py MoinMoin/converter/_tests/test_include.py MoinMoin/converter/html_out.py MoinMoin/converter/include.py MoinMoin/static/js/common.js MoinMoin/templates/itemviews.html MoinMoin/themes/modernized/static/css/common.css
diffstat 7 files changed, 220 insertions(+), 100 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/config/default.py	Sat Mar 10 20:16:29 2012 +0100
+++ b/MoinMoin/config/default.py	Sun Mar 18 12:55:06 2012 -0700
@@ -366,6 +366,8 @@
         ('special.supplementation', None, None, False, ),
         ('frontend.index', L_('Index'), L_('List sub-items'), False, ),
         ('special.comments', L_('Comments'), L_('Switch showing comments on or off'), True, ),
+        # note: the | character below separates the off/on title (tooltip) values that will be swapped by javascript
+        ('special.transclusions', L_('Transclusions'), L_('Show transclusions|Hide transclusions'), True, ),
         ('frontend.highlight_item', L_('Highlight'), L_('Show with Syntax-Highlighting'), True, ),
         ('frontend.show_item_meta', L_('Meta'), L_('Display Metadata'), True, ),
         ('frontend.quicklink_item', None, L_('Create or remove a navigation link to this item'), False, ),
--- a/MoinMoin/converter/_tests/test_include.py	Sat Mar 10 20:16:29 2012 +0100
+++ b/MoinMoin/converter/_tests/test_include.py	Sun Mar 18 12:55:06 2012 -0700
@@ -72,32 +72,36 @@
         update_item(u'page4', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{page2}}')
 
         page1 = MoinWiki.create(u'page1')
+        rendered = page1._render_data()
+        # an error message will follow strong tag
+        assert '<strong class="moin-error">' in rendered
 
-        page1._render_data()
+    def test_ExternalInclude(self):
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{http://moinmo.in}}')
+        rendered = MoinWiki.create(u'page1')._render_data()
+        assert '<object class="moin-http moin-transclusion" data="http://moinmo.in" data-href="http://moinmo.in">http://moinmo.in</object>' in rendered
 
     def test_InlineInclude(self):
         # issue #28
-        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Content of page2 is "{{page2}}"')
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Content of page2 is "{{page2}}".')
 
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Single line')
         rendered = MoinWiki.create(u'page1')._render_data()
-        assert 'Content of page2 is "Single line"' in rendered
+        assert '<p>Content of page2 is "<span class="moin-transclusion" data-href="/page2">Single line</span>".</p>' in rendered
 
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Two\n\nParagraphs')
         rendered = MoinWiki.create(u'page1')._render_data()
-        assert '<p>Two</p>' in rendered
-        assert '<p>Paragraphs</p>' in rendered
+        assert '<p>Content of page2 is "</p><div class="moin-transclusion" data-href="/page2"><p>Two</p><p>Paragraphs</p></div><p>".</p></div>' in rendered
 
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"this text contains ''italic'' string")
         rendered = MoinWiki.create(u'page1')._render_data()
-        assert 'Content of page2 is "this text contains' in rendered
-        assert '<em>italic</em>' in rendered
+        assert 'Content of page2 is "<span class="moin-transclusion" data-href="/page2">this text contains <em>italic</em>' in rendered
 
         update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Content of page2 is\n\n{{page2}}')
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"Single Line")
         rendered = MoinWiki.create(u'page1')._render_data()
-        assert 'Content of page2 is</p>' in rendered
-        assert '<p>Single Line</p>' in rendered
+        assert '<p>Content of page2 is</p><p><span class="moin-transclusion" data-href="/page2">Single Line</span></p>' in rendered
+        #           '<p>Content of page2 is</p><p><span class="moin-transclusion" data-href="http://127.0.0.1:8080/page2">Single Line</span></p>'
 
         update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Content of page2 is "{{page2}}"')
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"|| table || cell ||")
@@ -112,3 +116,17 @@
         assert 'Content of page2 is "</p>' in rendered
         assert '<table>' in rendered
         assert rendered.count('<table>') == 1
+
+    def test_InlineIncludeLogo(self):
+        # the 3rd parameter, u'',  should be a binary string defining a png image, but it is not needed for this simple test
+        update_item(u'logo', {CONTENTTYPE: u'image/png'}, u'')
+
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{logo}}')
+        rendered = MoinWiki.create(u'page1')._render_data()
+        assert '<img alt="logo" class="moin-transclusion"' in rendered
+
+        # <p /> is not valid html5; should be <p></p>. to be valid.  Even better, there should be no empty p's.
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{logo}}{{logo}}')
+        rendered = MoinWiki.create(u'page1')._render_data()
+        assert '<p />' not in rendered
+        assert '<p></p>' not in rendered
--- a/MoinMoin/converter/html_out.py	Sat Mar 10 20:16:29 2012 +0100
+++ b/MoinMoin/converter/html_out.py	Sun Mar 18 12:55:06 2012 -0700
@@ -12,48 +12,50 @@
 
 from __future__ import absolute_import, division
 
+import re
+
+from flask import request
 from emeraldtree import ElementTree as ET
 
 from MoinMoin import wikiutil
 from MoinMoin.i18n import _, L_, N_
 from MoinMoin.util.tree import html, moin_page, xlink, xml, Name
 
-
-def remove_overlay_prefixes(url):
-    """
-    Returns url without the prefixes, like +get or +modify
-
-    TODO: Find a way to limit the removal to internal links only
-    This could remove +get or +modify for external links,
-        when they shouldn't really be removed.
-    """
-    return unicode(url).replace("/+get/", "/+show/").replace("/+modify/", "/+show/")
+from MoinMoin import log
+logging = log.getLogger(__name__)
 
 
-def wrap_object_with_overlay(elem, href):
+def convert_getlink_to_showlink(href):
     """
-    Given both an element and either an href or text, wraps an object with the appropriate div,
-    and attaches the overlay element.
+    If the incoming transclusion reference is within this domain, then remove "+get/<revision number>/".
     """
-    txt = u"→"
-
-    href = remove_overlay_prefixes(href)
+    if href.startswith('/'):
+        return re.sub(r'\+get/\+[0-9a-fA-F]+/', '', href)
+    return href
 
-    child = html.a(attrib={
-        html.href: href
-    }, children=(txt, ))
+def mark_item_as_transclusion(elem, href):
+    """
+    Return elem after adding a "moin-transclusion" class and a "data-href" attribute with
+    a link to the transcluded item.
 
-    overlay = html.div(attrib={
-        html.class_: "object-overlay"
-    }, children=(child, ))
-
-    owrapper = html.div(attrib={
-        html.class_: "object-overlay-wrapper"
-    }, children=(overlay, ))
-
-    return html.div(attrib={
-        html.class_: "page-object"
-    }, children=(elem, owrapper))
+    On the client side, a Javascript function will wrap the element (or a parent element)
+    in a span or div and 2 overlay siblings will be created.
+    """
+    href = unicode(href)
+    # href will be "/wikiroot/SomeObject" or "/SomePage" for internal wiki items
+    # or "http://Some.Org/SomeThing" for external link
+    if elem.tag.name == 'page':
+        # if wiki is not running at server root, prefix href with wiki root
+        wiki_root = request.url_root[len(request.host_url):-1]
+        if wiki_root:
+            href = '/' + wiki_root + href
+    href = convert_getlink_to_showlink(href)
+    # data_href will create an attribute named data-href: any attribute beginning with "data-" passes html5 validation
+    elem.attrib[html.data_href] = href
+    classes = elem.attrib.get(html.class_, '').split()
+    classes.append('moin-transclusion')
+    elem.attrib[html.class_] = ' '.join(classes)
+    return elem
 
 
 class ElementException(RuntimeError):
@@ -376,7 +378,7 @@
                 attrib[html.controls] = 'controls'
             new_elem = self.new_copy(getattr(html, obj_type), elem, attrib)
 
-        return wrap_object_with_overlay(new_elem, href=href)
+        return mark_item_as_transclusion(new_elem, href)
 
     def visit_moinpage_p(self, elem):
         return self.new_copy(html.p, elem)
@@ -384,7 +386,11 @@
     def visit_moinpage_page(self, elem):
         for item in elem:
             if item.tag.uri == moin_page and item.tag.name == 'body':
-                return self.new_copy(html.div, item)
+                # if this is a transcluded page, we must pass the class and data-href attribs
+                attribs = elem.attrib.copy()
+                if moin_page.page_href in attribs:
+                    del attribs[moin_page.page_href]
+                return self.new_copy(html.div, item, attribs)
 
         raise RuntimeError('page:page need to contain exactly one page:body tag, got {0!r}'.format(elem[:]))
 
--- a/MoinMoin/converter/include.py	Sat Mar 10 20:16:29 2012 +0100
+++ b/MoinMoin/converter/include.py	Sun Mar 18 12:55:06 2012 -0700
@@ -6,6 +6,9 @@
 MoinMoin - Include handling
 
 Expands include elements in an internal Moin document.
+
+Although this module is named include.py, many comments within and the moin docs
+use the word transclude as defined by http://www.linfo.org/transclusion.html, etc.
 """
 
 
@@ -29,7 +32,7 @@
 from MoinMoin.util.iri import Iri, IriPath
 from MoinMoin.util.tree import html, moin_page, xinclude, xlink
 
-from MoinMoin.converter.html_out import wrap_object_with_overlay
+from MoinMoin.converter.html_out import mark_item_as_transclusion, Attributes
 
 
 class XPointer(list):
@@ -113,7 +116,9 @@
             return cls()
 
     def recurse(self, elem, page_href):
-        # Check if we reached a new page
+        # on first call, elem.tag.name=='page'. Decendants (body, div, p, include, page, etc.) are processed by recursing through DOM
+
+        # stack is used to detect transclusion loops
         page_href_new = elem.get(self.tag_page_href)
         if page_href_new:
             page_href_new = Iri(page_href_new)
@@ -127,6 +132,8 @@
 
         try:
             if elem.tag == self.tag_xi_include:
+                # we have already recursed several levels and found a transclusion: "{{SomePage}}" or similar
+                # process the transclusion and add it to the DOM.  Subsequent recursions will traverse through the transclusion's elements.
                 href = elem.get(self.tag_xi_href)
                 xpointer = elem.get(self.tag_xi_xpointer)
 
@@ -173,7 +180,7 @@
                                 xp_include_level = data
 
                 if href:
-                    # We have a single page to include
+                    # We have a single page to transclude
                     href = Iri(href)
                     link = Iri(scheme='wiki', authority='')
                     if href.scheme == 'wiki':
@@ -245,12 +252,13 @@
 
                     page_doc = page.internal_representation()
                     # page_doc.tag = self.tag_div # XXX why did we have this?
+
                     self.recurse(page_doc, page_href)
-                    # Wrap the page with the overlay, but only if it's a "page", or "a".
+
+                    # if this is an existing item, mark it as a transclusion.  non-existent items are not marked (page_doc.tag.name == u'a')
                     # The href needs to be an absolute URI, without the prefix "wiki://"
-                    if page_doc.tag.endswith("page") or page_doc.tag.endswith("a"):
-                        page_doc = wrap_object_with_overlay(page_doc, href=unicode(p_href.path))
-
+                    if page_doc.tag.name == u'page':
+                        page_doc = mark_item_as_transclusion(page_doc, p_href.path)
                     included_elements.append(page_doc)
 
                 if len(included_elements) > 1:
@@ -261,45 +269,73 @@
                     result = included_elements[0]
                 else:
                     result = None
-
+                #  end of processing for transclusion; the "result" will get inserted into the DOM below
                 return result
 
-            container = [elem]
 
+            # Traverse the DOM by calling self.recurse with each child of the current elem.  Starting elem.tag.name=='page'.
+            container = []
             i = 0
             while i < len(elem):
                 child = elem[i]
                 if isinstance(child, ET.Node):
+                    # almost everything in the DOM will be an ET.Node, exceptions are unicode nodes under p nodes
+
                     ret = self.recurse(child, page_href)
+
                     if ret:
-                        if type(ret) == types.ListType:
+                        # "Normally" we are here because child.tag.name==include and ret is a transcluded item (ret.tag.name=page, image, or object, etc.)
+                        # that must be inserted into the DOM replacing elem[i].
+                        # This is complicated by the DOM having many inclusions, such as "\n{{SomePage}}\n" that are a child of a "p".
+                        # To prevent generation of invalid HTML5 (e.g. "<p>text<p>text</p></p>"), the DOM must be adjusted.
+                        if isinstance(ret, types.ListType):
+                            # the transclusion may be a return of the container variable from below, add to DOM replacing the current node
                             elem[i:i+1] = ret
                         elif elem.tag.name == 'p':
-                            try:
-                                body = ret[0][0]
-                                if len(body) == 1 and body[0].tag.name == 'p':
-                                    single = True
-                                else:
-                                    single = False
-                            except AttributeError:
-                                single = False
-
-                            if single:
-                                # content inside P is inserted directly into this P
-                                p = ret[0][0][0]
-                                elem[i:i+1] = [p[k] for k in xrange(len(p))]
+                            # ancestor P nodes with tranclusions  have special case issues, we may need to mangle the ret
+                            body = ret[0]
+                            # check for instance where ret is a page, ret[0] a body, ret[0][0] a P
+                            if not isinstance(body, unicode) and ret.tag.name == 'page' and body.tag.name == 'body' and \
+                                len(body) == 1 and body[0].tag.name == 'p':
+                                # special case:  "some text {{SomePage}} more text" or "\n{{SomePage}}\n" where SomePage contains a single p.
+                                # the content of the transcluded P will be inserted directly into ancestor P.
+                                p = body[0]
+                                # get attributes from page node; we expect {class: "moin-transclusion"; data-href: "http://some.org/somepage"}
+                                attrib = Attributes(ret).convert()
+                                # make new span node and "convert" p to span by copying all of p's children
+                                span = ET.Element(html.span, attrib=attrib, children=p[:])
+                                # insert the new span into the DOM replacing old include, page, body, and p elements
+                                elem[i] = span
+                            elif not isinstance(body, unicode) and ret.tag.name == 'page' and body.tag.name == 'body':
+                                # special case: "some text {{SomePage}} more text" or "\n{{SomePage}}\n" and SomePage body contains multiple p's, a table, preformatted text, etc.
+                                # note: ancestor P may have text before or after include
+                                if i > 0:
+                                    # there is text before transclude, make new p node to hold text before include and save in container
+                                    pa = ET.Element(html.p)
+                                    pa[:] = elem[0:i]
+                                    container.append(pa)
+                                # get attributes from page node; we expect {class: "moin-transclusion"; data-href: "http://some.org/somepage"}
+                                attrib = Attributes(ret).convert()
+                                # make new div node, copy all of body's children, and save in container
+                                div = ET.Element(html.div, attrib=attrib, children=body[:])
+                                container.append(div)
+                                 # empty elem of siblings that were just placed in container
+                                elem[0:i+1] = []
+                                if len(elem) > 0:
+                                    # there is text after transclude, make new p node to hold text, copy siblings, save in container
+                                    pa = ET.Element(html.p)
+                                    pa[:] = elem[:]
+                                    container.append(pa)
+                                    elem[:] = []
+                                # elem is now empty so while loop will terminate and container will be returned up one level in recursion
                             else:
-                                # P is closed and element is inserted after
-                                pa = ET.Element(html.p)
-                                pa[0:i] = elem[0:i]
-                                ret[0:1] = elem[i:i+1]
-                                elem[0:i+1] = []
-                                container[0:0] = [pa, ret]
-                                i = 0
+                                # ret may be a unicode string: take default action
+                                elem[i] = ret
                         else:
+                            # default action for any ret not fitting special cases above
                             elem[i] = ret
                 i += 1
-            if len(container) > 1:
+            if len(container) > 0:
                 return container
 
         finally:
@@ -307,7 +343,6 @@
 
     def __call__(self, tree):
         self.stack = []
-
         self.recurse(tree, None)
 
         return tree
--- a/MoinMoin/static/js/common.js	Sat Mar 10 20:16:29 2012 +0100
+++ b/MoinMoin/static/js/common.js	Sun Mar 18 12:55:06 2012 -0700
@@ -786,27 +786,71 @@
 }
 jQuery(moinFirefoxWordBreak);
 
-/* For the overlays on transcluded objects */
-function removeURLPrefixes(url) {
-    return url.replace("+get/", "").replace("+modify/", "")
+
+// globals used to save translated show/hide titles (tooltips) for Transclusions buttons
+transclusionShowTitle = ''; // "Show Transclusions"
+transclusionHideTitle = ''; // "Hide Transclusions"
+
+// This is executed when user clicks a Transclusions button
+function toggleTransclusionOverlays() {
+    var overlays = jQuery('.moin-item-overlay-ul, .moin-item-overlay-lr');
+    if (overlays.length > 0) {
+        var buttons = jQuery('.moin-transclusions-button > a');
+        if (overlays.is(':visible')) {
+            overlays.hide();
+            buttons.attr('title', transclusionShowTitle);
+        } else {
+            overlays.show();
+            buttons.attr('title', transclusionHideTitle);
 }
-function attachHoverToObjects() {
-    $(".page-object").mouseenter(function(e) {
-        elements = $(".object-overlay", this)
-        elements.each(function(i) {
-            if (location.href == removeURLPrefixes(this.firstChild.href)) {
-                var elem = $(this)
-                setTimeout(function() {
-                    elem.hide()
-                }, 10)
             }
-        })
-
-        $(elements.slice(1)).hide()
-    })
 }
 
-$(document).ready(attachHoverToObjects)
+// Transclusion initialization is executed once after document ready
+function initTransclusionOverlays() {
+    var elem, overlayUL, overlayLR, wrapper, wrappers;
+    var rightArrow = '\u2192';
+    // get list of elements to be wrapped;  must work in reverse order in case there are nested transclusions
+    var transclusions = jQuery(jQuery('.moin-transclusion').get().reverse());
+    transclusions.each(function (index) {
+        elem = transclusions[index];
+        // if this is the transcluded item page, do not wrap (avoid creating useless overlay links to same page)
+        if (location.href !== elem.getAttribute('data-href')) {
+            if (elem.tagName === 'DIV') {
+                wrapper = jQuery('<div class="moin-item-wrapper"></div>');
+            } else {
+                wrapper = jQuery('<span class="moin-item-wrapper"></span>');
+            }
+            overlayUL = jQuery('<a class="moin-item-overlay-ul"></a>');
+            jQuery(overlayUL).attr('href', elem.getAttribute('data-href'));
+            jQuery(overlayUL).append(rightArrow);
+            overlayLR = jQuery(overlayUL).clone(true);
+            jQuery(overlayLR).attr('class', 'moin-item-overlay-lr');
+            // if the parent of this element is an A, then wrap parent (avoid A's within A's)
+            if (jQuery(elem).parent()[0].tagName === 'A') {
+                elem = jQuery(elem).parent()[0];
+            }
+            // wrap element, add UL and LR overlay siblings, and replace old elem with wrapped elem
+            jQuery(wrapper).append(jQuery(elem).clone(true));
+            jQuery(wrapper).append(overlayUL);
+            jQuery(wrapper).append(overlayLR);
+            jQuery(elem).replaceWith(wrapper);
+        }
+    });
+    // if an element was wrapped above, then make the Transclusions buttons visible
+    wrappers = jQuery('.moin-item-wrapper');
+    if (wrappers.length > 0) {
+        jQuery('.moin-transclusions-button').css('display', '');
+        // read translated show|hide Transclusions button title, split into show and hide parts, and save
+        var titles = jQuery('.moin-transclusions-button > a').attr('title').split('|');
+        if (titles.length === 2) {
+            transclusionShowTitle = titles[0];
+            transclusionHideTitle = titles[1];
+            jQuery('.moin-transclusions-button > a').attr('title', transclusionShowTitle);
+        }
+    }
+}
+jQuery(document).ready(initTransclusionOverlays);
 
 /*
     For the quicklinks patch that
--- a/MoinMoin/templates/itemviews.html	Sat Mar 10 20:16:29 2012 +0100
+++ b/MoinMoin/templates/itemviews.html	Sun Mar 18 12:55:06 2012 -0700
@@ -66,6 +66,12 @@
                 </li>
             {%- endif %}
 
+        {% if endpoint == 'special.transclusions' -%}
+            <li class="moin-transclusions-button" style="display:none;">
+            <a href="#" onClick="toggleTransclusionOverlays();return false;" title="{{ title }}">{{ label }}</a>
+            </li>
+        {%- endif %}
+
             {%- if endpoint == 'special.supplementation' %}
                 {%- for sub_item_name in cfg.supplementation_item_names %}
                     {%- set current_sub = item_name.rsplit('/', 1)[-1] %}
--- a/MoinMoin/themes/modernized/static/css/common.css	Sat Mar 10 20:16:29 2012 +0100
+++ b/MoinMoin/themes/modernized/static/css/common.css	Sun Mar 18 12:55:06 2012 -0700
@@ -646,16 +646,25 @@
 #moin-creditlogos { float: right; list-style: none; margin: 0 10px; }
 #moin-creditlogos li { display: inline-block; margin: 10px 0 10px 10px; }
 
-/* transclusion overlay */
-.page-object { margin: 0px; width: auto; height: auto; display: block; position: relative; }
-.object-overlay { background-color: black; position: absolute; padding: 5px; border-radius: 1em;
-            text-align: center; white-space: nowrap; font-size: 120%;
-            font-weight: bold; display: none; margin: 0; opacity: .5; }
-.object-overlay-wrapper { width: 20px; height: 20px; position: absolute; z-index: 100; top: 0px; left: 0px; margin: 0; }
-.object-overlay-wrapper:hover  .object-overlay { display: block; top: -20px; left: 0px; }
-.page-object:hover .object-overlay:hover { opacity: .8; filter: alpha(opacity=90); }
-.object-overlay a:link,
-.object-overlay a:visited { color: white; }
+/* Transcluded items are wrapped in a div or span and have two overlay siblings that link to the item page. */
+/* When a Transclusions button is clicked, a Javascript function will show/hide the corners of the overlay siblings. */
+.moin-item-wrapper { position: relative; display: inline-block; }
+.moin-item-wrapper > a:hover { color: blue; text-decoration: none; }
+a.moin-item-overlay-ul,
+a.moin-item-overlay-lr { display: none; position: absolute; color: transparent; background-color: transparent;
+            font-size: 120%; font-weight: bold; margin: 0; opacity: .5; filter: alpha(opacity=50);
+            padding: 1px; border-color: blue; border-style: double; }
+.moin-item-overlay-ul { top: -4px; left: -4px; border-width: 3px 0 0 3px; }
+.moin-item-overlay-lr { bottom: -4px; right: -4px;  border-width: 0 3px 3px 0; }
+/* On overlay mouseover (if Transclusions toggle state is "show"), the arrow and background are revealed. */
+.moin-item-overlay-ul:hover,
+.moin-item-overlay-lr:hover { opacity: .8; filter: alpha(opacity=80); background-color: #C4D9FF; color: blue; }
+/* Prevent double spacing in nested transclusions that consist of paragraphs of text */
+ div.moin-item-wrapper,
+ div.moin-item-wrapper > div,
+ div.moin-item-wrapper > div > p:first-child,
+ div.moin-item-wrapper > div > p:last-child { margin: 0px; }
+ div.moin-item-wrapper > div >  p:first-child ~ p:last-child { margin-top: 1em; }
 
 /* special style for heading with mouseover permalinks */
 .permalink { display: none; cursor: pointer; font-size: 80%; margin-left: 3px; }