avoid potential infinite loop in smiley expansion
[ikiwiki.git] / IkiWiki / Plugin / smiley.pm
1 #!/usr/bin/perl
2 package IkiWiki::Plugin::smiley;
3
4 use warnings;
5 use strict;
6 use IkiWiki 3.00;
7
8 my %smileys;
9 my $smiley_regexp;
10
11 sub import {
12         add_underlay("smiley");
13         hook(type => "getsetup", id => "smiley", call => \&getsetup);
14         hook(type => "sanitize", id => "smiley", call => \&sanitize);
15 }
16
17 sub getsetup () {
18         return
19                 plugin => {
20                         safe => 1,
21                         # force a rebuild because turning it off
22                         # removes the smileys, which would break links
23                         rebuild => 1,
24                 },
25 }
26
27 sub build_regexp () {
28         my $list=readfile(srcfile("smileys.mdwn"));
29         while ($list =~ m/^\s*\*\s+\\\\([^\s]+)\s+\[\[([^]]+)\]\]/mg) {
30                 my $smiley=$1;
31                 my $file=$2;
32
33                 $smileys{$smiley}=$file;
34
35                 # Add a version with < and > escaped, since they probably
36                 # will be (by markdown) by the time the sanitize hook runs.
37                 $smiley=~s/</&lt;/g;
38                 $smiley=~s/>/&gt;/g;
39                 $smileys{$smiley}=$file;
40         }
41         
42         if (! %smileys) {
43                 debug(gettext("failed to parse any smileys"));
44                 $smiley_regexp='';
45                 return;
46         }
47         
48         # sort and reverse so that substrings come after longer strings
49         # that contain them, in most cases.
50         $smiley_regexp='('.join('|', map { quotemeta }
51                 reverse sort keys %smileys).')';
52         #debug($smiley_regexp);
53 }
54
55 sub sanitize (@) {
56         my %params=@_;
57
58         build_regexp() unless defined $smiley_regexp;
59         
60         $_=$params{content};
61         return $_ unless length $smiley_regexp;
62                         
63 MATCH:  while (m{(?:^|(?<=\s|>))(\\?)$smiley_regexp(?:(?=\s|<)|$)}g) {
64                 my $escape=$1;
65                 my $smiley=$2;
66                 my $epos=$-[1];
67                 my $spos=$-[2];
68                 
69                 # Smilies are not allowed inside <pre> or <code>.
70                 # For each tag in turn, match forward to find the next <tag>
71                 # or </tag> after the smiley.
72                 my $pos=pos;
73                 foreach my $tag ("pre", "code") {
74                         if (m/<(\/)?\s*$tag\s*>/isg && defined $1) {
75                                 # </tag> found first, so the smiley is
76                                 # inside the tag, so do not expand it.
77                                 next MATCH;
78                         }
79                         # Reset pos back to where it was before this test.
80                         pos=$pos;
81                 }
82         
83                 if ($escape) {
84                         # Remove escape.
85                         substr($_, $epos, 1)="";
86                         pos=$epos+1;
87                 }
88                 else {
89                         # Replace the smiley with its expanded value.
90                         my $link=htmllink($params{page}, $params{destpage},
91                                          $smileys{$smiley}, linktext => $smiley);
92                         substr($_, $spos, length($smiley))=$link;
93                         pos=$epos+length($link);
94                 }
95         }
96
97         return $_;
98 }
99
100 1