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
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
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")) {
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}}];
}
$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}}) {
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 ($) {
$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)
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_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 ($$;@) {
use strict;
use IkiWiki 3.00;
-my %tags;
-
sub import {
hook(type => "getopt", id => "tag", call => \&getopt);
hook(type => "getsetup", id => "tag", call => \&getsetup);
safe => 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 ($) {
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 "";
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);
}
}
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]);
}
}
}
sub match_tagged ($$;@) {
my $page = 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
else {
$links{$page}=[];
}
+ delete $typedlinks{$page};
run_hooks(scan => sub {
shift->(
push @del, $pagesources{$page};
}
$links{$page}=[];
+ delete $typedlinks{$page};
$renderedfiles{$page}=[];
$pagemtime{$page}=0;
}
}
}
+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)=@_;
}
$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;
>> is valid. [[todo/matching_different_kinds_of_links]] is probably
>> 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.
> 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.
specified IP address.
* "`postcomment(glob)`" - matches only when comments are being
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
and were written in 2005:
It also provides the `tagged()` [[ikiwiki/PageSpec]], which can be used to
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="""
This wiki has the tag plugin enabled, so you'll see a note below that this
page is tagged with the "tags" tag.
$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
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
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}";
$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};
# Clear state.
%oldrenderedfiles=%pagectime=();
-%pagesources=%pagemtime=%oldlinks=%links=%depends=
+%pagesources=%pagemtime=%oldlinks=%links=%depends=%typedlinks=%oldtypedlinks=
%destsources=%renderedfiles=%pagecase=%pagestate=();
ok(loadindex(), "load index");
"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
}, "%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}";
--- /dev/null
+#!/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;