Daizu::Plugin::HeaderAnchor - add anchors to headings in all articles


Daizu documentation Contained in the Daizu distribution.

Index


Code Index:

NAME

Top

Daizu::Plugin::HeaderAnchor - add anchors to headings in all articles

DESCRIPTION

Top

This plugin filters articles and adds anchors (a elements with an id attribute but no href attribute) to all the headings (from h1 to h6). This allows people to link to a specific section of your web page.

The names used for the anchors are based on the textual content of the headings. All the names have a sec- prefix added.

Care is taken to ensure that anchor names aren't duplicated, although if the templates add any which start with the same prefix then it would be possible to get a duplicate value. Anchors are not added to headings which already contain an a element with either a name or id attribute.

CONFIGURATION

Top

To turn on this plugin, include the following in your Daizu CMS configuration file:

    <plugin class="Daizu::Plugin::HeaderAnchor" />

METHODS

Top

Daizu::Plugin::HeaderAnchor->register($cms, $whole_config, $plugin_config, $path)

Registers the plugin as a filter for all articles at or in $path.

$self->filter_article($cms, $file, $doc)

Does the actual filtering in-place on $doc and returns it. Currently $cms and $file are ignored.

COPYRIGHT

Top


Daizu documentation Contained in the Daizu distribution.
package Daizu::Plugin::HeaderAnchor;
use warnings;
use strict;

use XML::LibXML;

sub register
{
    my ($class, $cms, $whole_config, $plugin_config, $path) = @_;
    my $self = bless {}, $class;
    $cms->add_html_dom_filter($path, $self => 'filter_article');
}

sub filter_article
{
    my (undef, undef, undef, $doc) = @_;
    my %name_used;

    # Find any anchors already used in the article, in case the user
    # wants to customize one, or put move an anchor to a specific place.
    # In that case we need to avoid adding an anchor with the same name.
    # We're only interested in ones starting with 'sec-' because that's
    # all we generate.  Treat the IDs case insensitvely just to be on
    # the safe side.
    for ($doc->findnodes(qq{
                //@*[name() = 'id' or name() = 'name' or name() = 'xml:id']
        }))
    {
        my $value = $_->getValue;
        $name_used{lc $value} = undef
            if $value =~ /^sec-/i;
    }

    # Search for heading elements and add the anchors.
    for my $elem ($doc->findnodes(qq{
                //*[namespace-uri() = 'http://www.w3.org/1999/xhtml' and
                        substring(local-name(), 1, 1) = 'h']
        }))
    {
        # Only process heading elements: h1, h2, h3, h4, h5, and h6.
        next unless $elem->localname =~ /^h[123456]$/;

        # If the heading already has an anchor, ignore it.
        next if $elem->findnodes(q{
                        *[namespace-uri() = 'http://www.w3.org/1999/xhtml' and
                            local-name() = 'a' and
                            (@name or @id)]
                });
        next if $elem->hasAttribute('id');

        my $text = lc $elem->textContent;
        for ($text) {
            s/\.+/./g;
            s/[^-.a-zA-Z0-9]+/ /g;
            s/^[-. ]+//;
            s/[-. ]+$//;
        }
        my @words = ('sec', split ' ', $text);
        @words = map { $_ eq '' ? () : ($_) } @words;

        # Shorten it to at most three words.
        my $max_words = 3;      # doesn't include 'sec-' prefix.
        $#words = $max_words
            if @words > ($max_words + 1);
        $#words = $max_words - 1
            if @words == ($max_words + 1) &&
               $words[$max_words] =~/^(?:a|the|and|or|of|in|at|to)$/;

        push @words, 'unnamed' if @words == 1;
        my $anchor_name = join '-', @words;

        # Make sure it's unique (within the content we can see) by
        # appending a number if necessary.
        if (exists $name_used{$anchor_name}) {
            my $n = 2;
            while (exists $name_used{"$anchor_name-$n"}) {
                ++$n;
            }
            $anchor_name = "$anchor_name-$n";
        }
        $name_used{$anchor_name} = undef;

        # Add a new empty anchor element at the start of the heading.
        my $anchor = XML::LibXML::Element->new('a');
        $anchor->setAttribute(id => $anchor_name);
        $elem->insertBefore($anchor, $elem->firstChild);
    }

    return { content => $doc };
}

1;
# vi:ts=4 sw=4 expandtab