%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 use_pagespec
+ 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 () {
$links{$page}=$d->{links};
$oldlinks{$page}=[@{$d->{links}}];
}
- if (exists $d->{depends_simple}) {
+ 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 { $_ => 1 } @{$d->{dependslist}}
+ 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} => 1};
+ $depends{$page}=$d->{depends};
}
if (exists $d->{state}) {
$pagestate{$page}=$d->{state};
};
if (exists $depends{$page}) {
- $index{page}{$src}{dependslist} = [ keys %{$depends{$page}} ];
+ $index{page}{$src}{depends} = $depends{$page};
}
if (exists $depends_simple{$page}) {
- $index{page}{$src}{depends_simple} = [ keys %{$depends_simple{$page}} ];
+ $index{page}{$src}{depends_simple} = $depends_simple{$page};
}
if (exists $pagestate{$page}) {
$hooks{rcs}{rcs_receive}{call}->();
}
-sub add_depends ($$) {
+sub add_depends ($$;$) {
my $page=shift;
my $pagespec=shift;
+ my $deptype=shift || $DEPEND_CONTENT;
+ # Is the pagespec a simple page name?
if ($pagespec =~ /$config{wiki_file_regexp}/ &&
- $pagespec !~ /[\s*?()!]/) {
- # a simple dependency, which can be matched by string eq
- $depends_simple{$page}{lc $pagespec} = 1;
+ $pagespec !~ /[\s*?()!]/) {
+ $depends_simple{$page}{lc $pagespec} |= $deptype;
return 1;
}
- return unless pagespec_valid($pagespec);
-
- $depends{$page}{$pagespec} = 1;
+ # Analyse the pagespec, and match it against all pages
+ # to get a list of influences, and add explicit dependencies
+ # for those.
+ #my $sub=pagespec_translate($pagespec);
+ #return if $@;
+ #foreach my $p (keys %pagesources) {
+ # my $r=$sub->($p, location => $page );
+ # my %i=$r->influences;
+ # foreach my $i (keys %i) {
+ # $depends_simple{$page}{lc $i} |= $i{$i};
+ # }
+ #}
+ print STDERR "warning: use of add_depends; influences not tracked\n";
+
+ $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 use_pagespec ($$;@) {
+ my $page=shift;
+ my $pagespec=shift;
+ my %params=@_;
+
+ my $sub=pagespec_translate($pagespec);
+ error "syntax error in pagespec \"$pagespec\""
+ if $@ || ! defined $sub;
+
+ my @candidates;
+ if (exists $params{limit}) {
+ @candidates=grep { $params{limit}->($_) } keys %pagesources;
+ }
+ else {
+ @candidates=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};
+
+ my @matches;
+ my $firstfail;
+ my $count=0;
+ foreach my $p (@candidates) {
+ my $r=$sub->($p, location => $page);
+ if ($r) {
+ push @matches, [$p, $r];
+ last if defined $params{num} && ++$count == $params{num};
+ }
+ elsif (! defined $firstfail) {
+ $firstfail=$r;
+ }
+ }
+
+ $depends{$page}{$pagespec} |= ($params{deptype} || $DEPEND_CONTENT);
+
+ my @ret;
+ if (@matches) {
+ # Add all influences from successful matches.
+ foreach my $m (@matches) {
+ push @ret, $m->[0];
+ my %i=$m->[1]->influences;
+ foreach my $i (keys %i) {
+ $depends_simple{$page}{lc $i} |= $i{$i};
+ }
+ }
+ }
+ elsif (defined $firstfail) {
+ # Add influences from one failure. (Which one should not
+ # matter; all should have the same influences.)
+ my %i=$firstfail->influences;
+ foreach my $i (keys %i) {
+ $depends_simple{$page}{lc $i} |= $i{$i};
+ }
+
+ error(sprintf(gettext("cannot match pages: %s"), $firstfail))
+ if $firstfail->isa("IkiWiki::ErrorReason");
+ }
+
+ return @ret;
+}
+
+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 () {
}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;
package IkiWiki::FailReason;
use overload (
- '""' => sub { ${$_[0]} },
+ '""' => sub { $_[0][0] },
'0+' => sub { 0 },
'!' => sub { bless $_[0], 'IkiWiki::SuccessReason'},
+ '&' => sub { $_[0]->merge_influences($_[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] },
+ '|' => 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;
+ if (! @_) {
+ return %{$this->[1]};
+ }
+ else {
+ $this->[1]={@_};
+ }
+}
+
+sub merge_influences {
+ my $this=shift;
+ my $other=shift;
+ foreach my $influence (keys %{$other->[1]}) {
+ $this->[1]{$influence} |= $other->[1]{$influence};
+ }
+}
+
+package IkiWiki::ErrorReason;
+
+our @ISA = 'IkiWiki::FailReason';
package IkiWiki::PageSpec;
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")
+ 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)
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)
if match_glob($p, $link, %params);
my ($p_rel)=$p=~/^\/?(.*)/;
$link=~s/^\///;
- return IkiWiki::SuccessReason->new("$page links to page $p_rel matching $link")
+ return IkiWiki::SuccessReason->new("$page links to page $p_rel matching $link", $page => $IkiWiki::DEPEND_LINKS)
if match_glob($p_rel, $link, %params);
}
}
}
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 ($$;@) {
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);
}
}
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);
}
}
use IkiWiki;
use Encode;
-my %backlinks;
+my (%backlinks, %rendered);
our %brokenlinks;
my $links_calculated=0;
sub scan ($) {
my $file=shift;
+ debug(sprintf(gettext("scanning %s"), $file));
+
my $type=pagetype($file);
if (defined $type) {
my $srcfile=srcfile($file);
}
}
-sub render ($) {
+sub render ($$) {
my $file=shift;
+ return if $rendered{$file};
+ debug(shift);
+ $rendered{$file}=1;
my $type=pagetype($file);
my $srcfile=srcfile($file);
}
sub find_src_files () {
- my (@files, %pages);
+ my @files;
+ my %pages;
eval q{use File::Find};
error($@) if $@;
find({
no_chdir => 1,
wanted => sub {
- $_=decode_utf8($_);
- if (file_pruned($_, $config{srcdir})) {
+ my $file=decode_utf8($_);
+ $file=~s/^\Q$config{srcdir}\E\/?//;
+ my $page = pagename($file);
+ if (! exists $pagesources{$page} &&
+ file_pruned($file)) {
$File::Find::prune=1;
+ return;
}
- elsif (! -l $_ && ! -d _) {
- my ($f)=/$config{wiki_file_regexp}/; # untaint
- if (! defined $f) {
- warn(sprintf(gettext("skipping bad filename %s"), $_)."\n");
- }
- else {
- $f=~s/^\Q$config{srcdir}\E\/?//;
- push @files, $f;
- my $page = pagename($f);
- if ($pages{$page}) {
- debug(sprintf(gettext("%s has multiple possible source pages"), $page));
- }
- $pages{$page}=1;
+ return if -l $_ || -d _ || ! length $file;
+
+ my ($f) = $file =~ /$config{wiki_file_regexp}/; # untaint
+ if (! defined $f) {
+ warn(sprintf(gettext("skipping bad filename %s"), $file)."\n");
+ }
+ else {
+ push @files, $f;
+ if ($pages{$page}) {
+ debug(sprintf(gettext("%s has multiple possible source pages"), $page));
}
+ $pages{$page}=1;
}
},
}, $config{srcdir});
find({
no_chdir => 1,
wanted => sub {
- $_=decode_utf8($_);
- if (file_pruned($_, $dir)) {
+ my $file=decode_utf8($_);
+ $file=~s/^\Q$dir\E\/?//;
+ my $page=pagename($file);
+ if (! exists $pagesources{$page} &&
+ file_pruned($file)) {
$File::Find::prune=1;
+ return;
}
- elsif (! -l $_ && ! -d _) {
- my ($f)=/$config{wiki_file_regexp}/; # untaint
- if (! defined $f) {
- warn(sprintf(gettext("skipping bad filename %s"), $_)."\n");
- }
- else {
- $f=~s/^\Q$dir\E\/?//;
- # avoid underlaydir
- # override attacks; see
- # security.mdwn
- if (! -l "$config{srcdir}/$f" &&
- ! -e _) {
- my $page=pagename($f);
- if (! $pages{$page}) {
- push @files, $f;
- $pages{$page}=1;
- }
+ return if -l $_ || -d _ || ! length $file;
+
+ my ($f) = $file =~ /$config{wiki_file_regexp}/; # untaint
+ if (! defined $f) {
+ warn(sprintf(gettext("skipping bad filename %s"), $file)."\n");
+ }
+ else {
+ # avoid underlaydir override
+ # attacks; see security.mdwn
+ if (! -l "$config{srcdir}/$f" &&
+ ! -e _) {
+ if (! $pages{$page}) {
+ push @files, $f;
+ $pages{$page}=1;
}
}
}
},
}, $dir);
};
-
- # Returns a list of all source files found, and a hash of
- # the corresponding page names.
return \@files, \%pages;
}
-sub refresh () {
- srcdir_check();
- run_hooks(refresh => sub { shift->() });
- my ($files, $exists)=find_src_files();
+sub find_new_files ($) {
+ my $files=shift;
+ my @new;
+ my @internal_new;
- my (%rendered, @add, @del, @internal);
- # check for added or removed pages
foreach my $file (@$files) {
my $page=pagename($file);
if (exists $pagesources{$page} && $pagesources{$page} ne $file) {
$pagesources{$page}=$file;
if (! $pagemtime{$page}) {
if (isinternal($page)) {
- push @internal, $file;
+ push @internal_new, $file;
}
else {
- push @add, $file;
+ push @new, $file;
if ($config{getctime} && -e "$config{srcdir}/$file") {
eval {
my $time=rcs_getctime("$config{srcdir}/$file");
}
}
}
+
+ return \@new, \@internal_new;
+}
+
+sub find_del_files ($) {
+ my $pages=shift;
+ my @del;
+ my @internal_del;
+
foreach my $page (keys %pagemtime) {
- if (! $exists->{$page}) {
+ if (! $pages->{$page}) {
if (isinternal($page)) {
- push @internal, $pagesources{$page};
+ push @internal_del, $pagesources{$page};
}
else {
debug(sprintf(gettext("removing old page %s"), $page));
}
}
- # find changed and new files
- my @needsbuild;
+ return \@del, \@internal_del;
+}
+
+sub find_changed ($) {
+ my $files=shift;
+ my @changed;
+ my @internal_changed;
foreach my $file (@$files) {
my $page=pagename($file);
my ($srcfile, @stat)=srcfile_stat($file);
$stat[9] > $pagemtime{$page} ||
$forcerebuild{$page}) {
$pagemtime{$page}=$stat[9];
+
if (isinternal($page)) {
- push @internal, $file;
# Preprocess internal page in scan-only mode.
preprocess($page, $page, readfile($srcfile), 1);
+ push @internal_changed, $file;
}
else {
- push @needsbuild, $file;
+ push @changed, $file;
}
}
}
- run_hooks(needsbuild => sub { shift->(\@needsbuild) });
+ return \@changed, \@internal_changed;
+}
- # scan and render files
- foreach my $file (@needsbuild) {
- debug(sprintf(gettext("scanning %s"), $file));
- scan($file);
- }
- calculate_links();
- foreach my $file (@needsbuild) {
- debug(sprintf(gettext("building %s"), $file));
- render($file);
- $rendered{$file}=1;
- }
- foreach my $file (@internal) {
- # internal pages are not rendered
+sub calculate_old_links ($$) {
+ my ($changed, $del)=@_;
+ my %oldlink_targets;
+ foreach my $file (@$changed, @$del) {
my $page=pagename($file);
- delete $depends{$page};
- delete $depends_simple{$page};
- foreach my $old (@{$renderedfiles{$page}}) {
- delete $destsources{$old};
+ if (exists $oldlinks{$page}) {
+ foreach my $l (@{$oldlinks{$page}}) {
+ $oldlink_targets{$page}{$l}=bestlink($page, $l);
+ }
}
- $renderedfiles{$page}=[];
}
-
- # rebuild pages that link to added or removed pages
- if (@add || @del) {
- foreach my $f (@add, @del) {
- my $p=pagename($f);
- foreach my $page (keys %{$backlinks{$p}}) {
- my $file=$pagesources{$page};
- next if $rendered{$file};
- debug(sprintf(gettext("building %s, which links to %s"), $file, $p));
- render($file);
- $rendered{$file}=1;
+ return \%oldlink_targets;
+}
+
+sub derender_internal ($) {
+ my $file=shift;
+ my $page=pagename($file);
+ delete $depends{$page};
+ delete $depends_simple{$page};
+ foreach my $old (@{$renderedfiles{$page}}) {
+ delete $destsources{$old};
+ }
+ $renderedfiles{$page}=[];
+}
+
+sub render_linkers ($) {
+ my $f=shift;
+ my $p=pagename($f);
+ foreach my $page (keys %{$backlinks{$p}}) {
+ my $file=$pagesources{$page};
+ render($file, sprintf(gettext("building %s, which links to %s"), $file, $p));
+ }
+}
+
+sub remove_unrendered () {
+ foreach my $src (keys %rendered) {
+ my $page=pagename($src);
+ foreach my $file (@{$oldrenderedfiles{$page}}) {
+ if (! grep { $_ eq $file } @{$renderedfiles{$page}}) {
+ debug(sprintf(gettext("removing %s, no longer built by %s"), $file, $page));
+ prune($config{destdir}."/".$file);
}
}
}
+}
- if (%rendered || @del || @internal) {
- my @changed=(keys %rendered, @del);
+sub calculate_changed_links ($$$) {
+ my ($changed, $del, $oldlink_targets)=@_;
- my %lcchanged = map { lc(pagename($_)) => 1 } @changed;
-
- # rebuild dependant pages
- foreach my $f (@$files) {
- next if $rendered{$f};
- my $p=pagename($f);
- my $reason = undef;
+ my (%backlinkchanged, %linkchangers);
- if (exists $depends_simple{$p}) {
- foreach my $d (keys %{$depends_simple{$p}}) {
- if (exists $lcchanged{$d}) {
- $reason = $d;
- last;
- }
+ foreach my $file (@$changed, @$del) {
+ my $page=pagename($file);
+
+ if (exists $links{$page}) {
+ foreach my $l (@{$links{$page}}) {
+ my $target=bestlink($page, $l);
+ if (! exists $oldlink_targets->{$page}{$l} ||
+ $target ne $oldlink_targets->{$page}{$l}) {
+ $backlinkchanged{$target}=1;
+ $linkchangers{lc($page)}=1;
}
+ delete $oldlink_targets->{$page}{$l};
}
+ }
+ if (exists $oldlink_targets->{$page} &&
+ %{$oldlink_targets->{$page}}) {
+ foreach my $target (values %{$oldlink_targets->{$page}}) {
+ $backlinkchanged{$target}=1;
+ }
+ $linkchangers{lc($page)}=1;
+ }
+ }
- if (exists $depends{$p} && ! defined $reason) {
- D: foreach my $d (keys %{$depends{$p}}) {
- my $sub=pagespec_translate($d);
- next if $@ || ! defined $sub;
+ return \%backlinkchanged, \%linkchangers;
+}
- # only consider internal files
- # if the page explicitly depends
- # on such files
- foreach my $file (@changed, $d =~ /internal\(/ ? @internal : ()) {
+sub render_dependent ($$$$$$$) {
+ my ($files, $new, $internal_new, $del, $internal_del,
+ $internal_changed, $linkchangers)=@_;
+
+ my @changed=(keys %rendered, @$del);
+ my @exists_changed=(@$new, @$del);
+
+ my %lc_changed = map { lc(pagename($_)) => 1 } @changed;
+ my %lc_exists_changed = map { lc(pagename($_)) => 1 } @exists_changed;
+
+ foreach my $f (@$files) {
+ next if $rendered{$f};
+ my $p=pagename($f);
+ my $reason = undef;
+
+ if (exists $depends_simple{$p}) {
+ foreach my $d (keys %{$depends_simple{$p}}) {
+ if (($depends_simple{$p}{$d} & $IkiWiki::DEPEND_CONTENT &&
+ $lc_changed{$d})
+ ||
+ ($depends_simple{$p}{$d} & $IkiWiki::DEPEND_PRESENCE &&
+ $lc_exists_changed{$d})
+ ||
+ ($depends_simple{$p}{$d} & $IkiWiki::DEPEND_LINKS &&
+ $linkchangers->{$d})
+ ) {
+ $reason = $d;
+ last;
+ }
+ }
+ }
+
+ if (exists $depends{$p} && ! defined $reason) {
+ foreach my $dep (keys %{$depends{$p}}) {
+ my $sub=pagespec_translate($dep);
+ next if $@ || ! defined $sub;
+
+ # only consider internal files
+ # if the page explicitly depends
+ # on such files
+ my $internal_dep=$dep =~ /internal\(/;
+
+ my $in=sub {
+ my $list=shift;
+ my $type=shift;
+ foreach my $file (@$list) {
next if $file eq $f;
my $page=pagename($file);
if ($sub->($page, location => $p)) {
- $reason = $page;
- last D;
+ if ($type == $IkiWiki::DEPEND_LINKS) {
+ next unless $linkchangers->{lc($page)};
+ }
+ return $page;
}
}
+ return undef;
+ };
+
+ if ($depends{$p}{$dep} & $IkiWiki::DEPEND_CONTENT) {
+ last if $reason =
+ $in->(\@changed, $IkiWiki::DEPEND_CONTENT);
+ last if $internal_dep && ($reason =
+ $in->($internal_new, $IkiWiki::DEPEND_CONTENT) ||
+ $in->($internal_del, $IkiWiki::DEPEND_CONTENT) ||
+ $in->($internal_changed, $IkiWiki::DEPEND_CONTENT));
}
- }
-
- if (defined $reason) {
- debug(sprintf(gettext("building %s, which depends on %s"), $f, $reason));
- render($f);
- $rendered{$f}=1;
- }
- }
-
- # handle backlinks; if a page has added/removed links,
- # update the pages it links to
- my %linkchanged;
- foreach my $file (@changed) {
- my $page=pagename($file);
-
- if (exists $links{$page}) {
- foreach my $link (map { bestlink($page, $_) } @{$links{$page}}) {
- if (length $link &&
- (! exists $oldlinks{$page} ||
- ! grep { bestlink($page, $_) eq $link } @{$oldlinks{$page}})) {
- $linkchanged{$link}=1;
- }
+ if ($depends{$p}{$dep} & $IkiWiki::DEPEND_PRESENCE) {
+ last if $reason =
+ $in->(\@exists_changed, $IkiWiki::DEPEND_PRESENCE);
+ last if $internal_dep && ($reason =
+ $in->($internal_new, $IkiWiki::DEPEND_PRESENCE) ||
+ $in->($internal_del, $IkiWiki::DEPEND_PRESENCE));
}
- }
- if (exists $oldlinks{$page}) {
- foreach my $link (map { bestlink($page, $_) } @{$oldlinks{$page}}) {
- if (length $link &&
- (! exists $links{$page} ||
- ! grep { bestlink($page, $_) eq $link } @{$links{$page}})) {
- $linkchanged{$link}=1;
- }
+ if ($depends{$p}{$dep} & $IkiWiki::DEPEND_LINKS) {
+ last if $reason =
+ $in->(\@changed, $IkiWiki::DEPEND_LINKS);
+ last if $internal_dep && ($reason =
+ $in->($internal_new, $IkiWiki::DEPEND_LINKS) ||
+ $in->($internal_del, $IkiWiki::DEPEND_LINKS) ||
+ $in->($internal_changed, $IkiWiki::DEPEND_LINKS));
}
}
}
-
- foreach my $link (keys %linkchanged) {
- my $linkfile=$pagesources{$link};
- if (defined $linkfile) {
- next if $rendered{$linkfile};
- debug(sprintf(gettext("building %s, to update its backlinks"), $linkfile));
- render($linkfile);
- $rendered{$linkfile}=1;
- }
+
+ if (defined $reason) {
+ render($f, sprintf(gettext("building %s, which depends on %s"), $f, $reason));
+ return 1;
}
}
- # remove no longer rendered files
- foreach my $src (keys %rendered) {
- my $page=pagename($src);
- foreach my $file (@{$oldrenderedfiles{$page}}) {
- if (! grep { $_ eq $file } @{$renderedfiles{$page}}) {
- debug(sprintf(gettext("removing %s, no longer built by %s"), $file, $page));
- prune($config{destdir}."/".$file);
- }
+ return 0;
+}
+
+sub render_backlinks ($) {
+ my $backlinkchanged=shift;
+ foreach my $link (keys %$backlinkchanged) {
+ my $linkfile=$pagesources{$link};
+ if (defined $linkfile) {
+ render($linkfile, sprintf(gettext("building %s, to update its backlinks"), $linkfile));
}
}
+}
+
+sub refresh () {
+ srcdir_check();
+ run_hooks(refresh => sub { shift->() });
+ my ($files, $pages)=find_src_files();
+ my ($new, $internal_new)=find_new_files($files);
+ my ($del, $internal_del)=find_del_files($pages);
+ my ($changed, $internal_changed)=find_changed($files);
+ run_hooks(needsbuild => sub { shift->($changed) });
+ my $oldlink_targets=calculate_old_links($changed, $del);
+
+ foreach my $file (@$changed) {
+ scan($file);
+ }
+
+ calculate_links();
+
+ foreach my $file (@$changed) {
+ render($file, sprintf(gettext("building %s"), $file));
+ }
+ foreach my $file (@$internal_new, @$internal_del, @$internal_changed) {
+ derender_internal($file);
+ }
+
+ my ($backlinkchanged, $linkchangers)=calculate_changed_links($changed,
+ $del, $oldlink_targets);
+
+ foreach my $file (@$new, @$del) {
+ render_linkers($file);
+ }
+
+ if (@$changed || @$internal_changed ||
+ @$del || @$internal_del || @$internal_new) {
+ 1 while render_dependent($files, $new, $internal_new,
+ $del, $internal_del, $internal_changed,
+ $linkchangers);
+ }
+
+ render_backlinks($backlinkchanged);
+ remove_unrendered();
- if (@del) {
- run_hooks(delete => sub { shift->(@del) });
+ if (@$del) {
+ run_hooks(delete => sub { shift->(@$del) });
}
if (%rendered) {
run_hooks(change => sub { shift->(keys %rendered) });
* mirrorlist: Display nothing if list is empty.
* Fix a bug that could lead to duplicate links being recorded
for tags.
+ * Added support framework for multiple types of dependencies.
+ * Allow declaring that a dependency is only affected by page presence
+ or changes to its links.
+ * pagecount, calendar, postsparkline, progress: Use a presence dependency,
+ which makes these directives much less expensive to use, since page
+ edits will no longer trigger an unnecessary update.
+ * map: Use a presence dependency unless show= is specified.
+ This makes maps efficient enough that they can be used on sidebars!
+ * inline: Use a presence dependency in quick mode.
+ * brokenlinks: Use a link dependency.
+ This makes it much more efficient, only updating when really necessary.
+ * orphans, pagestats: Use a combination of presence and link dependencies.
+ This makes them more efficient. It also fixes a longstanding bug,
+ where if only a small set of pages were considered by orphans/pagestats,
+ changes to links on other pages failed to cause an update.
+ * linkmap: Use a combination of presence and link dependencies.
+ This makes the map be regenerated much less frequently in many cases,
+ so larger maps are more practical to use now.
+ * Transitive dependencies are now correctly supported.
+ * Rebuild wikis on upgrade to this version to get improved dependency
+ info.
+ * Plugins providing PageSpec `match_*` functions should pass additional
+ influence information when creating result objects.
+ * Added `use_pagespec` function, that plugins can use to find a list
+ of matching pages and add dependencies and influences, all at once,
+ and efficiently.
+ * Optimize away most expensive file prune calls, when refreshing,
+ by only checking new files.
-- Joey Hess <joeyh@debian.org> Sun, 27 Sep 2009 17:40:03 -0400
Does ShavedByBob.mdwn include itself?
- (Yeah - in IkiWiki currently links are included by include, but the idea holds. I had a good example a while back, but I can't think of it right now.)
+ (Yeah - in IkiWiki currently links are *not* included by include, but the idea holds. I had a good example a while back, but I can't think of it right now.)
sigh.
> to determine what metadata, pages, etc they depend on. It is indeed
> tricky to do. More thoughts on influence lists a bit below. --[[Joey]]
+ >> The big part of what makes this tricky is that there may be cycles in the
+ >> dependency graph. This can lead to situations where the result is just not
+ >> well defined. This is what I was trying to get at above. -- [[Will]]
+
+ >>> Hmm, I'm not seeing cycles be a problem, at least with the current
+ >>> pagespec terms. --[[Joey]]
+
+ >>>> Oh, they're not with current pagespec terms. But this is really close to extending to handle
+ >>>> functional pagespecs, etc. And I think I'd like to think about that now.
+ >>>>
+ >>>> Having said that, I don't want to hold you up - you seem to be making progress. The best is
+ >>>> the enemy of the good, etc. etc.
+ >>>>
+ >>>> For my part, I'm imagining we have two more constructs in IkiWiki:
+ >>>>
+ >>>> * A map directive that actually wikilinks to the pages it links to, and
+ >>>> * A `match_sharedLink(pageX)` matching function that matches pageY if both pageX and pageY each have links to any same third page, pageZ.
+ >>>>
+ >>>> With those two constructs, one page changing might change the set of pages included in a map somewhere, which might then change the set of pages matched by some other pagespec, which might then...
+ >>>>
+ >>>> --[[Will]]
+
+ >>>>> I think that should be supported by [[bugs/transitive_dependencies]].
+ >>>>> At least in the current implementation, which considers each page
+ >>>>> that is rendered to be changed, and rebuilds pages that are dependent
+ >>>>> on it, in a loop. An alternate implementation, which could be faster,
+ >>>>> is to construct a directed graph and traverse it just once. Sounds
+ >>>>> like that would probably not support what you want to do.
+ >>>>> --[[Joey]]
+
----
### Link dependencies
that the page links to, which is just what link dependencies are
triggered on.
+[[done]]
----
### the removal problem
that currently match it. If the list changes, the dependency is triggered.
Should be doable, but may involve more work than
- currently. Consider that a dependency on "bugs/*" currently
+ currently. Consider that a dependency on `bugs/*` currently
is triggered by just checking until *one* page is found to match it.
But to store the list, *every* page would have to be tried against it.
Unless the list can somehow be intelligently updated, looking at only the
I'm using this term for the concept of a list of pages whose modification
can indirectly influence what pages a pagespec matches.
+ > Trying to make a formal definition of this: (Note, I'm using the term sets rather than lists, but they're roughly equivalent)
+ >
+ > * Let the *matching set* for a pagespec be the set of existing pages that the pagespec matches.
+ > * Let a *influence set* for a pagespec be the set of all pages, *p*, whose alteration might:
+ > * cause the pagespec to include or exclude a page other than *p*, or
+ > * cause the pagespec to exclude *p*.
+ >
+ >> \[Will snipped some stuff and edited the formal definition]
+ >
+ > --[[Will]]
+
+ >> I appreciate the formalism!
+ >>
+ >> Only existing pages need to be in these sets, because if a page is added
+ >> in the future, the existing dependency code will always test to see
+ >> if it matches. So it will be in the maching set (or not) at that point.
+ >>
+ >>> Hrm, I agree with you in general, but I think I can come up with nasty counter-examples. What about a pagespec
+ >>> of "!backlink(bogus)" where the page bogus doesn't exist? In this case, the page 'bogus' needs to be in the influence
+ >>> set even though it doesn't exist.
+ >>>
+ >>>> I think you're right, this is a case that the current code is not
+ >>>> handling. Actually, I made all the pagespecs return influences
+ >>>> even if the influence was not present or did not match. But, it
+ >>>> currently only records influences as dependencies when a pagespec
+ >>>> successfully matches. Now I'm sure that is wrong, and I've removed
+ >>>> that false optimisation. I've updated some of the below. --[[Joey]]
+ >>>
+ >>> Also, I would really like the formalism to include the whole dependency system, not just any additions to it. That will make
+ >>> the whole thing much easier to reason about.
+ >>
+ >> The problem with your definition of direct influence set seems to be
+ >> that it doesn't allow `link()` and `title()` to have as an indirect
+ >> influence, the page that matches. But I'm quite sure we need those.
+ >> --[[Joey]]
+
+ >>> I see what you mean. Does the revised definition capture this effectively?
+ >>> The problem with this revised definition is that it still doesn't match your examples below.
+ >>> My revised definition will include pretty much all currently matching pages to be in the influence list
+ >>> because deletion of any of them would cause a change in which pages are matched - the removal problem.
+ >>> -- [[Will]]
+
#### Examples
* The pagespec "created_before(foo)" has an influence list that contains foo.
- The removal or (re)creation of foo changes what pages match it.
+ The removal or (re)creation of foo changes what pages match it. Note that
+ this is true even if the pagespec currently fails to match.
* The pagespec "foo" has an empty influence list. This is because a
modification/creation/removal of foo directly changes what the pagespec
Avoiding including every page in the wiki into its influence list is
very important!
+ >>> So, why don't the above influence lists contain the currently matched pages?
+ >>> Don't you need this to handle the removal problem? -- [[Will]]
+
+ >>>> The removal problem is slightly confusingly named, since it does not
+ >>>> affect pages that were matched by a glob and have been removed. Such
+ >>>> pages can be handled without being influences, because ikiwiki knows
+ >>>> they have been removed, and so can still match them against the
+ >>>> pagespec, and see they used to match; and thus knows that the
+ >>>> dependency has triggered.
+ >>>>
+ >>>> Maybe the thing to do is consider this an optimisation, where such
+ >>>> pages are influences, but ikiwiki is able to implicitly find them,
+ >>>> so they do not need to be explicitly stored. --[[Joey]]
+
* The pagespec "title(foo)" has an influence list that contains every page
that currently matches it. A change to any matching page can change its
- title. Why is that considered an indirect influence? Well, the pagespec
- might be used in a presence dependency, and so its title changing
- would not directly affect the dependency.
+ title, making it not match any more, and so the list is needed due to the
+ removal problem. A page that does not have a matching title is not an
+ influence, because modifying the page to change its title directly
+ changes what the pagespec matches.
* The pagespec "backlink(index)" has an influence list
that contains index (because a change to index changes the backlinks).
+ Note that this is true even if the backlink currently fails.
* The pagespec "link(done)" has an influence list that
contains every page that it matches. A change to any matching page can
remove a link and make it not match any more, and so the list is needed
due to the removal problem.
+ >> Why doesn't this include every page? If I change a page that doesn't have a link to
+ >> 'done' to include a link to 'done', then it will now match... or is that considered a
+ >> 'direct match'? -- [[Will]]
+
+ >>> The regular dependency calculation code will check if every changed
+ >>> page matches every dependency. So it will notice the link was added.
+ >>> --[[Joey]]
+
#### Low-level Calculation
One way to calculate a pagespec's influence would be to
onto the influence list. If we combine the influences from each
successful match, we get the right result.
- > This is implemented, seems to work ok. --[[Joey]]
+ > This is implemented, seems to work ok. --[[Joey]]
- #### High-level Calculation and Storage
+ > `or` short-circuits too, but the implementation correctly uses `|`,
+ > which I assume is what you meant. --[[smcv]]
- Calculating the full influence list for a pagespec requires trying to match
- it against every page in the wiki.
+ >> Er, yeah. --[[Joey]]
- I'd like to avoid doing such expensive matching redundantly. So add a
- `pagespec_match_all`, which returns a list of all pages in the whole
- wiki that match the pagespec, and also adds the pagespec as a dependency,
- and while it's at it, calculates and stores the influence list.
+ ----
+
+ What about: "!link(done)"
+
+ Specifically, I want to make sure it works now that I've changed
+ `match_link` to only return a page as an influence if it *does*
+ link to done.
+
+ So, when matching against page P, that does not link to done,
+ there are no influences, and the pagespec matches. If P is later
+ changed to add a link to done, then the dependency resolver will directly
+ notice that.
+
+ When matching against page P, that does link to done, P
+ is an influence, and the pagespec does not match. If P is later changed
+ to not link to done, the influence will do its job.
+
+ Looks good!
+
+ ----
+
+ Here is a case where this approach has some false positives.
+
+ "bugs/* and link(patch)"
+
+ This finds as influences all pages that link to patch, even
+ if they are not under bugs/, and so can never match.
+
+ To fix this, the influence calculation would need to consider boolean
+ operators. Currently, this turns into roughly:
+
+ `FailReason() & SuccessReason(patch)`
+
+ Let's say that the glob instead returns a HardFailReason, which when
+ ANDed with another object, drops their influences. (But when ORed, combines
+ them.) Fixes the above, but does it always work?
+
+ "(bugs/* or link(patch)) and backlink(index)" =>
+ `( HardFailReason() | SuccessReason(page) ) & SuccessReason(index)`` =>
+ `SuccessReason(page & SuccessReason(index)` =>
+ SuccessReason(page, index) => right
+
+ "(bugs/* and link(patch)) or backlink(index)" =>
+ `( HardFailReason() & SuccessReason(page) ) | SuccessReason(index)`` =>
+ `HardFailReason() | SuccessReason(index)` =>
+ `SuccessReason(index)` => right
+
+ "!bugs/* and link(patch)" =>
+ `HardFailReason() | SuccessReason(bugs/foo)` =>
+ `HardFailReason()` => right
+
+ #### High-level Calculation and Storage
+
+ Naively calculating the full influence list for a pagespec requires trying
+ to match it against every page in the wiki. I'd like to avoid doing such
+ expensive matching redundantly.
+
+ It may be possible, for some types of pagespecs, to just try matching a
+ single, arbitrary page against it, and know the full influence list has
+ been obtained. It seems to be that case that if a pagespec has any
+ influences, matching any page will return at least one. So if none are
+ returned, we can skip trying other pages.
+
+ If the influence list does not include the page that was tried, we know
+ that the pagespec does not things like `link()` and `title()`, that are
+ influenced by the page's own content. So it *might* be safe to not try
+ matching any more pages in this case too. I think it would work for all
+ current pagespec terms. There might be a hypothetical term where this
+ optimisation doesn't work. We could add a special case to ensure it can
+ work: If a term declares it is unfluenced by "", then it means it is
+ always influenced by the matching page.
+
+ Anyway, this seems worth doing: Add a `pagespec_match_all`, which returns a
+ list of all pages in the whole wiki that match the pagespec, and also adds
+ the pagespec as a dependency, and while it's at it, calculates and stores
+ the influence list.
It could have an optional sort parameter, and limit parameter, to control
how many items to return and the sort order. So when inline wants to
> I've implemented influence calculation in `add_depends`. As expected,
> it means rather a lot more work, and makes some things much slower.
- > Optimisation via `pagespec_match_depends` next.. --[[Joey]]
+ > Optimisations next.. --[[Joey]]
#### Influence types
For example, "backlink(foo)" has an influence of foo, of type links.
"created_before(foo)" also is influenced by foo, but it's a presence
type. Etc.
+
+ > This is an interesting concept that I hadn't considered. It might
+ > allow significant computational savings, but I suspect will be tricky
+ > to implement. -- [[Will]]
+
+ >> It was actually really easy to implement it, assuming I picked the right
+ >> dependency types of course. --[[Joey]]