X-Git-Url: http://git.vanrenterghem.biz/git.ikiwiki.info.git/blobdiff_plain/e8fd8583408a12d670b370716f4b7627a49533e0..354468d280354234d9c8c91333534c9784f427cf:/IkiWiki.pm diff --git a/IkiWiki.pm b/IkiWiki.pm index 43ffb1fd8..b8e599928 100644 --- a/IkiWiki.pm +++ b/IkiWiki.pm @@ -13,26 +13,32 @@ use open qw{:utf8 :std}; use vars qw{%config %links %oldlinks %pagemtime %pagectime %pagecase %pagestate %wikistate %renderedfiles %oldrenderedfiles - %pagesources %destsources %depends %hooks %forcerebuild - %loaded_plugins}; + %pagesources %destsources %depends %depends_simple %hooks + %forcerebuild %loaded_plugins}; use Exporter q{import}; -our @EXPORT = qw(hook debug error template htmlpage add_depends pagespec_match - pagespec_match_list bestlink htmllink readfile writefile - pagetype srcfile pagename displaytime will_render gettext urlto - targetpage add_underlay pagetitle titlepage linkpage - newpagefile inject add_link +our @EXPORT = qw(hook debug error template htmlpage deptype + add_depends pagespec_match pagespec_match_list bestlink + htmllink readfile writefile pagetype srcfile pagename + displaytime will_render gettext urlto targetpage + add_underlay pagetitle titlepage linkpage newpagefile + inject add_link %config %links %pagestate %wikistate %renderedfiles %pagesources %destsources); our $VERSION = 3.00; # plugin interface version, next is ikiwiki version our $version='unknown'; # VERSION_AUTOREPLACE done by Makefile, DNE our $installdir='/usr'; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE +# Page dependency types. +our $DEPEND_CONTENT=1; +our $DEPEND_PRESENCE=2; +our $DEPEND_LINKS=4; + # Optimisation. use Memoize; memoize("abs2rel"); memoize("pagespec_translate"); -memoize("file_pruned"); +memoize("template_file"); sub getsetup () { wikiname => { @@ -149,6 +155,13 @@ sub getsetup () { safe => 0, # path rebuild => 1, }, + templatedirs => { + type => "internal", + default => [], + description => "additional directories containing template files", + safe => 0, + rebuild => 0, + }, underlaydir => { type => "string", default => "$installdir/share/ikiwiki/basewiki", @@ -336,7 +349,7 @@ sub getsetup () { qr/\.x?html?$/, qr/\.ikiwiki-new$/, qr/(^|\/).svn\//, qr/.arch-ids\//, qr/{arch}\//, qr/(^|\/)_MTN\//, qr/(^|\/)_darcs\//, - qr/\.dpkg-tmp$/], + qr/(^|\/)CVS\//, qr/\.dpkg-tmp$/], description => "regexps of source files to ignore", safe => 0, rebuild => 1, @@ -356,7 +369,7 @@ sub getsetup () { }, web_commit_regexp => { type => "internal", - default => qr/^web commit (by (.*?(?=: |$))|from (\d+\.\d+\.\d+\.\d+)):?(.*)/, + default => qr/^web commit (by (.*?(?=: |$))|from ([0-9a-fA-F:.]+[0-9a-fA-F])):?(.*)/, description => "regexp to parse web commits from logs", safe => 0, rebuild => 0, @@ -536,12 +549,12 @@ sub loadplugins () { } if ($config{rcs}) { - if (exists $IkiWiki::hooks{rcs}) { + if (exists $hooks{rcs}) { error(gettext("cannot use multiple rcs plugins")); } loadplugin($config{rcs}); } - if (! exists $IkiWiki::hooks{rcs}) { + if (! exists $hooks{rcs}) { loadplugin("norcs"); } @@ -661,9 +674,15 @@ sub pagetype ($) { return; } +my %pagename_cache; + sub pagename ($) { my $file=shift; + if (exists $pagename_cache{$file}) { + return $pagename_cache{$file}; + } + my $type=pagetype($file); my $page=$file; $page=~s/\Q.$type\E*$// @@ -672,6 +691,8 @@ sub pagename ($) { if ($config{indexpages} && $page=~/(.*)\/index$/) { $page=$1; } + + $pagename_cache{$file} = $page; return $page; } @@ -860,7 +881,7 @@ sub bestlink ($$) { $l.="/" if length $l; $l.=$link; - if (exists $links{$l}) { + if (exists $pagesources{$l}) { return $l; } elsif (exists $pagecase{lc $l}) { @@ -870,7 +891,7 @@ sub bestlink ($$) { if (length $config{userdir}) { my $l = "$config{userdir}/".lc($link); - if (exists $links{$l}) { + if (exists $pagesources{$l}) { return $l; } elsif (exists $pagecase{lc $l}) { @@ -1060,11 +1081,10 @@ sub htmllink ($$$;@) { } my @attrs; - if (defined $opts{rel}) { - push @attrs, ' rel="'.$opts{rel}.'"'; - } - if (defined $opts{class}) { - push @attrs, ' class="'.$opts{class}.'"'; + foreach my $attr (qw{rel class title}) { + if (defined $opts{$attr}) { + push @attrs, " $attr=\"$opts{$attr}\""; + } } return "$linktext"; @@ -1387,7 +1407,7 @@ sub check_content (@) { my %old=map { $_ => 1 } split("\n", readfile(srcfile($pagesources{$params{page}}))); foreach my $line (split("\n", $params{content})) { - push @diff, $line if ! exists $old{$_}; + push @diff, $line if ! exists $old{$line}; } $params{diff}=join("\n", @diff); } @@ -1467,7 +1487,8 @@ sub loadindex () { %oldrenderedfiles=%pagectime=(); if (! $config{rebuild}) { %pagesources=%pagemtime=%oldlinks=%links=%depends= - %destsources=%renderedfiles=%pagecase=%pagestate=(); + %destsources=%renderedfiles=%pagecase=%pagestate= + %depends_simple=(); } my $in; if (! open ($in, "<", "$config{wikistatedir}/indexdb")) { @@ -1507,7 +1528,27 @@ sub loadindex () { $links{$page}=$d->{links}; $oldlinks{$page}=[@{$d->{links}}]; } - if (exists $d->{depends}) { + if (ref $d->{depends_simple} eq 'ARRAY') { + # old format + $depends_simple{$page}={ + map { $_ => 1 } @{$d->{depends_simple}} + }; + } + elsif (exists $d->{depends_simple}) { + $depends_simple{$page}=$d->{depends_simple}; + } + if (exists $d->{dependslist}) { + # old format + $depends{$page}={ + map { $_ => $DEPEND_CONTENT } + @{$d->{dependslist}} + }; + } + elsif (exists $d->{depends} && ! ref $d->{depends}) { + # old format + $depends{$page}={$d->{depends} => $DEPEND_CONTENT }; + } + elsif (exists $d->{depends}) { $depends{$page}=$d->{depends}; } if (exists $d->{state}) { @@ -1557,6 +1598,10 @@ sub saveindex () { $index{page}{$src}{depends} = $depends{$page}; } + if (exists $depends_simple{$page}) { + $index{page}{$src}{depends_simple} = $depends_simple{$page}; + } + if (exists $pagestate{$page}) { foreach my $id (@hookids) { foreach my $key (keys %{$pagestate{$page}{$id}}) { @@ -1586,7 +1631,8 @@ sub saveindex () { sub template_file ($) { my $template=shift; - foreach my $dir ($config{templatedir}, "$installdir/share/ikiwiki/templates") { + foreach my $dir ($config{templatedir}, @{$config{templatedirs}}, + "$installdir/share/ikiwiki/templates") { return "$dir/$template" if -e "$dir/$template"; } return; @@ -1721,30 +1767,62 @@ sub rcs_receive () { $hooks{rcs}{rcs_receive}{call}->(); } -sub add_depends ($$) { +sub add_depends ($$;$) { my $page=shift; my $pagespec=shift; - - return unless pagespec_valid($pagespec); - - if (! exists $depends{$page}) { - $depends{$page}=$pagespec; - } - else { - $depends{$page}=pagespec_merge($depends{$page}, $pagespec); + my $deptype=shift || $DEPEND_CONTENT; + + # Is the pagespec a simple page name? + if ($pagespec =~ /$config{wiki_file_regexp}/ && + $pagespec !~ /[\s*?()!]/) { + $depends_simple{$page}{lc $pagespec} |= $deptype; + return 1; + } + + # Add explicit dependencies for influences. + my $sub=pagespec_translate($pagespec); + return if $@; + foreach my $p (keys %pagesources) { + my $r=$sub->($p, location => $page); + my $i=$r->influences; + foreach my $k (keys %$i) { + $depends_simple{$page}{lc $k} |= $i->{$k}; + } + last if $r->influences_static; } + $depends{$page}{$pagespec} |= $deptype; return 1; } -sub file_pruned ($$) { - require File::Spec; - my $file=File::Spec->canonpath(shift); - my $base=File::Spec->canonpath(shift); - $file =~ s#^\Q$base\E/+##; +sub deptype (@) { + my $deptype=0; + foreach my $type (@_) { + if ($type eq 'presence') { + $deptype |= $DEPEND_PRESENCE; + } + elsif ($type eq 'links') { + $deptype |= $DEPEND_LINKS; + } + elsif ($type eq 'content') { + $deptype |= $DEPEND_CONTENT; + } + } + return $deptype; +} + +sub file_pruned ($;$) { + my $file=shift; + if (@_) { + require File::Spec; + $file=File::Spec->canonpath($file); + my $base=File::Spec->canonpath(shift); + return if $file eq $base; + $file =~ s#^\Q$base\E/+##; + } my $regexp='('.join('|', @{$config{wiki_file_prune_regexps}}).')'; - return $file =~ m/$regexp/ && $file ne $base; + return $file =~ m/$regexp/; } sub define_gettext () { @@ -1817,14 +1895,6 @@ sub add_link ($$) { unless grep { $_ eq $link } @{$links{$page}}; } -sub pagespec_merge ($$) { - my $a=shift; - my $b=shift; - - return $a if $a eq $b; - return "($a) or ($b)"; -} - sub pagespec_translate ($) { my $spec=shift; @@ -1845,13 +1915,13 @@ sub pagespec_translate ($) { [^\s()]+ # any other text ) \s* # ignore whitespace - }igx) { + }gx) { my $word=$1; if (lc $word eq 'and') { - $code.=' &&'; + $code.=' &'; } elsif (lc $word eq 'or') { - $code.=' ||'; + $code.=' |'; } elsif ($word eq "(" || $word eq ")" || $word eq "!") { $code.=' '.$word; @@ -1897,27 +1967,87 @@ sub pagespec_match ($$;@) { } sub pagespec_match_list ($$;@) { - my $pages=shift; - my $spec=shift; - my @params=@_; + my $page=shift; + my $pagespec=shift; + my %params=@_; - my $sub=pagespec_translate($spec); - error "syntax error in pagespec \"$spec\"" - if $@ || ! defined $sub; - - my @ret; - my $r; - foreach my $page (@$pages) { - $r=$sub->($page, @params); - push @ret, $page if $r; + # Backwards compatability with old calling convention. + if (ref $page) { + print STDERR "warning: a plugin (".caller().") is using pagespec_match_list in an obsolete way, and needs to be updated\n"; + $params{list}=$page; + $page=$params{location}; # ugh! } - if (! @ret && defined $r && $r->isa("IkiWiki::ErrorReason")) { - error(sprintf(gettext("cannot match pages: %s"), $r)); + my $sub=pagespec_translate($pagespec); + error "syntax error in pagespec \"$pagespec\"" + if $@ || ! defined $sub; + + my @candidates; + if (exists $params{list}) { + @candidates=exists $params{filter} + ? grep { ! $params{filter}->($_) } @{$params{list}} + : @{$params{list}}; } else { - return @ret; + @candidates=exists $params{filter} + ? grep { ! $params{filter}->($_) } keys %pagesources + : keys %pagesources; + } + + if (defined $params{sort}) { + my $f; + if ($params{sort} eq 'title') { + $f=sub { pagetitle(basename($a)) cmp pagetitle(basename($b)) }; + } + elsif ($params{sort} eq 'title_natural') { + eval q{use Sort::Naturally}; + if ($@) { + error(gettext("Sort::Naturally needed for title_natural sort")); + } + $f=sub { Sort::Naturally::ncmp(pagetitle(basename($a)), pagetitle(basename($b))) }; + } + elsif ($params{sort} eq 'mtime') { + $f=sub { $pagemtime{$b} <=> $pagemtime{$a} }; + } + elsif ($params{sort} eq 'age') { + $f=sub { $pagectime{$b} <=> $pagectime{$a} }; + } + else { + error sprintf(gettext("unknown sort type %s"), $params{sort}); + } + @candidates = sort { &$f } @candidates; } + + @candidates=reverse(@candidates) if $params{reverse}; + + $depends{$page}{$pagespec} |= ($params{deptype} || $DEPEND_CONTENT); + + # clear params, remainder is passed to pagespec + my $num=$params{num}; + delete @params{qw{num deptype reverse sort filter list}}; + + my @matches; + my $firstfail; + my $count=0; + my $accum=IkiWiki::SuccessReason->new(); + foreach my $p (@candidates) { + my $r=$sub->($p, %params, location => $page); + error(sprintf(gettext("cannot match pages: %s"), $r)) + if $r->isa("IkiWiki::ErrorReason"); + $accum |= $r; + if ($r) { + push @matches, $p; + last if defined $num && ++$count == $num; + } + } + + # Add simple dependencies for accumulated influences. + my $i=$accum->influences; + foreach my $k (keys %$i) { + $depends_simple{$page}{lc $k} |= $i->{$k}; + } + + return @matches; } sub pagespec_valid ($) { @@ -1937,36 +2067,65 @@ sub glob2re ($) { package IkiWiki::FailReason; use overload ( - '""' => sub { ${$_[0]} }, + '""' => sub { $_[0][0] }, '0+' => sub { 0 }, '!' => sub { bless $_[0], 'IkiWiki::SuccessReason'}, + '&' => sub { $_[0]->merge_influences($_[1], 1); $_[0] }, + '|' => sub { $_[1]->merge_influences($_[0]); $_[1] }, fallback => 1, ); -sub new { - my $class = shift; - my $value = shift; - return bless \$value, $class; -} - -package IkiWiki::ErrorReason; - -our @ISA = 'IkiWiki::FailReason'; +our @ISA = 'IkiWiki::SuccessReason'; package IkiWiki::SuccessReason; use overload ( - '""' => sub { ${$_[0]} }, + '""' => sub { $_[0][0] }, '0+' => sub { 1 }, '!' => sub { bless $_[0], 'IkiWiki::FailReason'}, + '&' => sub { $_[1]->merge_influences($_[0], 1); $_[1] }, + '|' => sub { $_[0]->merge_influences($_[1]); $_[0] }, fallback => 1, ); sub new { my $class = shift; my $value = shift; - return bless \$value, $class; -}; + return bless [$value, {@_}], $class; +} + +sub influences { + my $this=shift; + $this->[1]={@_} if @_; + my %i=%{$this->[1]}; + delete $i{""}; + return \%i; +} + +sub influences_static { + return ! $_[0][1]->{""}; +} + +sub merge_influences { + my $this=shift; + my $other=shift; + my $anded=shift; + + if (! $anded || (($this || %{$this->[1]}) && + ($other || %{$other->[1]}))) { + foreach my $influence (keys %{$other->[1]}) { + $this->[1]{$influence} |= $other->[1]{$influence}; + } + } + else { + # influence blocker + $this->[1]={}; + } +} + +package IkiWiki::ErrorReason; + +our @ISA = 'IkiWiki::FailReason'; package IkiWiki::PageSpec; @@ -2017,27 +2176,30 @@ sub match_link ($$;@) { my $from=exists $params{location} ? $params{location} : ''; my $links = $IkiWiki::links{$page}; - return IkiWiki::FailReason->new("$page has no links") unless $links && @{$links}; + return IkiWiki::FailReason->new("$page has no links", "" => 1) + unless $links && @{$links}; my $bestlink = IkiWiki::bestlink($from, $link); foreach my $p (@{$links}) { if (length $bestlink) { - return IkiWiki::SuccessReason->new("$page links to $link") + return IkiWiki::SuccessReason->new("$page links to $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1) if $bestlink eq IkiWiki::bestlink($page, $p); } else { - return IkiWiki::SuccessReason->new("$page links to page $p matching $link") + return IkiWiki::SuccessReason->new("$page links to page $p matching $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1) if match_glob($p, $link, %params); - $p=~s/^\///; + my ($p_rel)=$p=~/^\/?(.*)/; $link=~s/^\///; - return IkiWiki::SuccessReason->new("$page links to page $p matching $link") - if match_glob($p, $link, %params); + return IkiWiki::SuccessReason->new("$page links to page $p_rel matching $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1) + if match_glob($p_rel, $link, %params); } } - return IkiWiki::FailReason->new("$page does not link to $link"); + return IkiWiki::FailReason->new("$page does not link to $link", "" => 1); } sub match_backlink ($$;@) { - return match_link($_[1], $_[0], @_); + my $ret=match_link($_[1], $_[0], @_); + $ret->influences($_[1] => $IkiWiki::DEPEND_LINKS); + return $ret; } sub match_created_before ($$;@) { @@ -2049,14 +2211,14 @@ sub match_created_before ($$;@) { if (exists $IkiWiki::pagectime{$testpage}) { if ($IkiWiki::pagectime{$page} < $IkiWiki::pagectime{$testpage}) { - return IkiWiki::SuccessReason->new("$page created before $testpage"); + return IkiWiki::SuccessReason->new("$page created before $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE); } else { - return IkiWiki::FailReason->new("$page not created before $testpage"); + return IkiWiki::FailReason->new("$page not created before $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE); } } else { - return IkiWiki::ErrorReason->new("$testpage does not exist"); + return IkiWiki::ErrorReason->new("$testpage does not exist", $testpage => $IkiWiki::DEPEND_PRESENCE); } } @@ -2069,14 +2231,14 @@ sub match_created_after ($$;@) { if (exists $IkiWiki::pagectime{$testpage}) { if ($IkiWiki::pagectime{$page} > $IkiWiki::pagectime{$testpage}) { - return IkiWiki::SuccessReason->new("$page created after $testpage"); + return IkiWiki::SuccessReason->new("$page created after $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE); } else { - return IkiWiki::FailReason->new("$page not created after $testpage"); + return IkiWiki::FailReason->new("$page not created after $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE); } } else { - return IkiWiki::ErrorReason->new("$testpage does not exist"); + return IkiWiki::ErrorReason->new("$testpage does not exist", $testpage => $IkiWiki::DEPEND_PRESENCE); } }