Search

Hi everybody,

is it possible to catch the output generated by a text-formatter and manipulate it before the whole layout is rendered?

The reason is the following: recently, I decided to build my own website in HTML5. A quick look at some past threads has shown me somebody already managed to do that. In the meantime some nice XSLT utilities have been published too.

What I’m trying to do is to wrap every piece of content inside a section element. As you know, this element is used in conjunction with h[1-6] to describe a hierarchy of contents. While it’s easy to manually edit a static HTML page, things might get complicated on complex CMSes.

The function I wrote is pretty simple: it is given a string containing some input HTML and it returns the “HTML5ified” content, where each portion is wrapped inside a section element.

While it works, I don’t really know how to achieve integration with Symphony. Apparently, there aren’t any delegates that can pass me the output of a text-formatter before rendering everything (except FrontendOutputPostGenerate).

Is there any other way to accomplish what I need, or is it possible to request such a delegate?

Thank you!

You might do this in XSLT, which is the designated template layer in Symphony. Is there any special reason why you want to use PHP?

Actually I tried to do that with XSLT, but without success. In order to properly generate <section> tags, you should select every hn element followed by any element which is not hm (with 1 <= n <= m). I fiddled around with following-siblings and other XPath axes but eventually I just got a headache! :)

I don’t like using PHP for that kind of stuff, but that is the only solution I’ve found so far.

I don’t think you should wrap every set of hn elements with section: you may find article is more appropriate, or even neither. Whichever you choose, a text formatter shouldn’t add structural elements, it should just format the text itself. Structural elements should be created in your templating layer (XSLT).

Who don’t you simply output like:

<section>
    <xsl:copy-of select="field/*"/>
</section>

Are you saying you need to add additional structure to a block of HTML output from a Markdown-enabled Textarea field?

That’s the way I understand it. I once did this with h2s to introduce div elements with class names.

If you need it for every hedline (do you really???), you could use s.th. like the following:

<xsl:template match="body" mode="sections">
    <xsl:call-template name="intro" mode="section"/>
    <xsl:apply-templates select="h2|h3|h4|h5|h6" mode="section"/>
</xsl:template>

<xsl:template name="intro" mode="section">
    <xsl:variable name="elements-number-total" select="count(*[not(self::h2 or self::h3 or self::h4 or self::h5 or self::h6)])"/>
    <xsl:variable name="elements-number-after-next-h" select="count(*[self::h2 or self::h3 or self::h4 or self::h5 or self::h6]/following-sibling::*[not(self::h2 or self::h3 or self::h4 or self::h5 or self::h6)])"/>
    <xsl:variable name="elements-number" select="$elements-number-total - $elements-number-after-next-h"/>
    <xsl:if test="$elements-number &gt; 0">
        <div class="intro">
            <xsl:apply-templates select="*[position() &lt;= $elements-number]" mode="html"/>
        </div>
    </xsl:if>
</xsl:template>

<xsl:template match="h2|h3|h4|h5|h6" mode="section">
    <xsl:variable name="elements-number-total" select="count(following-sibling::*[not(self::h2 or self::h3 or self::h4 or self::h5 or self::h6)])"/>
    <xsl:variable name="elements-number-after-next-h" select="count(following-sibling::*[self::h2 or self::h3 or self::h4 or self::h5 or self::h6]/following-sibling::*[not(self::h2 or self::h3 or self::h4 or self::h5 or self::h6)])"/>
    <xsl:variable name="elements-number" select="$elements-number-total - $elements-number-after-next-h"/>
    <div class="section" id="section-{position()}">
        <xsl:apply-templates select="." mode="html"/>
        <xsl:apply-templates select="following-sibling::*[position() &lt;= $elements-number]" mode="html"/>
    </div>
</xsl:template>

You should have the Ninja technique in place…

This solution (simply counting those HTML elements to deal with) is probably not the most elegant. But it should work. Simply use it like so:

<xsl:apply-templates select="body" mode="sections"/>

Please note that it is important to always count elements only which are not headlines. (Otherwise you will run into problems.) The “intro” template is needed because your HTML might start with an intro text before the first headline appears.

EDIT: Improved XSLT code.

EDIT2: Removed debug output in XSLT code.

And here’s a more elegant solution, using generate-id() to select the right elements:

<xsl:template match="body" mode="sections">
    <xsl:call-template name="intro" mode="section"/>
    <xsl:apply-templates select="h2|h3|h4|h5|h6" mode="section"/>
</xsl:template>

<xsl:template name="intro" mode="section">
    <xsl:variable name="next-h" select="generate-id(h2|h3|h4|h5|h6)"/>
    <xsl:if test="*[not(generate-id(.) = $next-h or preceding-sibling::*[generate-id(.) = $next-h])]">
        <div class="intro">
            <xsl:apply-templates select="*[not(generate-id(.) = $next-h or preceding-sibling::*[generate-id(.) = $next-h])]" mode="html"/>
        </div>      
    </xsl:if>
</xsl:template>

<xsl:template match="h2|h3|h4|h5|h6" mode="section">
    <xsl:variable name="next-h" select="generate-id(following-sibling::h2|following-sibling::h3|following-sibling::h4|following-sibling::h5|following-sibling::h6)"/>
    <div class="section" id="section-{position()}">
        <xsl:apply-templates select=". | following-sibling::*[not(generate-id(.) = $next-h or preceding-sibling::*[generate-id(.) = $next-h])]" mode="html"/>
    </div>
</xsl:template>

[EDIT]: Further simplification.

@nickdunn: Actually article and section, as far as I’ve understood, have different scopes. Have a look at this example: while you can use article to identify an entry, the same entry may be splitted into many sections.

Are you saying you need to add additional structure to a block of HTML output from a Markdown-enabled Textarea field?

Yes, but unfortunately I didn’t manage to do that with XSLT… that’s why I switched to PHP.

@michael-e: Usually I don’t go any deeper than h3s for each of my entries. To be honest, I think that the section element is one of very few useful additions in HTML5, as it improves the semantics of one’s webpage. ;)

Thank you for your time, I will try your code as soon as I can!

@michael-e: Your code solves half the problem. While it correctly identifies all the direct siblings of each heading, it doesn’t wrap everything which is between a level n heading and the next one of same type inside a section element, then making it recursive for each header which is of type n+1.

For example, it creates a structure like this:

<section>
    <h2>...</h2>
    [...]
</section>

<section>
    <h3>...</h3>
    [...]
</section>

<section>
    <h4>...</h4>
    [...]
</section>

but not like this:

<section>
    <h2>...</h2>
    [...]

    <section>
        <h3>...</h3>
        [...]

        <section>
            <h4>...</h4>
            [...]
        </section>

    </section>

</section>

Finally I managed to find a solution and here’s what I’ve come up to:

<xsl:template match="body" mode="section">
    <xsl:apply-templates select="h2" mode="section"/>
</xsl:template>

<xsl:template match="h4" mode="section">
    <xsl:variable name="next-h" select="generate-id(following-sibling::h4|following-sibling::h3|following-sibling::h2)"/>

    <section class="level_4">
        <xsl:apply-templates select=". | following-sibling::*[not(generate-id(.) = $next-h or preceding-sibling::*[generate-id(.) = $next-h])]" mode="html"/>
    </section>

</xsl:template>

<xsl:template match="h3" mode="section">
    <xsl:variable name="current-h" select="."/>
    <xsl:variable name="next-h" select="generate-id(following-sibling::h2|following-sibling::h3)"/>
    <xsl:variable name="nested-h" select="generate-id(following-sibling::h4)"/>

    <section class="level_3">
        <xsl:apply-templates select=". | following-sibling::*[not(generate-id(.) = $next-h or preceding-sibling::*[generate-id(.) = $next-h] or generate-id(.) = $nested-h or preceding-sibling::*[generate-id(.) = $nested-h])]" mode="html"/>
        <xsl:apply-templates select="following-sibling::h4[preceding-sibling::h3[1] = $current-h]" mode="section"/>
    </section>

</xsl:template>

<xsl:template match="h2" mode="section">
    <xsl:variable name="current-h" select="."/>
    <xsl:variable name="next-h" select="generate-id(following-sibling::h2)"/>
    <xsl:variable name="nested-h" select="generate-id(following-sibling::h3)"/>

    <section class="level_2">
        <xsl:apply-templates select=". | following-sibling::*[not(generate-id(.) = $next-h or preceding-sibling::*[generate-id(.) = $next-h] or generate-id(.) = $nested-h or preceding-sibling::*[generate-id(.) = $nested-h])]" mode="html"/>
        <xsl:apply-templates select="following-sibling::h4[preceding-sibling::h2[1] = $current-h]" mode="section"/>
    </section>

</xsl:template>

The assumptions are the following:

  1. The first occurring heading in an article is h2
  2. There are no jumps from a level n heading to a level n+2. Every heading is followed by one of type m+1 (with m <= n).
  3. Headings stop at level 4 (at least that was what I needed!)

I think there are ways to make the code more elegant and readable. In the meantime let me know what are your thoughts!

Your code looks good to me (apart from a typo in the third line from the end — shouldn’t it be following-sibling::h3?). My own code was just a guess (and an exercise to me), without knowing your desired outcome (because I never used HTML5 sections).

Assumption number 2 is somehow critical, however. AFAIK, not even any W3C specs set up this rule. Are you sure that your authors will understand the need for it (and stick to it)?

My own code was just a guess (and an exercise to me), without knowing your desired outcome (because I never used HTML5 sections).

Sorry michael, I didn’t mean to say that your code is not working or is not useful. Instead, it helped me a lot to understand how to write complex XSLT templates, so thanks again for your precious help!

Assumption number 2 is somehow critical, however. AFAIK, not even any W3C specs set up this rule. Are you sure that your authors will understand the need for it (and stick to it)?

There has never been a consensus about how to use headings. This document encourages authors to “use headings that are properly nested” and I’m on that side because, from an accessibility point of view, it’s easier to navigate a page if headers are not skipped at all.

P.S: Yes… that is a typo, d’oh!

and I’m on that side because, from an accessibility point of view, it’s easier to navigate a page if headers are not skipped at all.

I agree! But don’t authors tend to forget things like this? That’s the dangerous thing here, I think.

You’re right, it’s a delicate matter and also dangerous to assume that headings are used throughout the page in the correct way. I think that text formatters like Markdown and Textile ensure accessible, semantic markup in 99% of cases, but it’s not enough.

I’ll try improving the code in order to make it digest jumps as well. It shouldn’t be difficult. At least, I hope so. :)

Create an account or sign in to comment.

Symphony • Open Source XSLT CMS

Server Requirements

  • PHP 5.3-5.6 or 7.0-7.3
  • PHP's LibXML module, with the XSLT extension enabled (--with-xsl)
  • MySQL 5.5 or above
  • An Apache or Litespeed webserver
  • Apache's mod_rewrite module or equivalent

Compatible Hosts

Sign in

Login details