From c1a42e76bc6667bfb2882a12d53c25d9f952ca82 Mon Sep 17 00:00:00 2001 From: Simon McVittie Date: Fri, 2 Apr 2010 00:28:02 +0100 Subject: [PATCH] implement typed links; add tagged_is_strict config option --- IkiWiki.pm | 58 +++++++++++++++---- IkiWiki/Plugin/tag.pm | 36 +++++++----- IkiWiki/Render.pm | 33 +++++++++++ ...tagged__40____41___matching_wikilinks.mdwn | 3 + doc/ikiwiki/pagespec.mdwn | 3 + doc/plugins/tag.mdwn | 5 ++ doc/plugins/write.mdwn | 21 ++++++- t/index.t | 17 +++++- t/tag.t | 45 ++++++++++++++ 9 files changed, 193 insertions(+), 28 deletions(-) create mode 100755 t/tag.t diff --git a/IkiWiki.pm b/IkiWiki.pm index 6739ba56c..25e9247b2 100644 --- a/IkiWiki.pm +++ b/IkiWiki.pm @@ -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 @@ -1503,7 +1503,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")) { @@ -1569,6 +1569,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}}]; } @@ -1617,6 +1625,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}}) { @@ -1926,12 +1938,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 ($) { @@ -2212,6 +2229,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) @@ -2219,19 +2241,33 @@ 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_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 ($$;@) { diff --git a/IkiWiki/Plugin/tag.pm b/IkiWiki/Plugin/tag.pm index cdcfaf536..af4bff1bc 100644 --- a/IkiWiki/Plugin/tag.pm +++ b/IkiWiki/Plugin/tag.pm @@ -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); @@ -36,6 +34,13 @@ sub 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 ($) { @@ -71,9 +76,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 +91,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 +112,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]); } } } @@ -130,7 +134,13 @@ package IkiWiki::PageSpec; 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 diff --git a/IkiWiki/Render.pm b/IkiWiki/Render.pm index abafb0887..5810fc974 100644 --- a/IkiWiki/Render.pm +++ b/IkiWiki/Render.pm @@ -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}, $oldlinktypes{$page})) { + $linkchangers{lc($page)}=1; + } + } } return \%backlinkchanged, \%linkchangers; diff --git a/doc/bugs/tagged__40____41___matching_wikilinks.mdwn b/doc/bugs/tagged__40____41___matching_wikilinks.mdwn index e7e4af7c3..9037d6c02 100644 --- a/doc/bugs/tagged__40____41___matching_wikilinks.mdwn +++ b/doc/bugs/tagged__40____41___matching_wikilinks.mdwn @@ -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 >> 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. diff --git a/doc/ikiwiki/pagespec.mdwn b/doc/ikiwiki/pagespec.mdwn index 5c191f23f..ca6693024 100644 --- a/doc/ikiwiki/pagespec.mdwn +++ b/doc/ikiwiki/pagespec.mdwn @@ -52,6 +52,9 @@ Some more elaborate limits can be added to what matches using these functions: 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: diff --git a/doc/plugins/tag.mdwn b/doc/plugins/tag.mdwn index 8ff70a069..8cd79da41 100644 --- a/doc/plugins/tag.mdwn +++ b/doc/plugins/tag.mdwn @@ -8,6 +8,11 @@ These directives allow tagging pages. 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. diff --git a/doc/plugins/write.mdwn b/doc/plugins/write.mdwn index 96a2aa16d..fe7cf0183 100644 --- a/doc/plugins/write.mdwn +++ b/doc/plugins/write.mdwn @@ -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/index.t b/t/index.t index 2f23524a7..44273059d 100755 --- 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 index 000000000..3383fd475 --- /dev/null +++ b/t/tag.t @@ -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; -- 2.39.5