#!/usr/bin/perl # Ikiwiki field plugin. # See doc/plugin/contrib/field.mdwn for documentation. package IkiWiki::Plugin::field; use warnings; use strict; =head1 NAME IkiWiki::Plugin::field - front-end for per-page record fields. =head1 VERSION This describes version B<1.20101101> of IkiWiki::Plugin::field =cut our $VERSION = '1.20101115'; =head1 PREREQUISITES IkiWiki =head1 AUTHOR Kathryn Andersen (RUBYKAT) http://github.com/rubykat =head1 COPYRIGHT Copyright (c) 2009-2010 Kathryn Andersen This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut use IkiWiki 3.00; my %Fields = ( _first => { id => '_first', seq => 'BB', }, _last => { id => '_last', seq => 'YY', }, _middle => { id => '_middle', seq => 'MM', }, ); my @FieldsLookupOrder = (); my %Cache = (); sub field_get_value ($$); sub import { hook(type => "getsetup", id => "field", call => \&getsetup); hook(type => "checkconfig", id => "field", call => \&checkconfig); hook(type => "scan", id => "field", call => \&scan, last=>1); hook(type => "pagetemplate", id => "field", call => \&pagetemplate); } # =============================================== # Hooks # --------------------------- sub getsetup () { return plugin => { safe => 1, rebuild => undef, }, field_register => { type => "hash", example => "field_register => {meta => 'last'}", description => "simple registration of fields by plugin", safe => 0, rebuild => undef, }, field_allow_config => { type => "boolean", example => "field_allow_config => 1", description => "allow config settings to be queried", safe => 0, rebuild => undef, }, field_tags => { type => "hash", example => "field_tags => {BookAuthor => '/books/authors'}", description => "fields flagged as tag-fields", safe => 0, rebuild => undef, }, } sub checkconfig () { # use the simple by-plugin pagestatus method for # those plugins registered with the field_register config option. if (defined $config{field_register}) { if (ref $config{field_register} eq 'ARRAY') { foreach my $id (@{$config{field_register}}) { field_register(id=>$id); } } elsif (ref $config{field_register} eq 'HASH') { foreach my $id (keys %{$config{field_register}}) { field_register(id=>$id, order=>$config{field_register}->{$id}); } } else { field_register(id=>$config{field_register}); } } if (!defined $config{field_allow_config}) { $config{field_allow_config} = 0; } } # checkconfig sub scan (@) { my %params=@_; my $page=$params{page}; my $content=$params{content}; # scan for tag fields if ($config{field_tags}) { foreach my $field (keys %{$config{field_tags}}) { my @values = field_get_value($field, $page); if (@values) { foreach my $tag (@values) { if ($tag) { my $link = $config{field_tags}{$field} . '/' . titlepage($tag); add_link($page, $link, lc($field)); } } } } } } # scan sub pagetemplate (@) { my %params=@_; my $page=$params{page}; my $template=$params{template}; field_set_template_values($template, $page); } # pagetemplate # =============================================== # Field interface # --------------------------- sub field_register (%) { my %param=@_; if (!exists $param{id}) { error 'field_register requires id parameter'; return 0; } if (exists $param{call} and !ref $param{call}) { error 'field_register call parameter must be function'; return 0; } my $id = $param{id}; $Fields{$id} = \%param; # add this to the ordering hash # first, last, order; by default, middle my $when = ($param{first} ? '_first' : ($param{last} ? '_last' : ($param{order} ? ($param{order} eq 'first' ? '_first' : ($param{order} eq 'last' ? '_last' : ($param{order} eq 'middle' ? '_middle' : $param{order} ) ) ) : '_middle' ) )); add_lookup_order($id, $when); return 1; } # field_register sub field_get_value ($$) { my $field_name = shift; my $page = shift; # This will return the first value it finds # where the value returned is not undefined. # This will return an array of values if wantarray is true. # The reason why it checks every registered plugin rather than have # plugins declare which fields they know about, is that it is quite # possible that a plugin doesn't know, ahead of time, what fields # will be available; for example, a YAML format plugin would return # any field that happens to be defined in a YAML page file, which # could be anything! # check the cache first my $lc_field_name = lc($field_name); if (wantarray) { if (exists $Cache{$page}{$lc_field_name}{array} and defined $Cache{$page}{$lc_field_name}{array}) { return @{$Cache{$page}{$lc_field_name}{array}}; } } else { if (exists $Cache{$page}{$lc_field_name}{scalar} and defined $Cache{$page}{$lc_field_name}{scalar}) { return $Cache{$page}{$lc_field_name}{scalar}; } } if (!@FieldsLookupOrder) { build_fields_lookup_order(); } # Get either the scalar or the array value depending # on what is requested - don't get both because it wastes time. if (wantarray) { my @array_value = undef; foreach my $id (@FieldsLookupOrder) { # get the data from the pagestate hash if it's there if (exists $pagestate{$page}{$id}{$field_name} and defined $pagestate{$page}{$id}{$field_name}) { @array_value = (ref $pagestate{$page}{$id}{$field_name} ? @{$pagestate{$page}{$id}{$field_name}} : ($pagestate{$page}{$id}{$field_name})); } elsif (exists $pagestate{$page}{$id}{$lc_field_name} and defined $pagestate{$page}{$id}{$lc_field_name}) { @array_value = (ref $pagestate{$page}{$id}{$lc_field_name} ? @{$pagestate{$page}{$id}{$lc_field_name}} : ($pagestate{$page}{$id}{$lc_field_name})); } elsif (exists $Fields{$id}{call}) { @array_value = $Fields{$id}{call}->($field_name, $page); } if (@array_value and $array_value[0]) { last; } } if (!@array_value) { @array_value = field_calculated_values($field_name, $page); } # cache the value $Cache{$page}{$lc_field_name}{array} = \@array_value; return @array_value; } else # scalar { my $value = undef; foreach my $id (@FieldsLookupOrder) { # get the data from the pagestate hash if it's there # but only if it's already a scalar if (exists $pagestate{$page}{$id}{$field_name} and !ref $pagestate{$page}{$id}{$field_name}) { $value = $pagestate{$page}{$id}{$field_name}; } elsif (exists $pagestate{$page}{$id}{$lc_field_name} and !ref $pagestate{$page}{$id}{$lc_field_name}) { $value = $pagestate{$page}{$id}{$lc_field_name}; } elsif (exists $Fields{$id}{call}) { $value = $Fields{$id}{call}->($field_name, $page); } if (defined $value) { last; } } if (!defined $value) { $value = field_calculated_values($field_name, $page); } # cache the value $Cache{$page}{$lc_field_name}{scalar} = $value; return $value; } return undef; } # field_get_value # set the values for the given HTML::Template template sub field_set_template_values ($$;@) { my $template = shift; my $page = shift; my %params = @_; my $get_value_fn = (exists $params{value_fn} ? $params{value_fn} : \&field_get_value); # Find the parameter names in this template # and see if you can find their values. # The reason we check the template for field names is because we # don't know what fields the registered plugins provide; and this is # reasonable because for some plugins (e.g. a YAML data plugin) they # have no way of knowing, ahead of time, what fields they might be # able to provide. my @parameter_names = $template->param(); foreach my $field (@parameter_names) { # Don't redefine if the field already has a value set. next if ($template->param($field)); my $type = $template->query(name => $field); if ($type eq 'LOOP' and $field =~ /_LOOP$/oi) { # Loop fields want arrays. # Figure out what field names to look for: # * names are from the enclosed loop fields my @loop_fields = $template->query(loop => $field); my @loop_vals = (); my %loop_field_arrays = (); foreach my $fn (@loop_fields) { if ($fn !~ /^__/o) # not a special loop variable { my @ival_array = $get_value_fn->($fn, $page); if (@ival_array) { $loop_field_arrays{$fn} = \@ival_array; } } } foreach my $fn (sort keys %loop_field_arrays) { my $i = 0; foreach my $v (@{$loop_field_arrays{$fn}}) { if (!defined $loop_vals[$i]) { $loop_vals[$i] = {}; } $loop_vals[$i]{$fn} = $v; $i++; } } $template->param($field => \@loop_vals); } else # not a loop field { my $value = $get_value_fn->($field, $page); if (defined $value) { $template->param($field => $value); } } } } # field_set_template_values # =============================================== # Private Functions # --------------------------- # Calculate the lookup order # module, AZ # This is crabbed from the PmWiki Markup function sub add_lookup_order { my $id = shift; my $when = shift; # may have given an explicit ordering if ($when =~ /^[A-Z][A-Z]$/o) { $Fields{$id}{seq} = $when; } else { my $cmp = '='; my $seq_field = $when; if ($when =~ /^([<>])(.+)$/o) { $cmp = $1; $seq_field = $2; } $Fields{$seq_field}{dep}{$id} = $cmp; if (exists $Fields{$seq_field}{seq} and defined $Fields{$seq_field}{seq}) { $Fields{$id}{seq} = $Fields{$seq_field}{seq} . $cmp; } } if ($Fields{$id}{seq}) { foreach my $i (keys %{$Fields{$id}{dep}}) { my $m = $Fields{$id}{dep}{$i}; add_lookup_order($i, "$m$id"); } delete $Fields{$id}{dep}; } } sub build_fields_lookup_order { # remove the _first, _last and _middle dummy fields # because we don't need them anymore delete $Fields{_first}; delete $Fields{_last}; delete $Fields{_middle}; my %lookup_spec = (); # Make a hash of the lookup sequences foreach my $id (sort keys %Fields) { my $seq = ($Fields{$id}{seq} ? $Fields{$id}{seq} : 'MM'); if (!exists $lookup_spec{$seq}) { $lookup_spec{$seq} = {}; } $lookup_spec{$seq}{$id} = 1; } # get the field-lookup order by (a) sorting by lookup_spec # and (b) sorting by field-name for the fields that registered # the same field-lookup order foreach my $ord (sort keys %lookup_spec) { push @FieldsLookupOrder, sort keys %{$lookup_spec{$ord}}; } } # build_fields_lookup_order # standard values deduced from other values sub field_calculated_values { my $field_name = shift; my $page = shift; my $value = undef; # Exception for titles # If the title hasn't been found, construct it if ($field_name eq 'title') { $value = pagetitle(IkiWiki::basename($page)); } # and set "page" if desired elsif ($field_name eq 'page') { $value = $page; } # the page above this page; aka the current directory elsif ($field_name eq 'parent_page') { if ($page =~ m{^(.*)/[-\.\w]+$}o) { $value = $1; } } elsif ($field_name eq 'basename') { $value = IkiWiki::basename($page); } elsif ($config{field_allow_config} and $field_name =~ /^config-(.*)$/oi) { my $cfield = $1; if (exists $config{$cfield}) { $value = $config{$cfield}; } } elsif ($field_name =~ /^(.*)-tagpage$/o) { my @array_value = undef; my $real_fn = $1; if (exists $config{field_tags}{$real_fn} and defined $config{field_tags}{$real_fn}) { my @values = field_get_value($real_fn, $page); if (@values) { foreach my $tag (@values) { if ($tag) { my $link = $config{field_tags}{$real_fn} . '/' . $tag; push @array_value, $link; } } if (wantarray) { return @array_value; } else { $value = join(",", @array_value) if $array_value[0]; } } } } return (wantarray ? ($value) : $value); } # field_calculated_values # match field funcs # page-to-check, wanted sub match_a_field ($$) { my $page=shift; my $wanted=shift; # The field name is first; the rest is the match my $field_name; my $glob; if ($wanted =~ /^(\w+)\s+(.*)$/o) { $field_name = $1; $glob = $2; } else { return IkiWiki::FailReason->new("cannot match field"); } # turn glob into a safe regexp my $re=IkiWiki::glob2re($glob); my $val = IkiWiki::Plugin::field::field_get_value($field_name, $page); if (defined $val) { if ($val=~/^$re$/i) { return IkiWiki::SuccessReason->new("$re matches $field_name of $page", $page => $IkiWiki::DEPEND_CONTENT, "" => 1); } else { return IkiWiki::FailReason->new("$re does not match $field_name of $page", "" => 1); } } else { return IkiWiki::FailReason->new("$page does not have a $field_name", "" => 1); } } # match_a_field # check against individual items of a field # (treat the field as an array) # page-to-check, wanted sub match_a_field_item ($$) { my $page=shift; my $wanted=shift; # The field name is first; the rest is the match my $field_name; my $glob; if ($wanted =~ /^(\w+)\s+(.*)$/o) { $field_name = $1; $glob = $2; } else { return IkiWiki::FailReason->new("cannot match field"); } # turn glob into a safe regexp my $re=IkiWiki::glob2re($glob); my @val_array = IkiWiki::Plugin::field::field_get_value($field_name, $page); if (@val_array) { foreach my $val (@val_array) { if (defined $val) { if ($val=~/^$re$/i) { return IkiWiki::SuccessReason->new("$re matches $field_name of $page", $page => $IkiWiki::DEPEND_CONTENT, "" => 1); } } } # not found return IkiWiki::FailReason->new("$re does not match $field_name of $page", "" => 1); } else { return IkiWiki::FailReason->new("$page does not have a $field_name", "" => 1); } } # match_a_field_item # =============================================== # PageSpec functions # --------------------------- package IkiWiki::PageSpec; sub match_field ($$;@) { my $page=shift; my $wanted=shift; return IkiWiki::Plugin::field::match_a_field($page, $wanted); } # match_field sub match_destfield ($$;@) { my $page=shift; my $wanted=shift; my %params=@_; return IkiWiki::FailReason->new("cannot match destpage") unless exists $params{destpage}; # Match the field on the destination page, not the source page return IkiWiki::Plugin::field::match_a_field($params{destpage}, $wanted); } # match_destfield sub match_field_item ($$;@) { my $page=shift; my $wanted=shift; return IkiWiki::Plugin::field::match_a_field_item($page, $wanted); } # match_field sub match_destfield_item ($$;@) { my $page=shift; my $wanted=shift; my %params=@_; return IkiWiki::FailReason->new("cannot match destpage") unless exists $params{destpage}; # Match the field on the destination page, not the source page return IkiWiki::Plugin::field::match_a_field_item($params{destpage}, $wanted); } # match_destfield sub match_field_tagged ($$;@) { my $page=shift; my $wanted=shift; my %params=@_; # The field name is first; the rest is the match my $field_name; my $glob; if ($wanted =~ /^(\w+)\s+(.*)$/o) { $field_name = $1; $glob = $2; } else { return IkiWiki::FailReason->new("cannot match field"); } return match_link($page, $glob, linktype => lc($field_name), @_); } sub match_destfield_tagged ($$;@) { my $page=shift; my $wanted=shift; my %params=@_; return IkiWiki::FailReason->new("cannot match destpage") unless exists $params{destpage}; # Match the field on the destination page, not the source page return IkiWiki::Plugin::field::match_field_tagged($params{destpage}, $wanted); } # =============================================== # SortSpec functions # --------------------------- package IkiWiki::SortSpec; sub cmp_field { my $field = shift; error(gettext("sort=field requires a parameter")) unless defined $field; my $left = IkiWiki::Plugin::field::field_get_value($field, $a); my $right = IkiWiki::Plugin::field::field_get_value($field, $b); $left = "" unless defined $left; $right = "" unless defined $right; return $left cmp $right; } sub cmp_field_natural { my $field = shift; error(gettext("sort=field requires a parameter")) unless defined $field; eval q{use Sort::Naturally}; error $@ if $@; my $left = IkiWiki::Plugin::field::field_get_value($field, $a); my $right = IkiWiki::Plugin::field::field_get_value($field, $b); $left = "" unless defined $left; $right = "" unless defined $right; return Sort::Naturally::ncmp($left, $right); } 1;