Merge remote branch 'smcv/ready/link-types'
authorJoey Hess <joey@gnu.kitenet.net>
Wed, 7 Apr 2010 02:50:19 +0000 (22:50 -0400)
committerJoey Hess <joey@gnu.kitenet.net>
Wed, 7 Apr 2010 02:50:19 +0000 (22:50 -0400)
IkiWiki.pm
IkiWiki/Plugin/tag.pm
IkiWiki/Render.pm
debian/NEWS
debian/postinst
doc/bugs/tagged__40____41___matching_wikilinks.mdwn
doc/plugins/write.mdwn
t/calculate_changed_links.t [new file with mode: 0755]
t/index.t
t/tag.t [new file with mode: 0755]

index 0cbc847887b1b6d8493268e41721a7f8d8784a32..2415307d456b1f0b2aabcdaa569666e33337475c 100644 (file)
@@ -14,7 +14,7 @@ use open qw{:utf8 :std};
 use vars qw{%config %links %oldlinks %pagemtime %pagectime %pagecase
            %pagestate %wikistate %renderedfiles %oldrenderedfiles
            %pagesources %destsources %depends %depends_simple %hooks
-           %forcerebuild %loaded_plugins};
+           %forcerebuild %loaded_plugins %typedlinks %oldtypedlinks};
 
 use Exporter q{import};
 our @EXPORT = qw(hook debug error template htmlpage deptype
@@ -24,7 +24,7 @@ our @EXPORT = qw(hook debug error template htmlpage deptype
                 add_underlay pagetitle titlepage linkpage newpagefile
                 inject add_link
                  %config %links %pagestate %wikistate %renderedfiles
-                 %pagesources %destsources);
+                 %pagesources %destsources %typedlinks);
 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
@@ -1502,7 +1502,7 @@ sub loadindex () {
        if (! $config{rebuild}) {
                %pagesources=%pagemtime=%oldlinks=%links=%depends=
                %destsources=%renderedfiles=%pagecase=%pagestate=
-               %depends_simple=();
+               %depends_simple=%typedlinks=%oldtypedlinks=();
        }
        my $in;
        if (! open ($in, "<", "$config{wikistatedir}/indexdb")) {
@@ -1568,6 +1568,14 @@ sub loadindex () {
                        if (exists $d->{state}) {
                                $pagestate{$page}=$d->{state};
                        }
+                       if (exists $d->{typedlinks}) {
+                               $typedlinks{$page}=$d->{typedlinks};
+
+                               while (my ($type, $links) = each %{$typedlinks{$page}}) {
+                                       next unless %$links;
+                                       $oldtypedlinks{$page}{$type} = {%$links};
+                               }
+                       }
                }
                $oldrenderedfiles{$page}=[@{$d->{dest}}];
        }
@@ -1616,6 +1624,10 @@ sub saveindex () {
                        $index{page}{$src}{depends_simple} = $depends_simple{$page};
                }
 
+               if (exists $typedlinks{$page} && %{$typedlinks{$page}}) {
+                       $index{page}{$src}{typedlinks} = $typedlinks{$page};
+               }
+
                if (exists $pagestate{$page}) {
                        foreach my $id (@hookids) {
                                foreach my $key (keys %{$pagestate{$page}{$id}}) {
@@ -1925,12 +1937,17 @@ sub inject {
        use warnings;
 }
 
-sub add_link ($$) {
+sub add_link ($$;$) {
        my $page=shift;
        my $link=shift;
+       my $type=shift;
 
        push @{$links{$page}}, $link
                unless grep { $_ eq $link } @{$links{$page}};
+
+       if (defined $type) {
+               $typedlinks{$page}{$type}{$link} = 1;
+       }
 }
 
 sub pagespec_translate ($) {
@@ -2211,6 +2228,11 @@ sub match_link ($$;@) {
 
        $link=derel($link, $params{location});
        my $from=exists $params{location} ? $params{location} : '';
+       my $linktype=$params{linktype};
+       my $qualifier='';
+       if (defined $linktype) {
+               $qualifier=" with type $linktype";
+       }
 
        my $links = $IkiWiki::links{$page};
        return IkiWiki::FailReason->new("$page has no links", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
@@ -2218,19 +2240,22 @@ sub match_link ($$;@) {
        my $bestlink = IkiWiki::bestlink($from, $link);
        foreach my $p (@{$links}) {
                if (length $bestlink) {
-                       return IkiWiki::SuccessReason->new("$page links to $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
-                               if $bestlink eq IkiWiki::bestlink($page, $p);
+                       if ((!defined $linktype || exists $IkiWiki::typedlinks{$page}{$linktype}{$p}) && $bestlink eq IkiWiki::bestlink($page, $p)) {
+                               return IkiWiki::SuccessReason->new("$page links to $link$qualifier", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
+                       }
                }
                else {
-                       return IkiWiki::SuccessReason->new("$page links to page $p matching $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
-                               if match_glob($p, $link, %params);
+                       if ((!defined $linktype || exists $IkiWiki::typedlinks{$page}{$linktype}{$p}) && match_glob($p, $link, %params)) {
+                               return IkiWiki::SuccessReason->new("$page links to page $p$qualifier, matching $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
+                       }
                        my ($p_rel)=$p=~/^\/?(.*)/;
                        $link=~s/^\///;
-                       return IkiWiki::SuccessReason->new("$page links to page $p_rel matching $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
-                               if match_glob($p_rel, $link, %params);
+                       if ((!defined $linktype || exists $IkiWiki::typedlinks{$page}{$linktype}{$p_rel}) && match_glob($p_rel, $link, %params)) {
+                               return IkiWiki::SuccessReason->new("$page links to page $p_rel$qualifier, matching $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
+                       }
                }
        }
-       return IkiWiki::FailReason->new("$page does not link to $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1);
+       return IkiWiki::FailReason->new("$page does not link to $link$qualifier", $page => $IkiWiki::DEPEND_LINKS, "" => 1);
 }
 
 sub match_backlink ($$;@) {
index cdcfaf536082825444b3e8d3f27b406bc413cd53..7a85874f6e928e11e85ec57aee279b0d8197687c 100644 (file)
@@ -6,8 +6,6 @@ use warnings;
 use strict;
 use IkiWiki 3.00;
 
-my %tags;
-
 sub import {
        hook(type => "getopt", id => "tag", call => \&getopt);
        hook(type => "getsetup", id => "tag", call => \&getsetup);
@@ -71,9 +69,8 @@ sub preprocess_tag (@) {
 
        foreach my $tag (keys %params) {
                $tag=linkpage($tag);
-               $tags{$page}{$tag}=1;
                # hidden WikiLink
-               add_link($page, tagpage($tag));
+               add_link($page, tagpage($tag), 'tag');
        }
                
        return "";
@@ -87,15 +84,13 @@ sub preprocess_taglink (@) {
        return join(" ", map {
                if (/(.*)\|(.*)/) {
                        my $tag=linkpage($2);
-                       $tags{$params{page}}{$tag}=1;
-                       add_link($params{page}, tagpage($tag));
+                       add_link($params{page}, tagpage($tag), 'tag');
                        return taglink($params{page}, $params{destpage}, $tag,
                                linktext => pagetitle($1));
                }
                else {
                        my $tag=linkpage($_);
-                       $tags{$params{page}}{$tag}=1;
-                       add_link($params{page}, tagpage($tag));
+                       add_link($params{page}, tagpage($tag), 'tag');
                        return taglink($params{page}, $params{destpage}, $tag);
                }
        }
@@ -110,17 +105,19 @@ sub pagetemplate (@) {
        my $destpage=$params{destpage};
        my $template=$params{template};
 
+       my $tags = $typedlinks{$page}{tag};
+
        $template->param(tags => [
                map { 
                        link => taglink($page, $destpage, $_, rel => "tag")
-               }, sort keys %{$tags{$page}}
-       ]) if exists $tags{$page} && %{$tags{$page}} && $template->query(name => "tags");
+               }, sort keys %$tags
+       ]) if defined $tags && %$tags && $template->query(name => "tags");
 
        if ($template->query(name => "categories")) {
                # It's an rss/atom template. Add any categories.
-               if (exists $tags{$page} && %{$tags{$page}}) {
+               if (defined $tags && %$tags) {
                        $template->param(categories => [map { category => $_ },
-                               sort keys %{$tags{$page}}]);
+                               sort keys %$tags]);
                }
        }
 }
@@ -128,9 +125,7 @@ sub pagetemplate (@) {
 package IkiWiki::PageSpec;
 
 sub match_tagged ($$;@) {
-       my $page = shift;
-       my $glob = shift;
-       return match_link($page, IkiWiki::Plugin::tag::tagpage($glob));
+       return match_link($_[0], IkiWiki::Plugin::tag::tagpage($_[1]), linktype => 'tag');
 }
 
 1
index abafb0887f32b0a6fcc25115b6ecb6be63bc4d6a..e98888d76ecd17709e054b3a7068e0c439341a8a 100644 (file)
@@ -167,6 +167,7 @@ sub scan ($) {
                else {
                        $links{$page}=[];
                }
+               delete $typedlinks{$page};
 
                run_hooks(scan => sub {
                        shift->(
@@ -398,6 +399,7 @@ sub find_del_files ($) {
                                push @del, $pagesources{$page};
                        }
                        $links{$page}=[];
+                       delete $typedlinks{$page};
                        $renderedfiles{$page}=[];
                        $pagemtime{$page}=0;
                }
@@ -499,6 +501,29 @@ sub remove_unrendered () {
        }
 }
 
+sub link_types_changed ($$) {
+       # each is of the form { type => { link => 1 } }
+       my $new = shift;
+       my $old = shift;
+
+       return 0 if !defined $new && !defined $old;
+       return 1 if !defined $new || !defined $old;
+
+       while (my ($type, $links) = each %$new) {
+               foreach my $link (keys %$links) {
+                       return 1 unless exists $old->{$type}{$link};
+               }
+       }
+
+       while (my ($type, $links) = each %$old) {
+               foreach my $link (keys %$links) {
+                       return 1 unless exists $new->{$type}{$link};
+               }
+       }
+
+       return 0;
+}
+
 sub calculate_changed_links ($$$) {
        my ($changed, $del, $oldlink_targets)=@_;
 
@@ -525,6 +550,14 @@ sub calculate_changed_links ($$$) {
                        }
                        $linkchangers{lc($page)}=1;
                }
+
+               # we currently assume that changing the type of a link doesn't
+               # change backlinks
+               if (!exists $linkchangers{lc($page)}) {
+                       if (link_types_changed($typedlinks{$page}, $oldtypedlinks{$page})) {
+                               $linkchangers{lc($page)}=1;
+                       }
+               }
        }
 
        return \%backlinkchanged, \%linkchangers;
index 50332670ff96bf3e0b36f6073835e8af831f6da0..afb3a86524e4e5a246c93ec6994738a57b736967 100644 (file)
@@ -1,3 +1,13 @@
+ikiwiki (3.UNRELEASED) UNRELEASED; urgency=low
+
+  Starting from this version, the tagged(x) pagespec only matches links that
+  were set up by the [[!tag]] or [[!taglink]] directives. To make this change,
+  all wikis need to be rebuilt on upgrade to this version. If you listed your
+  wiki in /etc/ikiwiki/wikilist this will be done automatically when the
+  Debian package is upgraded. Or use ikiwiki-mass-rebuild to force a rebuild.
+
+ -- Simon McVittie <smcv@debian.org>  Tue, 06 Apr 2010 20:53:07 +0100
+
 ikiwiki (3.20091017) unstable; urgency=low
 
   To take advantage of significant performance improvements, all
index bf1825ab7b1ca5239fe1e831d44e2dd08dcafbf5..90b9e8a421dd0e8d1a89d0eee173df1c9417624f 100755 (executable)
@@ -4,7 +4,7 @@ set -e
 
 # Change this when some incompatible change is made that requires
 # rebuilding all wikis.
-firstcompat=3.20091010
+firstcompat=3.20100401
 
 if [ "$1" = configure ] && \
    dpkg --compare-versions "$2" lt "$firstcompat"; then
index e7e4af7c3267b155b0b45141968746d08b5aa79a..a211654f14337f4a4fb96002325cf66da2961b1f 100644 (file)
@@ -28,6 +28,8 @@ rationale on this, or what am I doing wrong, and how to achieve what I want?
 >> is valid. [[todo/matching_different_kinds_of_links]] is probably
 >> how it will eventually be solved. --[[Joey]] 
 
+>>> [[Done]]: `tagged` no longer matches other wikilinks. --[[smcv]]
+
 > And this is an illustration why a clean work-around (without changing the software) is not possible: while thinking about [[todo/matching_different_kinds_of_links]], I thought one could work around the problem by simply explicitly including the kind of the relation into the link target (like the tagbase in tags), and by having a separate page without the "tagbase" to link to when one wants simply to refer to the tag without tagging. But this won't work: one has to at least once refer to the real tag page if one wants to talk about it, and this reference will count as tagging (unwanted). --Ivan Z.
 
 > But well, perhaps there is a workaround without introducing different kinds of links. One could modify the [[tag plugin|plugins/tag]] so that it adds 2 links to a page: for tagging -- `tagbase/TAG`, and for navigation -- `tagdescription/TAG` (displayed at the bottom). Then the `tagdescription/TAG` page would hold whatever list one wishes (with `tagged(TAG)` in the pagespec), and whenever one wants to merely refer to the tag, one should link to `tagdescription/TAG`--this link won't count as tagging. So, `tagbase/TAG` would become completely auxiliary (internal) link targets for ikiwiki, the users would edit or link to only `tagdescription/TAG`. --Ivan Z.
index 96a2aa16d02c84697072f71f51ff63226e29be8b..fe7cf0183bc4232750fe71a2cde3fb9d9a601bb8 100644 (file)
@@ -633,6 +633,22 @@ reference. Do not modify this hash directly; call `add_link()`.
 
        $links{"foo"} = ["bar", "baz"];
 
+### `%typedlinks`
+
+The `%typedlinks` hash records links of specific types. Do not modify this
+hash directly; call `add_link()`. The keys are page names, and the values
+are hash references. In each page's hash reference, the keys are link types
+defined by plugins, and the values are hash references with link targets
+as keys, and 1 as a dummy value, something like this:
+
+       $typedlinks{"foo"} = {
+               tag => { short_word => 1, metasyntactic_variable => 1 },
+               next_page => { bar => 1 },
+       };
+
+Ordinary [[WikiLinks|ikiwiki/WikiLink]] appear in `%links`, but not in
+`%typedlinks`.
+
 ### `%pagesources`
 
 The `%pagesources` has can be used to look up the source filename
@@ -939,11 +955,14 @@ Optionally, a third parameter can be passed, to specify the preferred
 filename of the page. For example, `targetpage("foo", "rss", "feed")`
 will yield something like `foo/feed.rss`.
 
-### `add_link($$)`
+### `add_link($$;$)`
 
 This adds a link to `%links`, ensuring that duplicate links are not
 added. Pass it the page that contains the link, and the link text.
 
+An optional third parameter sets the link type (`undef` produces an ordinary
+[[ikiwiki/WikiLink]]).
+
 ## Miscellaneous
 
 ### Internal use pages
diff --git a/t/calculate_changed_links.t b/t/calculate_changed_links.t
new file mode 100755 (executable)
index 0000000..bf6e2af
--- /dev/null
@@ -0,0 +1,58 @@
+#!/usr/bin/perl
+package IkiWiki;
+
+use warnings;
+use strict;
+use Test::More tests => 5;
+
+BEGIN { use_ok("IkiWiki"); }
+BEGIN { use_ok("IkiWiki::Render"); }
+%config=IkiWiki::defaultconfig();
+$config{srcdir}=$config{destdir}="/dev/null";
+
+%oldrenderedfiles=%pagectime=();
+%pagesources=%pagemtime=%oldlinks=%links=%depends=%typedlinks=%oldtypedlinks=
+%destsources=%renderedfiles=%pagecase=%pagestate=();
+
+IkiWiki::checkconfig();
+
+foreach my $page (qw(tags/a tags/b Reorder Add Remove TypeAdd TypeRemove)) {
+       $pagesources{$page} = "$page.mdwn";
+       $pagemtime{$page} = $pagectime{$page} = 1000000;
+}
+
+$oldlinks{Reorder} = [qw{tags/a tags/b}];
+$links{Reorder} = [qw{tags/b tags/a}];
+
+$oldlinks{Add} = [qw{tags/b}];
+$links{Add} = [qw{tags/a tags/b}];
+
+$oldlinks{Remove} = [qw{tags/a}];
+$links{Remove} = [];
+
+$oldlinks{TypeAdd} = [qw{tags/a tags/b}];
+$links{TypeAdd} = [qw{tags/a tags/b}];
+# This causes TypeAdd to be rebuilt, but isn't a backlink change, so it doesn't
+# cause tags/b to be rebuilt.
+$oldtypedlinks{TypeAdd}{tag} = { "tags/a" => 1 };
+$typedlinks{TypeAdd}{tag} = { "tags/a" => 1, "tags/b" => 1 };
+
+$oldlinks{TypeRemove} = [qw{tags/a tags/b}];
+$links{TypeRemove} = [qw{tags/a tags/b}];
+# This causes TypeRemove to be rebuilt, but isn't a backlink change, so it
+# doesn't cause tags/b to be rebuilt.
+$oldtypedlinks{TypeRemove}{tag} = { "tags/a" => 1 };
+$typedlinks{TypeRemove}{tag} = { "tags/a" => 1, "tags/b" => 1 };
+
+my $oldlink_targets = calculate_old_links([keys %pagesources], []);
+is_deeply($oldlink_targets, {
+               Reorder => { "tags/a" => "tags/a", "tags/b" => "tags/b" },
+               Add => { "tags/b" => "tags/b" },
+               Remove => { "tags/a" => "tags/a" },
+               TypeAdd => { "tags/a" => "tags/a", "tags/b" => "tags/b" },
+               TypeRemove => { "tags/a" => "tags/a", "tags/b" => "tags/b" },
+       });
+my ($backlinkchanged, $linkchangers) = calculate_changed_links([keys %pagesources], [], $oldlink_targets);
+
+is_deeply($backlinkchanged, { "tags/a" => 1 });
+is_deeply($linkchangers, { add => 1, remove => 1, typeadd => 1, typeremove => 1 });
index 2f23524a7cc987cf82329a2035370840d36ee3ef..44273059dcad28262b35c788bfe22f00690e8947 100755 (executable)
--- a/t/index.t
+++ b/t/index.t
@@ -4,7 +4,7 @@ use strict;
 use IkiWiki;
 
 package IkiWiki; # use internal variables
-use Test::More tests => 27;
+use Test::More tests => 31;
 
 $config{wikistatedir}="/tmp/ikiwiki-test.$$";
 system "rm -rf $config{wikistatedir}";
@@ -31,6 +31,7 @@ $renderedfiles{"bar"}=["bar.html", "bar.rss", "sparkline-foo.gif"];
 $renderedfiles{"bar.png"}=["bar.png"];
 $links{"Foo"}=["bar.png"];
 $links{"bar"}=["Foo", "new-page"];
+$typedlinks{"bar"}={tag => {"Foo" => 1}};
 $links{"bar.png"}=[];
 $depends{"Foo"}={};
 $depends{"bar"}={"foo*" => 1};
@@ -45,7 +46,7 @@ ok(-s "$config{wikistatedir}/indexdb", "index file created");
 
 # Clear state.
 %oldrenderedfiles=%pagectime=();
-%pagesources=%pagemtime=%oldlinks=%links=%depends=
+%pagesources=%pagemtime=%oldlinks=%links=%depends=%typedlinks=%oldtypedlinks=
 %destsources=%renderedfiles=%pagecase=%pagestate=();
 
 ok(loadindex(), "load index");
@@ -104,10 +105,16 @@ is_deeply(\%destsources, {
        "sparkline-foo.gif" => "bar",
        "bar.png" => "bar.png",
 }, "%destsources generated correctly");
+is_deeply(\%typedlinks, {
+       bar => {tag => {"Foo" => 1}},
+}, "%typedlinks loaded correctly");
+is_deeply(\%oldtypedlinks, {
+       bar => {tag => {"Foo" => 1}},
+}, "%oldtypedlinks loaded correctly");
 
 # Clear state.
 %oldrenderedfiles=%pagectime=();
-%pagesources=%pagemtime=%oldlinks=%links=%depends=
+%pagesources=%pagemtime=%oldlinks=%links=%depends=%typedlinks=%oldtypedlinks=
 %destsources=%renderedfiles=%pagecase=%pagestate=();
 
 # When state is loaded for a wiki rebuild, only ctime and oldrenderedfiles
@@ -140,5 +147,9 @@ is_deeply(\%pagecase, {
 }, "%pagecase generated correctly");
 is_deeply(\%destsources, {
 }, "%destsources generated correctly");
+is_deeply(\%typedlinks, {
+}, "%typedlinks cleared correctly");
+is_deeply(\%oldtypedlinks, {
+}, "%oldtypedlinks cleared correctly");
 
 system "rm -rf $config{wikistatedir}";
diff --git a/t/tag.t b/t/tag.t
new file mode 100755 (executable)
index 0000000..cf3bbdf
--- /dev/null
+++ b/t/tag.t
@@ -0,0 +1,44 @@
+#!/usr/bin/perl
+package IkiWiki;
+
+use warnings;
+use strict;
+use Test::More tests => 10;
+
+BEGIN { use_ok("IkiWiki"); }
+BEGIN { use_ok("IkiWiki::Plugin::tag"); }
+
+ok(! system("rm -rf t/tmp; mkdir t/tmp"));
+
+$config{userdir} = "users";
+$config{tagbase} = "tags";
+
+%oldrenderedfiles=%pagectime=();
+%pagesources=%pagemtime=%oldlinks=%links=%depends=%typedlinks=%oldtypedlinks=
+%destsources=%renderedfiles=%pagecase=%pagestate=();
+
+foreach my $page (qw(tags/numbers tags/letters one two alpha beta)) {
+       $pagesources{$page} = "$page.mdwn";
+       $pagemtime{$page} = $pagectime{$page} = 1000000;
+}
+
+$links{one}=[qw(tags/numbers alpha tags/letters)];
+$links{two}=[qw(tags/numbers)];
+$links{alpha}=[qw(tags/letters one)];
+$links{beta}=[qw(tags/letters)];
+$typedlinks{one}={tag => {"tags/numbers" => 1 }};
+$typedlinks{two}={tag => {"tags/numbers" => 1 }};
+$typedlinks{alpha}={tag => {"tags/letters" => 1 }};
+$typedlinks{beta}={tag => {"tags/letters" => 1 }};
+
+ok(pagespec_match("one", "tagged(numbers)"));
+ok(!pagespec_match("two", "tagged(alpha)"));
+ok(pagespec_match("one", "link(tags/numbers)"));
+ok(pagespec_match("one", "link(alpha)"));
+
+ok(pagespec_match("one", "typedlink(tag tags/numbers)"));
+ok(!pagespec_match("one", "typedlink(tag tags/letters)"));
+# invalid syntax
+ok(pagespec_match("one", "typedlink(tag)")->isa("IkiWiki::ErrorReason"));
+
+1;