1
votes

How would I prevent empty elements in XSL from nesting into each other?

I have a portion of my xsl that looks like this

     <div class="social-share">
        <div class="twitter-icon" data-url="{link}" data-text="{title}"></div>
        <div class="facebook-icon" data-url="{link}" data-text="{title}"></div>
        <div class="linkedin-icon" data-url="{link}" data-text="{title}"></div>
     </div>

The javascript at the beginning of the file pulls in APIs related to these social media links, so they are intentionally empty. However, leaving these empty will result in the following structure which I do not want.

     <div class="social-share">
        <div class="twitter-icon" data-url="{link}" data-text="{title}">
        <div class="facebook-icon" data-url="{link}" data-text="{title}">
        <div class="linkedin-icon" data-url="{link}" data-text="{title}">
        </div>
        </div>
        </div>
     </div>

Full XSL

<xsl:stylesheet 
  version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 >
 <xsl:output method="html" indent="yes" 
 doctype-system='http://www.w3.org/TR/html4/strict.dtd'
 doctype-public='-//W3C//DTD HTML 4.01//EN' 
  />
 <xsl:template match="/rss">
 <SCRIPT LANGUAGE="javascript" DEFER="true">
 <xsl:comment>
  <![CDATA[ 
jQuery(document).ready(function () {
(function (factory) {
if (typeof define === 'function' && define.amd) {
    define(['jquery'], factory);
} else {
    factory(jQuery);
}
}(function ($) {
$.timeago = function (timestamp) {
    if (timestamp instanceof Date) {
        return inWords(timestamp);
    } else if (typeof timestamp === "string") {
        return inWords($.timeago.parse(timestamp));
    } else if (typeof timestamp === "number") {
        return inWords(new Date(timestamp));
    } else {
        return inWords($.timeago.datetime(timestamp));
    }
};
var $t = $.timeago;

$.extend($.timeago, {
    settings: {
        refreshMillis: 60000,
        allowPast: true,
        allowFuture: false,
        localeTitle: false,
        cutoff: 0,
        strings: {
            prefixAgo: null,
            prefixFromNow: null,
            suffixAgo: "ago",
            suffixFromNow: "from now",
            inPast: 'any moment now',
            seconds: "a few seconds",
            minute: "about a minute",
            minutes: "%d minutes",
            hour: "about an hour",
            hours: "about %d hours",
            day: "a day",
            days: "%d days",
            month: "about a month",
            months: "%d months",
            year: "about a year",
            years: "%d years",
            wordSeparator: " ",
            numbers: []
        }
    },

    inWords: function (distanceMillis) {
        if (!this.settings.allowPast && !this.settings.allowFuture) {
            throw 'timeago allowPast and allowFuture settings can not both be set to false.';
        }

        var $l = this.settings.strings;
        var prefix = $l.prefixAgo;
        var suffix = $l.suffixAgo;
        if (this.settings.allowFuture) {
            if (distanceMillis < 0) {
                prefix = $l.prefixFromNow;
                suffix = $l.suffixFromNow;
            }
        }

        if (!this.settings.allowPast && distanceMillis >= 0) {
            return this.settings.strings.inPast;
        }

        var seconds = Math.abs(distanceMillis) / 1000;
        var minutes = seconds / 60;
        var hours = minutes / 60;
        var days = hours / 24;
        var years = days / 365;

        function substitute(stringOrFunction, number) {
            var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
            var value = ($l.numbers && $l.numbers[number]) || number;
            return string.replace(/%d/i, value);
        }

        var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || seconds < 90 && substitute($l.minute, 1) || minutes < 45 && substitute($l.minutes, Math.round(minutes)) || minutes < 90 && substitute($l.hour, 1) || hours < 24 && substitute($l.hours, Math.round(hours)) || hours < 42 && substitute($l.day, 1) || days < 30 && substitute($l.days, Math.round(days)) || days < 45 && substitute($l.month, 1) || days < 365 && substitute($l.months, Math.round(days / 30)) || years < 1.5 && substitute($l.year, 1) || substitute($l.years, Math.round(years));

        var separator = $l.wordSeparator || "";
        if ($l.wordSeparator === undefined) {
            separator = " ";
        }
        return $.trim([prefix, words, suffix].join(separator));
    },

    parse: function (iso8601) {
        var s = $.trim(iso8601);
        s = s.replace(/\.\d+/, "");
        s = s.replace(/-/, "/").replace(/-/, "/");
        s = s.replace(/T/, " ").replace(/Z/, " UTC");
        s = s.replace(/([\+\-]\d\d)\:?(\d\d)/, " $1$2");
        s = s.replace(/([\+\-]\d\d)$/, " $100");
        return new Date(s);
    },
    datetime: function (elem) {
        var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
        return $t.parse(iso8601);
    },
    isTime: function (elem) {
        return $(elem).get(0).tagName.toLowerCase() === "time";
    }
});
var functions = {
    init: function () {
        var refresh_el = $.proxy(refresh, this);
        refresh_el();
        var $s = $t.settings;
        if ($s.refreshMillis > 0) {
            this._timeagoInterval = setInterval(refresh_el, $s.refreshMillis);
        }
    },
    update: function (time) {
        var parsedTime = $t.parse(time);
        $(this).data('timeago', {
            datetime: parsedTime
        });
        if ($t.settings.localeTitle) $(this).attr("title", parsedTime.toLocaleString());
        refresh.apply(this);
    },
    updateFromDOM: function () {
        $(this).data('timeago', {
            datetime: $t.parse($t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title"))
        });
        refresh.apply(this);
    },
    dispose: function () {
        if (this._timeagoInterval) {
            window.clearInterval(this._timeagoInterval);
            this._timeagoInterval = null;
        }
    }
};

$.fn.timeago = function (action, options) {
    var fn = action ? functions[action] : functions.init;
    if (!fn) {
        throw new Error("Unknown function name '" + action + "' for timeago");
    }
    this.each(function () {
        fn.call(this, options);
    });
    return this;
};

function refresh() {
    var data = prepareData(this);
    var $s = $t.settings;

    if (!isNaN(data.datetime)) {
        if ($s.cutoff == 0 || Math.abs(distance(data.datetime)) < $s.cutoff) {
            $(this).text(inWords(data.datetime));
        }
    }
    return this;
}

function prepareData(element) {
    element = $(element);
    if (!element.data("timeago")) {
        element.data("timeago", {
            datetime: $t.datetime(element)
        });
        var text = $.trim(element.text());
        if ($t.settings.localeTitle) {
            element.attr("title", element.data('timeago').datetime.toLocaleString());
        } else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
            element.attr("title", text);
        }
    }
    return element.data("timeago");
}

function inWords(date) {
    return $t.inWords(distance(date));
}

function distance(date) {
    return (new Date().getTime() - date.getTime());
}
document.createElement("abbr");
document.createElement("time");
}));

$('.headline-details').click(function () {
$(this).closest('.headline-item-small').find('.headline-hover-small').fadeToggle(300);
});

$('.close-headline').click(function () {
$(this).closest('.headline-item-small').find('.headline-hover-small').fadeToggle(300);
});

$('.twitter-icon').sharrre({
share: {
twitter: true
},
template: '<a class="box" href="#"><div class="share"><i class="fa fa-twitter"></i></div>    <div class="count" href="#">{total}</div></a>',
enableHover: false,
enableTracking: true,
buttons: { twitter: {via: '_JulienH'}},
click: function(api, options){
api.simulateClick();
api.openPopup('twitter');
}
});
$('.facebook-icon').sharrre({
share: {
facebook: true
},
template: '<a class="box" href="#"><div class="share"><i class="fa fa-facebook"></i>   </div><div class="count" href="#">{total}</div></a>',
enableHover: false,
enableTracking: true,
click: function(api, options){
api.simulateClick();
api.openPopup('facebook');
}
});
$('.linkedin-icon').sharrre({
share: {
pinterest: true
},
template: '<a class="box" href="#"><div class="share"><i class="fa fa-linkedin"></i> </div><div class="count" href="#">{total}</div></a>',
enableHover: false,
enableTracking: true,
click: function(api, options){
api.simulateClick();
api.openPopup('linkedin');
}
});

jQuery("abbr.timeago").timeago(); 
});
  ]]> 
</xsl:comment>
</SCRIPT>
<div style="display:inline-block;">
    <xsl:apply-templates select="channel" />
</div>
</xsl:template>

<xsl:template match="channel">
<div style="display:inline-block;">
  <xsl:apply-templates select="item" />
</div>
</xsl:template>

<xsl:template match="item">
<!-- ... -->
 <div class="headline-container">
 <div class="headline-item-small">
    <xsl:attribute name="style">background-image:url('<xsl:value-of select="enclosure/@url" />');</xsl:attribute>
    <div>
     <abbr class="timeago">
     <xsl:attribute name="title"><xsl:value-of select="pubDate" disable-output-escaping="yes" /></xsl:attribute>
        <xsl:comment><![CDATA[[if IE]><![if lte IE 8]><i class="fa fa-clock-o"></i><![endif]]]></xsl:comment>
     </abbr>
     <div class="headline-details">.<i class="fa fa-share">.</i></div>
  </div>
  <div class="headline-hover-small">
     <i class="close-headline fa fa-times">.</i>
     <p class="headline-p ellipsis multiline"><b>Teaser: </b><xsl:value-of select="description" disable-output-escaping="yes" /></p>
     <div class="social-share">
        <div class="twitter-icon" data-url="{link}" data-text="{title}">.</div>
        <div class="facebook-icon" data-url="{link}" data-text="{title}">.</div>
        <div class="linkedin-icon" data-url="{link}" data-text="{title}">.</div>
     </div>
   </div>
   </div>
  <a target="_self">
  <xsl:attribute name="href"><xsl:value-of select="link" disable-output-escaping="yes" /></xsl:attribute>
   <h3 class="headline-header"><xsl:value-of select="title" disable-output-escaping="yes" /></h3>
   </a>
   <xsl:comment><![CDATA[[if IE]><hr/><p style="font-size: 0.75em;margin: 0px;padding: 10px;text-align: justify;"><xsl:value-of select="description" disable-output-escaping="yes" /></p><![endif]]]></xsl:comment>
   </div>
   <!-- ... -->
   </xsl:template>
 </xsl:stylesheet>
3

3 Answers

1
votes

When xsl:output/@method is html, empty elements are output without closing tags, which appears to be causing you problems.

Try changing your xsl:output statement from this:

 <xsl:output method="html"   ...   />

to this:

 <xsl:output method="xml"   ...   />

This will produce properly closed end tags.

Note, however, that other changes will also occur:

  • Within script and style elements, replacement of &amp; and &lt; will no longer occur automatically.
  • Within attributes, &gt; will no longer be replaced with >.
  • Minimized forms such as checked and selected will no longer be used for attributes.

You may or may not have to make corresponding compensations in any these areas, depending on your code.

0
votes

Assuming you mean that you want to prevent any empty element to be output to the final result tree, you can simply add the following to your stylesheet (works in XSLT 1.0, 2.0 and 3.0):

<xsl:template match="*[not(node())]" />

Of course, this also assumes that somewhere else you have an xsl:apply-templates that selects those empty elements.

If all you want is to process your input XML above and remove the empty <div /> elements, you can use a modified copy idiom:

<xsl:template match="node() | @*">
    <xsl:apply-templates select="@* | node()"/>
</xsl:template>

<xsl:template match="div[not(node())]" />
0
votes

I ended up solving this by using the following

<xsl:variable name="empty" select="''"/>

<div class="social-share">
    <div class="twitter-icon" data-url="{link}" data-text="{title}"><xsl:value-of select="$empty"/></div>
    <div class="facebook-icon" data-url="{link}" data-text="{title}"><xsl:value-of select="$empty"/></div>
    <div class="linkedin-icon" data-url="{link}" data-text="{title}"><xsl:value-of select="$empty"/></div>
</div>