summaryrefslogtreecommitdiff
path: root/IkiWiki/Render.pm
blob: 2c1ea3eb7c0b73f507fb51c70499f01c10c51ccc (plain)
  1. #!/usr/bin/perl
  2. package IkiWiki;
  3. use warnings;
  4. use strict;
  5. use IkiWiki;
  6. use Encode;
  7. sub linkify ($$$) { #{{{
  8. my $lpage=shift; # the page containing the links
  9. my $page=shift; # the page the link will end up on (different for inline)
  10. my $content=shift;
  11. $content =~ s{(\\?)$config{wiki_link_regexp}}{
  12. $2 ? ( $1 ? "[[$2|$3]]" : htmllink($lpage, $page, titlepage($3), 0, 0, pagetitle($2)))
  13. : ( $1 ? "[[$3]]" : htmllink($lpage, $page, titlepage($3)))
  14. }eg;
  15. return $content;
  16. } #}}}
  17. sub htmlize ($$$) { #{{{
  18. my $page=shift;
  19. my $type=shift;
  20. my $content=shift;
  21. if (exists $hooks{htmlize}{$type}) {
  22. $content=$hooks{htmlize}{$type}{call}->(
  23. page => $page,
  24. content => $content,
  25. );
  26. }
  27. else {
  28. error("htmlization of $type not supported");
  29. }
  30. run_hooks(sanitize => sub {
  31. $content=shift->(
  32. page => $page,
  33. content => $content,
  34. );
  35. });
  36. return $content;
  37. } #}}}
  38. sub backlinks ($) { #{{{
  39. my $page=shift;
  40. my @links;
  41. foreach my $p (keys %links) {
  42. next if bestlink($page, $p) eq $page;
  43. if (grep { length $_ && bestlink($p, $_) eq $page } @{$links{$p}}) {
  44. my $href=abs2rel(htmlpage($p), dirname($page));
  45. # Trim common dir prefixes from both pages.
  46. my $p_trimmed=$p;
  47. my $page_trimmed=$page;
  48. my $dir;
  49. 1 while (($dir)=$page_trimmed=~m!^([^/]+/)!) &&
  50. defined $dir &&
  51. $p_trimmed=~s/^\Q$dir\E// &&
  52. $page_trimmed=~s/^\Q$dir\E//;
  53. push @links, { url => $href, page => pagetitle($p_trimmed) };
  54. }
  55. }
  56. return sort { $a->{page} cmp $b->{page} } @links;
  57. } #}}}
  58. sub parentlinks ($) { #{{{
  59. my $page=shift;
  60. my @ret;
  61. my $pagelink="";
  62. my $path="";
  63. my $skip=1;
  64. return if $page eq 'index'; # toplevel
  65. foreach my $dir (reverse split("/", $page)) {
  66. if (! $skip) {
  67. $path.="../";
  68. unshift @ret, { url => $path.htmlpage($dir), page => pagetitle($dir) };
  69. }
  70. else {
  71. $skip=0;
  72. }
  73. }
  74. unshift @ret, { url => length $path ? $path : ".", page => $config{wikiname} };
  75. return @ret;
  76. } #}}}
  77. my %preprocessing;
  78. sub preprocess ($$$) { #{{{
  79. my $page=shift; # the page the data comes from
  80. my $destpage=shift; # the page the data will appear in (different for inline)
  81. my $content=shift;
  82. my $handle=sub {
  83. my $escape=shift;
  84. my $command=shift;
  85. my $params=shift;
  86. if (length $escape) {
  87. return "[[$command $params]]";
  88. }
  89. elsif (exists $hooks{preprocess}{$command}) {
  90. # Note: preserve order of params, some plugins may
  91. # consider it significant.
  92. my @params;
  93. while ($params =~ /(?:(\w+)=)?(?:"""(.*?)"""|"([^"]+)"|(\S+))(?:\s+|$)/sg) {
  94. my $key=$1;
  95. my $val;
  96. if (defined $2) {
  97. $val=$2;
  98. $val=~s/\r\n/\n/mg;
  99. $val=~s/^\n+//g;
  100. $val=~s/\n+$//g;
  101. }
  102. elsif (defined $3) {
  103. $val=$3;
  104. }
  105. elsif (defined $4) {
  106. $val=$4;
  107. }
  108. if (defined $key) {
  109. push @params, $key, $val;
  110. }
  111. else {
  112. push @params, $val, '';
  113. }
  114. }
  115. if ($preprocessing{$page}++ > 3) {
  116. # Avoid loops of preprocessed pages preprocessing
  117. # other pages that preprocess them, etc.
  118. return "[[$command preprocessing loop detected on $page at depth $preprocessing{$page}]]";
  119. }
  120. my $ret=$hooks{preprocess}{$command}{call}->(
  121. @params,
  122. page => $page,
  123. destpage => $destpage,
  124. );
  125. $preprocessing{$page}--;
  126. return $ret;
  127. }
  128. else {
  129. return "[[$command $params]]";
  130. }
  131. };
  132. $content =~ s{(\\?)\[\[(\w+)\s+((?:(?:\w+=)?(?:""".*?"""|"[^"]+"|[^\s\]]+)\s*)*)\]\]}{$handle->($1, $2, $3)}seg;
  133. return $content;
  134. } #}}}
  135. sub add_depends ($$) { #{{{
  136. my $page=shift;
  137. my $pagespec=shift;
  138. if (! exists $depends{$page}) {
  139. $depends{$page}=$pagespec;
  140. }
  141. else {
  142. $depends{$page}=pagespec_merge($depends{$page}, $pagespec);
  143. }
  144. } # }}}
  145. sub genpage ($$$) { #{{{
  146. my $page=shift;
  147. my $content=shift;
  148. my $mtime=shift;
  149. my $template=template("page.tmpl", blind_cache => 1);
  150. my $actions=0;
  151. if (length $config{cgiurl}) {
  152. $template->param(editurl => cgiurl(do => "edit", page => $page));
  153. $template->param(prefsurl => cgiurl(do => "prefs"));
  154. if ($config{rcs}) {
  155. $template->param(recentchangesurl => cgiurl(do => "recentchanges"));
  156. }
  157. $actions++;
  158. }
  159. if (length $config{historyurl}) {
  160. my $u=$config{historyurl};
  161. $u=~s/\[\[file\]\]/$pagesources{$page}/g;
  162. $template->param(historyurl => $u);
  163. $actions++;
  164. }
  165. if ($config{discussion}) {
  166. $template->param(discussionlink => htmllink($page, $page, "Discussion", 1, 1));
  167. $actions++;
  168. }
  169. if ($actions) {
  170. $template->param(have_actions => 1);
  171. }
  172. $template->param(
  173. title => $page eq 'index'
  174. ? $config{wikiname}
  175. : pagetitle(basename($page)),
  176. wikiname => $config{wikiname},
  177. parentlinks => [parentlinks($page)],
  178. content => $content,
  179. backlinks => [backlinks($page)],
  180. mtime => displaytime($mtime),
  181. baseurl => baseurl($page),
  182. );
  183. run_hooks(pagetemplate => sub {
  184. shift->(page => $page, destpage => $page, template => $template);
  185. });
  186. $content=$template->output;
  187. run_hooks(format => sub {
  188. $content=shift->(
  189. page => $page,
  190. content => $content,
  191. );
  192. });
  193. return $content;
  194. } #}}}
  195. sub check_overwrite ($$) { #{{{
  196. # Important security check. Make sure to call this before saving
  197. # any files to the source directory.
  198. my $dest=shift;
  199. my $src=shift;
  200. if (! exists $renderedfiles{$src} && -e $dest && ! $config{rebuild}) {
  201. error("$dest already exists and was not rendered from $src before");
  202. }
  203. } #}}}
  204. sub displaytime ($) { #{{{
  205. my $time=shift;
  206. eval q{use POSIX};
  207. # strftime doesn't know about encodings, so make sure
  208. # its output is properly treated as utf8
  209. return decode_utf8(POSIX::strftime(
  210. $config{timeformat}, localtime($time)));
  211. } #}}}
  212. sub mtime ($) { #{{{
  213. my $file=shift;
  214. return (stat($file))[9];
  215. } #}}}
  216. sub findlinks ($$) { #{{{
  217. my $page=shift;
  218. my $content=shift;
  219. my @links;
  220. while ($content =~ /(?<!\\)$config{wiki_link_regexp}/g) {
  221. push @links, titlepage($2);
  222. }
  223. if ($config{discussion}) {
  224. # Discussion links are a special case since they're not in the
  225. # text of the page, but on its template.
  226. return @links, "$page/discussion";
  227. }
  228. else {
  229. return @links;
  230. }
  231. } #}}}
  232. sub filter ($$) {
  233. my $page=shift;
  234. my $content=shift;
  235. run_hooks(filter => sub {
  236. $content=shift->(page => $page, content => $content);
  237. });
  238. return $content;
  239. }
  240. sub render ($) { #{{{
  241. my $file=shift;
  242. my $type=pagetype($file);
  243. my $srcfile=srcfile($file);
  244. if (defined $type) {
  245. my $content=readfile($srcfile);
  246. my $page=pagename($file);
  247. delete $depends{$page};
  248. $content=filter($page, $content);
  249. $links{$page}=[findlinks($page, $content)];
  250. $content=preprocess($page, $page, $content);
  251. $content=linkify($page, $page, $content);
  252. $content=htmlize($page, $type, $content);
  253. check_overwrite("$config{destdir}/".htmlpage($page), $page);
  254. writefile(htmlpage($page), $config{destdir},
  255. genpage($page, $content, mtime($srcfile)));
  256. $oldpagemtime{$page}=time;
  257. $renderedfiles{$page}=htmlpage($page);
  258. }
  259. else {
  260. my $content=readfile($srcfile, 1);
  261. $links{$file}=[];
  262. delete $depends{$file};
  263. check_overwrite("$config{destdir}/$file", $file);
  264. writefile($file, $config{destdir}, $content, 1);
  265. $oldpagemtime{$file}=time;
  266. $renderedfiles{$file}=$file;
  267. }
  268. } #}}}
  269. sub prune ($) { #{{{
  270. my $file=shift;
  271. unlink($file);
  272. my $dir=dirname($file);
  273. while (rmdir($dir)) {
  274. $dir=dirname($dir);
  275. }
  276. } #}}}
  277. sub refresh () { #{{{
  278. # find existing pages
  279. my %exists;
  280. my @files;
  281. eval q{use File::Find};
  282. find({
  283. no_chdir => 1,
  284. wanted => sub {
  285. $_=decode_utf8($_);
  286. if (/$config{wiki_file_prune_regexp}/) {
  287. $File::Find::prune=1;
  288. }
  289. elsif (! -d $_ && ! -l $_) {
  290. my ($f)=/$config{wiki_file_regexp}/; # untaint
  291. if (! defined $f) {
  292. warn("skipping bad filename $_\n");
  293. }
  294. else {
  295. $f=~s/^\Q$config{srcdir}\E\/?//;
  296. push @files, $f;
  297. $exists{pagename($f)}=1;
  298. }
  299. }
  300. },
  301. }, $config{srcdir});
  302. find({
  303. no_chdir => 1,
  304. wanted => sub {
  305. $_=decode_utf8($_);
  306. if (/$config{wiki_file_prune_regexp}/) {
  307. $File::Find::prune=1;
  308. }
  309. elsif (! -d $_ && ! -l $_) {
  310. my ($f)=/$config{wiki_file_regexp}/; # untaint
  311. if (! defined $f) {
  312. warn("skipping bad filename $_\n");
  313. }
  314. else {
  315. # Don't add files that are in the
  316. # srcdir.
  317. $f=~s/^\Q$config{underlaydir}\E\/?//;
  318. if (! -e "$config{srcdir}/$f" &&
  319. ! -l "$config{srcdir}/$f") {
  320. push @files, $f;
  321. $exists{pagename($f)}=1;
  322. }
  323. }
  324. }
  325. },
  326. }, $config{underlaydir});
  327. my %rendered;
  328. # check for added or removed pages
  329. my @add;
  330. foreach my $file (@files) {
  331. my $page=pagename($file);
  332. if (! $oldpagemtime{$page}) {
  333. debug("new page $page") unless exists $pagectime{$page};
  334. push @add, $file;
  335. $links{$page}=[];
  336. $pagecase{lc $page}=$page;
  337. $pagesources{$page}=$file;
  338. if ($config{getctime} && -e "$config{srcdir}/$file") {
  339. $pagectime{$page}=rcs_getctime("$config{srcdir}/$file");
  340. }
  341. elsif (! exists $pagectime{$page}) {
  342. $pagectime{$page}=mtime(srcfile($file));
  343. }
  344. }
  345. }
  346. my @del;
  347. foreach my $page (keys %oldpagemtime) {
  348. if (! $exists{$page}) {
  349. debug("removing old page $page");
  350. push @del, $pagesources{$page};
  351. prune($config{destdir}."/".$renderedfiles{$page});
  352. delete $renderedfiles{$page};
  353. $oldpagemtime{$page}=0;
  354. delete $pagesources{$page};
  355. }
  356. }
  357. # render any updated files
  358. foreach my $file (@files) {
  359. my $page=pagename($file);
  360. if (! exists $oldpagemtime{$page} ||
  361. mtime(srcfile($file)) > $oldpagemtime{$page} ||
  362. $forcerebuild{$page}) {
  363. debug("rendering $file");
  364. render($file);
  365. $rendered{$file}=1;
  366. }
  367. }
  368. # if any files were added or removed, check to see if each page
  369. # needs an update due to linking to them or inlining them.
  370. # TODO: inefficient; pages may get rendered above and again here;
  371. # problem is the bestlink may have changed and we won't know until
  372. # now
  373. if (@add || @del) {
  374. FILE: foreach my $file (@files) {
  375. my $page=pagename($file);
  376. foreach my $f (@add, @del) {
  377. my $p=pagename($f);
  378. foreach my $link (@{$links{$page}}) {
  379. if (bestlink($page, $link) eq $p) {
  380. debug("rendering $file, which links to $p");
  381. render($file);
  382. $rendered{$file}=1;
  383. next FILE;
  384. }
  385. }
  386. }
  387. }
  388. }
  389. # Handle backlinks; if a page has added/removed links, update the
  390. # pages it links to. Also handles rebuilding dependant pages.
  391. # TODO: inefficient; pages may get rendered above and again here;
  392. # problem is the backlinks could be wrong in the first pass render
  393. # above
  394. if (%rendered || @del) {
  395. foreach my $f (@files) {
  396. my $p=pagename($f);
  397. if (exists $depends{$p}) {
  398. foreach my $file (keys %rendered, @del) {
  399. next if $f eq $file;
  400. my $page=pagename($file);
  401. if (pagespec_match($page, $depends{$p})) {
  402. debug("rendering $f, which depends on $page");
  403. render($f);
  404. $rendered{$f}=1;
  405. last;
  406. }
  407. }
  408. }
  409. }
  410. my %linkchanged;
  411. foreach my $file (keys %rendered, @del) {
  412. my $page=pagename($file);
  413. if (exists $links{$page}) {
  414. foreach my $link (map { bestlink($page, $_) } @{$links{$page}}) {
  415. if (length $link &&
  416. (! exists $oldlinks{$page} ||
  417. ! grep { bestlink($page, $_) eq $link } @{$oldlinks{$page}})) {
  418. $linkchanged{$link}=1;
  419. }
  420. }
  421. }
  422. if (exists $oldlinks{$page}) {
  423. foreach my $link (map { bestlink($page, $_) } @{$oldlinks{$page}}) {
  424. if (length $link &&
  425. (! exists $links{$page} ||
  426. ! grep { bestlink($page, $_) eq $link } @{$links{$page}})) {
  427. $linkchanged{$link}=1;
  428. }
  429. }
  430. }
  431. }
  432. foreach my $link (keys %linkchanged) {
  433. my $linkfile=$pagesources{$link};
  434. if (defined $linkfile) {
  435. debug("rendering $linkfile, to update its backlinks");
  436. render($linkfile);
  437. $rendered{$linkfile}=1;
  438. }
  439. }
  440. }
  441. if (@del) {
  442. run_hooks(delete => sub { shift->(@del) });
  443. }
  444. if (%rendered) {
  445. run_hooks(change => sub { shift->(keys %rendered) });
  446. }
  447. } #}}}
  448. 1