implement typed links; add tagged_is_strict config option

master
Simon McVittie 2010-04-02 00:28:02 +01:00
parent 05b6e8ceee
commit c1a42e76bc
9 changed files with 193 additions and 28 deletions

View File

@ -14,7 +14,7 @@ use open qw{:utf8 :std};
use vars qw{%config %links %oldlinks %pagemtime %pagectime %pagecase use vars qw{%config %links %oldlinks %pagemtime %pagectime %pagecase
%pagestate %wikistate %renderedfiles %oldrenderedfiles %pagestate %wikistate %renderedfiles %oldrenderedfiles
%pagesources %destsources %depends %depends_simple %hooks %pagesources %destsources %depends %depends_simple %hooks
%forcerebuild %loaded_plugins}; %forcerebuild %loaded_plugins %typedlinks %oldtypedlinks};
use Exporter q{import}; use Exporter q{import};
our @EXPORT = qw(hook debug error template htmlpage deptype 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 add_underlay pagetitle titlepage linkpage newpagefile
inject add_link inject add_link
%config %links %pagestate %wikistate %renderedfiles %config %links %pagestate %wikistate %renderedfiles
%pagesources %destsources); %pagesources %destsources %typedlinks);
our $VERSION = 3.00; # plugin interface version, next is ikiwiki version our $VERSION = 3.00; # plugin interface version, next is ikiwiki version
our $version='unknown'; # VERSION_AUTOREPLACE done by Makefile, DNE our $version='unknown'; # VERSION_AUTOREPLACE done by Makefile, DNE
our $installdir='/usr'; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE our $installdir='/usr'; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE
@ -1503,7 +1503,7 @@ sub loadindex () {
if (! $config{rebuild}) { if (! $config{rebuild}) {
%pagesources=%pagemtime=%oldlinks=%links=%depends= %pagesources=%pagemtime=%oldlinks=%links=%depends=
%destsources=%renderedfiles=%pagecase=%pagestate= %destsources=%renderedfiles=%pagecase=%pagestate=
%depends_simple=(); %depends_simple=%typedlinks=%oldtypedlinks=();
} }
my $in; my $in;
if (! open ($in, "<", "$config{wikistatedir}/indexdb")) { if (! open ($in, "<", "$config{wikistatedir}/indexdb")) {
@ -1569,6 +1569,14 @@ sub loadindex () {
if (exists $d->{state}) { if (exists $d->{state}) {
$pagestate{$page}=$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}}]; $oldrenderedfiles{$page}=[@{$d->{dest}}];
} }
@ -1617,6 +1625,10 @@ sub saveindex () {
$index{page}{$src}{depends_simple} = $depends_simple{$page}; $index{page}{$src}{depends_simple} = $depends_simple{$page};
} }
if (exists $typedlinks{$page} && %{$typedlinks{$page}}) {
$index{page}{$src}{typedlinks} = $typedlinks{$page};
}
if (exists $pagestate{$page}) { if (exists $pagestate{$page}) {
foreach my $id (@hookids) { foreach my $id (@hookids) {
foreach my $key (keys %{$pagestate{$page}{$id}}) { foreach my $key (keys %{$pagestate{$page}{$id}}) {
@ -1926,12 +1938,17 @@ sub inject {
use warnings; use warnings;
} }
sub add_link ($$) { sub add_link ($$;$) {
my $page=shift; my $page=shift;
my $link=shift; my $link=shift;
my $type=shift;
push @{$links{$page}}, $link push @{$links{$page}}, $link
unless grep { $_ eq $link } @{$links{$page}}; unless grep { $_ eq $link } @{$links{$page}};
if (defined $type) {
$typedlinks{$page}{$type}{$link} = 1;
}
} }
sub pagespec_translate ($) { sub pagespec_translate ($) {
@ -2212,6 +2229,11 @@ sub match_link ($$;@) {
$link=derel($link, $params{location}); $link=derel($link, $params{location});
my $from=exists $params{location} ? $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}; my $links = $IkiWiki::links{$page};
return IkiWiki::FailReason->new("$page has no links", $page => $IkiWiki::DEPEND_LINKS, "" => 1) return IkiWiki::FailReason->new("$page has no links", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
@ -2219,19 +2241,33 @@ sub match_link ($$;@) {
my $bestlink = IkiWiki::bestlink($from, $link); my $bestlink = IkiWiki::bestlink($from, $link);
foreach my $p (@{$links}) { foreach my $p (@{$links}) {
if (length $bestlink) { if (length $bestlink) {
return IkiWiki::SuccessReason->new("$page links to $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1) if ((!defined $linktype || exists $IkiWiki::typedlinks{$page}{$linktype}{$p}) && $bestlink eq IkiWiki::bestlink($page, $p)) {
if $bestlink eq IkiWiki::bestlink($page, $p); return IkiWiki::SuccessReason->new("$page links to $link$qualifier", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
}
} }
else { else {
return IkiWiki::SuccessReason->new("$page links to page $p matching $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1) if ((!defined $linktype || exists $IkiWiki::typedlinks{$page}{$linktype}{$p}) && match_glob($p, $link, %params)) {
if 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=~/^\/?(.*)/; my ($p_rel)=$p=~/^\/?(.*)/;
$link=~s/^\///; $link=~s/^\///;
return IkiWiki::SuccessReason->new("$page links to page $p_rel matching $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1) if ((!defined $linktype || exists $IkiWiki::typedlinks{$page}{$linktype}{$p_rel}) && match_glob($p_rel, $link, %params)) {
if 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_typedlink($$;@) {
my $page = shift;
my $args = shift;
if ($args =~ /^(\w+)\s+(.*)$/) {
return match_link($page, $2, @_, linktype => $1);
}
return IkiWiki::ErrorReason->new("typedlink expects e.g. 'tag *' but got: $args");
} }
sub match_backlink ($$;@) { sub match_backlink ($$;@) {

View File

@ -6,8 +6,6 @@ use warnings;
use strict; use strict;
use IkiWiki 3.00; use IkiWiki 3.00;
my %tags;
sub import { sub import {
hook(type => "getopt", id => "tag", call => \&getopt); hook(type => "getopt", id => "tag", call => \&getopt);
hook(type => "getsetup", id => "tag", call => \&getsetup); hook(type => "getsetup", id => "tag", call => \&getsetup);
@ -36,6 +34,13 @@ sub getsetup () {
safe => 1, safe => 1,
rebuild => 1, rebuild => 1,
}, },
tagged_is_strict => {
type => "boolean",
default => 0,
description => "if 1, tagged() doesn't match normal WikiLinks to tag pages",
safe => 1,
rebuild => 1,
},
} }
sub tagpage ($) { sub tagpage ($) {
@ -71,9 +76,8 @@ sub preprocess_tag (@) {
foreach my $tag (keys %params) { foreach my $tag (keys %params) {
$tag=linkpage($tag); $tag=linkpage($tag);
$tags{$page}{$tag}=1;
# hidden WikiLink # hidden WikiLink
add_link($page, tagpage($tag)); add_link($page, tagpage($tag), 'tag');
} }
return ""; return "";
@ -87,15 +91,13 @@ sub preprocess_taglink (@) {
return join(" ", map { return join(" ", map {
if (/(.*)\|(.*)/) { if (/(.*)\|(.*)/) {
my $tag=linkpage($2); my $tag=linkpage($2);
$tags{$params{page}}{$tag}=1; add_link($params{page}, tagpage($tag), 'tag');
add_link($params{page}, tagpage($tag));
return taglink($params{page}, $params{destpage}, $tag, return taglink($params{page}, $params{destpage}, $tag,
linktext => pagetitle($1)); linktext => pagetitle($1));
} }
else { else {
my $tag=linkpage($_); my $tag=linkpage($_);
$tags{$params{page}}{$tag}=1; add_link($params{page}, tagpage($tag), 'tag');
add_link($params{page}, tagpage($tag));
return taglink($params{page}, $params{destpage}, $tag); return taglink($params{page}, $params{destpage}, $tag);
} }
} }
@ -110,17 +112,19 @@ sub pagetemplate (@) {
my $destpage=$params{destpage}; my $destpage=$params{destpage};
my $template=$params{template}; my $template=$params{template};
my $tags = $typedlinks{$page}{tag};
$template->param(tags => [ $template->param(tags => [
map { map {
link => taglink($page, $destpage, $_, rel => "tag") link => taglink($page, $destpage, $_, rel => "tag")
}, sort keys %{$tags{$page}} }, sort keys %$tags
]) if exists $tags{$page} && %{$tags{$page}} && $template->query(name => "tags"); ]) if defined $tags && %$tags && $template->query(name => "tags");
if ($template->query(name => "categories")) { if ($template->query(name => "categories")) {
# It's an rss/atom template. Add any 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 => $_ }, $template->param(categories => [map { category => $_ },
sort keys %{$tags{$page}}]); sort keys %$tags]);
} }
} }
} }
@ -130,7 +134,13 @@ package IkiWiki::PageSpec;
sub match_tagged ($$;@) { sub match_tagged ($$;@) {
my $page = shift; my $page = shift;
my $glob = shift; my $glob = shift;
return match_link($page, IkiWiki::Plugin::tag::tagpage($glob));
if ($IkiWiki::config{tagged_is_strict}) {
return match_link($page, IkiWiki::Plugin::tag::tagpage($glob), linktype => 'tag');
}
else {
return match_link($page, IkiWiki::Plugin::tag::tagpage($glob));
}
} }
1 1

View File

@ -167,6 +167,7 @@ sub scan ($) {
else { else {
$links{$page}=[]; $links{$page}=[];
} }
delete $typedlinks{$page};
run_hooks(scan => sub { run_hooks(scan => sub {
shift->( shift->(
@ -398,6 +399,7 @@ sub find_del_files ($) {
push @del, $pagesources{$page}; push @del, $pagesources{$page};
} }
$links{$page}=[]; $links{$page}=[];
delete $typedlinks{$page};
$renderedfiles{$page}=[]; $renderedfiles{$page}=[];
$pagemtime{$page}=0; $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 ($$$) { sub calculate_changed_links ($$$) {
my ($changed, $del, $oldlink_targets)=@_; my ($changed, $del, $oldlink_targets)=@_;
@ -525,6 +550,14 @@ sub calculate_changed_links ($$$) {
} }
$linkchangers{lc($page)}=1; $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}, $oldlinktypes{$page})) {
$linkchangers{lc($page)}=1;
}
}
} }
return \%backlinkchanged, \%linkchangers; return \%backlinkchanged, \%linkchangers;

View File

@ -28,6 +28,9 @@ 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 >> is valid. [[todo/matching_different_kinds_of_links]] is probably
>> how it will eventually be solved. --[[Joey]] >> how it will eventually be solved. --[[Joey]]
>>> [[Done]]: you can now set the `tagged_is_strict` config option to `1`
>>> if you don't want `tagged` to match 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. > 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. > 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.

View File

@ -52,6 +52,9 @@ Some more elaborate limits can be added to what matches using these functions:
specified IP address. specified IP address.
* "`postcomment(glob)`" - matches only when comments are being * "`postcomment(glob)`" - matches only when comments are being
posted to a page matching the specified glob posted to a page matching the specified glob
* "`typedlink(type glob)`" - matches pages that link to a given page (or glob)
with a given link type. Plugins can create links with a specific type:
for instance, the tag plugin creates links of type `tag`.
For example, to match all pages in a blog that link to the page about music For example, to match all pages in a blog that link to the page about music
and were written in 2005: and were written in 2005:

View File

@ -8,6 +8,11 @@ These directives allow tagging pages.
It also provides the `tagged()` [[ikiwiki/PageSpec]], which can be used to It also provides the `tagged()` [[ikiwiki/PageSpec]], which can be used to
match pages that are tagged with a specific tag. match pages that are tagged with a specific tag.
If the `tagged_is_strict` config option is set, `tagged()` will only match
tags explicitly set with [[ikiwiki/directive/tag]] or
[[ikiwiki/directive/taglink]]; if not (the default), it will also match
any other [[WikiLinks|ikiwiki/WikiLink]] to the tag page.
[[!if test="enabled(tag)" then=""" [[!if test="enabled(tag)" then="""
This wiki has the tag plugin enabled, so you'll see a note below that this This wiki has the tag plugin enabled, so you'll see a note below that this
page is tagged with the "tags" tag. page is tagged with the "tags" tag.

View File

@ -633,6 +633,22 @@ reference. Do not modify this hash directly; call `add_link()`.
$links{"foo"} = ["bar", "baz"]; $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` ### `%pagesources`
The `%pagesources` has can be used to look up the source filename 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")` filename of the page. For example, `targetpage("foo", "rss", "feed")`
will yield something like `foo/feed.rss`. will yield something like `foo/feed.rss`.
### `add_link($$)` ### `add_link($$;$)`
This adds a link to `%links`, ensuring that duplicate links are not 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. 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 ## Miscellaneous
### Internal use pages ### Internal use pages

View File

@ -4,7 +4,7 @@ use strict;
use IkiWiki; use IkiWiki;
package IkiWiki; # use internal variables package IkiWiki; # use internal variables
use Test::More tests => 27; use Test::More tests => 31;
$config{wikistatedir}="/tmp/ikiwiki-test.$$"; $config{wikistatedir}="/tmp/ikiwiki-test.$$";
system "rm -rf $config{wikistatedir}"; system "rm -rf $config{wikistatedir}";
@ -31,6 +31,7 @@ $renderedfiles{"bar"}=["bar.html", "bar.rss", "sparkline-foo.gif"];
$renderedfiles{"bar.png"}=["bar.png"]; $renderedfiles{"bar.png"}=["bar.png"];
$links{"Foo"}=["bar.png"]; $links{"Foo"}=["bar.png"];
$links{"bar"}=["Foo", "new-page"]; $links{"bar"}=["Foo", "new-page"];
$typedlinks{"bar"}={tag => {"Foo" => 1}};
$links{"bar.png"}=[]; $links{"bar.png"}=[];
$depends{"Foo"}={}; $depends{"Foo"}={};
$depends{"bar"}={"foo*" => 1}; $depends{"bar"}={"foo*" => 1};
@ -45,7 +46,7 @@ ok(-s "$config{wikistatedir}/indexdb", "index file created");
# Clear state. # Clear state.
%oldrenderedfiles=%pagectime=(); %oldrenderedfiles=%pagectime=();
%pagesources=%pagemtime=%oldlinks=%links=%depends= %pagesources=%pagemtime=%oldlinks=%links=%depends=%typedlinks=%oldtypedlinks=
%destsources=%renderedfiles=%pagecase=%pagestate=(); %destsources=%renderedfiles=%pagecase=%pagestate=();
ok(loadindex(), "load index"); ok(loadindex(), "load index");
@ -104,10 +105,16 @@ is_deeply(\%destsources, {
"sparkline-foo.gif" => "bar", "sparkline-foo.gif" => "bar",
"bar.png" => "bar.png", "bar.png" => "bar.png",
}, "%destsources generated correctly"); }, "%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. # Clear state.
%oldrenderedfiles=%pagectime=(); %oldrenderedfiles=%pagectime=();
%pagesources=%pagemtime=%oldlinks=%links=%depends= %pagesources=%pagemtime=%oldlinks=%links=%depends=%typedlinks=%oldtypedlinks=
%destsources=%renderedfiles=%pagecase=%pagestate=(); %destsources=%renderedfiles=%pagecase=%pagestate=();
# When state is loaded for a wiki rebuild, only ctime and oldrenderedfiles # When state is loaded for a wiki rebuild, only ctime and oldrenderedfiles
@ -140,5 +147,9 @@ is_deeply(\%pagecase, {
}, "%pagecase generated correctly"); }, "%pagecase generated correctly");
is_deeply(\%destsources, { is_deeply(\%destsources, {
}, "%destsources generated correctly"); }, "%destsources generated correctly");
is_deeply(\%typedlinks, {
}, "%typedlinks cleared correctly");
is_deeply(\%oldtypedlinks, {
}, "%oldtypedlinks cleared correctly");
system "rm -rf $config{wikistatedir}"; system "rm -rf $config{wikistatedir}";

45
t/tag.t 100755
View File

@ -0,0 +1,45 @@
#!/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";
$config{tagged_is_strict} = 1;
%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;