(no commit message)
[ikiwiki.git] / doc / tips / convert_mediawiki_to_ikiwiki / discussion.mdwn
1 The u32 page is excellent, but I wonder if documenting the procedure here
2 would be worthwhile. Who knows, the remote site might disappear. But also
3 there are some variations on the approach that might be useful:
4
5  * using a python script and the dom library to extract the page names from
6    Special:Allpages (such as
7    <http://www.staff.ncl.ac.uk/jon.dowland/unix/docs/get_pagenames.py>)
8  * Or, querying the mysql back-end to get the names
9  * using WWW::MediaWiki for importing/exporting pages from the wiki, instead
10    of Special::Export
11
12 Also, some detail on converting mediawiki transclusion to ikiwiki inlines...
13
14 -- [[users/Jon]]
15
16 > "Who knows, the remote site might disappear.". Right now, it appears to
17 > have done just that. -- [[users/Jon]]
18
19
20 The iki-fast-load ruby script from the u32 page is given below:
21
22         #!/usr/bin/env ruby
23
24         # This script is called on the final sorted, de-spammed revision
25         # XML file.
26         #
27         # It doesn't currently check for no-op revisions...  I believe
28         # that git-fast-load will dutifully load them even though nothing
29         # happened.  I don't care to solve this by adding a file cache
30         # to this script.  You can run iki-diff-next.rb to highlight any
31         # empty revisions that need to be removed.
32         #
33         # This turns each node into an equivalent file.
34         #    It does not convert spaces to underscores in file names.
35         #       This would break wikilinks.
36         #       I suppose you could fix this with mod_speling or mod_rewrite.
37         #
38         # It replaces nodes in the Image: namespace with the files themselves.
39
40
41         require 'rubygems'
42         require 'node-callback'
43         require 'time'
44         require 'ostruct'
45
46
47         # pipe is the stream to receive the git-fast-import commands
48         # putfrom is true if this branch has existing commits on it, false if not.
49         def format_git_commit(pipe, f)
50            # Need to escape backslashes and double-quotes for git?
51            # No, git breaks when I do this. 
52            # For the filename "path with \\", git sez: bad default revision 'HEAD'
53            # filename = '"' + filename.gsub('\\', '\\\\\\\\').gsub('"', '\\"') + '"'
54
55            # In the calls below, length must be the size in bytes!!
56            # TODO: I haven't figured out how this works in the land of UTF8 and Ruby 1.9.
57            pipe.puts "commit #{f.branch}"
58            pipe.puts "committer #{f.username} <#{f.email}> #{f.timestamp.rfc2822}"
59            pipe.puts "data #{f.message.length}\n#{f.message}\n"
60            pipe.puts "from #{f.branch}^0" if f.putfrom
61            pipe.puts "M 644 inline #{f.filename}"
62            pipe.puts "data #{f.content.length}\n#{f.content}\n"
63            pipe.puts
64         end
65
66 > Would be nice to know where you could get "node-callbacks"... this thing is useless without it. --[[users/simonraven]]
67
68
69 Mediawiki.pm - A plugin which supports mediawiki format.
70
71         #!/usr/bin/perl
72         # By Scott Bronson.  Licensed under the GPLv2+ License.
73         # Extends Ikiwiki to be able to handle Mediawiki markup.
74         #
75         # To use the Mediawiki Plugin:
76         # - Install Text::MediawikiFormat
77         # - Turn of prefix_directives in your setup file.
78         #     (TODO: we probably don't need to do this anymore?)
79         #        prefix_directives => 1,
80         # - Add this plugin on Ikiwiki's path (perl -V, look for @INC)
81         #       cp mediawiki.pm something/IkiWiki/Plugin
82         # - And enable it in your setup file
83         #        add_plugins => [qw{mediawiki}],
84         # - Finally, turn off the link plugin in setup (this is important)
85         #        disable_plugins => [qw{link}],
86         # - Rebuild everything (actually, this should be automatic right?)
87         # - Now all files with a .mediawiki extension should be rendered properly.
88         
89         
90         package IkiWiki::Plugin::mediawiki;
91         
92         use warnings;
93         use strict;
94         use IkiWiki 2.00;
95         use URI;
96         
97         
98         # This is a gross hack...  We disable the link plugin so that our
99         # linkify routine is always called.  Then we call the link plugin
100         # directly for all non-mediawiki pages.  Ouch...  Hopefully Ikiwiki
101         # will be updated soon to support multiple link plugins.
102         require IkiWiki::Plugin::link;
103         
104         # Even if T:MwF is not installed, we can still handle all the linking.
105         # The user will just see Mediawiki markup rather than formatted markup.
106         eval q{use Text::MediawikiFormat ()};
107         my $markup_disabled = $@;
108         
109         # Work around a UTF8 bug in Text::MediawikiFormat
110         # http://rt.cpan.org/Public/Bug/Display.html?id=26880
111         unless($markup_disabled) {
112            no strict 'refs';
113            no warnings;
114            *{'Text::MediawikiFormat::uri_escape'} = \&URI::Escape::uri_escape_utf8;
115         }
116         
117         my %metaheaders;    # keeps track of redirects for pagetemplate.
118         my %tags;      # keeps track of tags for pagetemplate.
119         
120         
121         sub import { #{{{
122            hook(type => "checkconfig", id => "mediawiki", call => \&checkconfig);
123            hook(type => "scan", id => "mediawiki", call => \&scan);
124            hook(type => "linkify", id => "mediawiki", call => \&linkify);
125            hook(type => "htmlize", id => "mediawiki", call => \&htmlize);
126            hook(type => "pagetemplate", id => "mediawiki", call => \&pagetemplate);
127         } # }}}
128         
129         
130         sub checkconfig
131         {
132            return IkiWiki::Plugin::link::checkconfig(@_);
133         }
134         
135         
136         my $link_regexp = qr{
137             \[\[(?=[^!])        # beginning of link
138             ([^\n\r\]#|<>]+)      # 1: page to link to
139             (?:
140                 \#              # '#', beginning of anchor
141                 ([^|\]]+)       # 2: anchor text
142             )?                  # optional
143         
144             (?:
145                 \|              # followed by '|'
146                 ([^\]\|]*)      # 3: link text
147             )?                  # optional
148             \]\]                # end of link
149                 ([a-zA-Z]*)   # optional trailing alphas
150         }x;
151         
152         
153         # Convert spaces in the passed-in string into underscores.
154         # If passed in undef, returns undef without throwing errors.
155         sub underscorize
156         {
157            my $var = shift;
158            $var =~ tr{ }{_} if $var;
159            return $var;
160         }
161         
162         
163         # Underscorize, strip leading and trailing space, and scrunch
164         # multiple runs of spaces into one underscore.
165         sub scrunch
166         {
167            my $var = shift;
168            if($var) {
169               $var =~ s/^\s+|\s+$//g;      # strip leading and trailing space
170               $var =~ s/\s+/ /g;      # squash multiple spaces to one
171            }
172            return $var;
173         }
174         
175         
176         # Translates Mediawiki paths into Ikiwiki paths.
177         # It needs to be pretty careful because Mediawiki and Ikiwiki handle
178         # relative vs. absolute exactly opposite from each other.
179         sub translate_path
180         {
181            my $page = shift;
182            my $path = scrunch(shift);
183         
184            # always start from root unless we're doing relative shenanigans.
185            $page = "/" unless $path =~ /^(?:\/|\.\.)/;
186         
187            my @result = ();
188            for(split(/\//, "$page/$path")) {
189               if($_ eq '..') {
190                  pop @result;
191               } else {
192                  push @result, $_ if $_ ne "";
193               }
194            }
195         
196            # temporary hack working around http://ikiwiki.info/bugs/Can__39__t_create_root_page/index.html?updated
197            # put this back the way it was once this bug is fixed upstream.
198            # This is actually a major problem because now Mediawiki pages can't link from /Git/git-svn to /git-svn.  And upstream appears to be uninterested in fixing this bug.  :(
199            # return "/" . join("/", @result);
200            return join("/", @result);
201         }
202         
203         
204         # Figures out the human-readable text for a wikilink
205         sub linktext
206         {
207            my($page, $inlink, $anchor, $title, $trailing) = @_;
208            my $link = translate_path($page,$inlink);
209         
210            # translate_path always produces an absolute link.
211            # get rid of the leading slash before we display this link.
212            $link =~ s#^/##;
213         
214            my $out = "";
215            if($title) {
216                $out = IkiWiki::pagetitle($title);
217            } else {
218               $link = $inlink if $inlink =~ /^\s*\//;
219                $out = $anchor ? "$link#$anchor" : $link;
220               if(defined $title && $title eq "") {
221                  # a bare pipe appeared in the link...
222                  # user wants to strip namespace and trailing parens.
223                  $out =~ s/^[A-Za-z0-9_-]*://;
224                  $out =~ s/\s*\(.*\)\s*$//;
225               }
226               # A trailing slash suppresses the leading slash
227               $out =~ s#^/(.*)/$#$1#;
228            }
229            $out .= $trailing if defined $trailing;
230            return $out;
231         }
232         
233         
234         sub tagpage ($)
235         {
236            my $tag=shift;
237         
238            if (exists $config{tagbase} && defined $config{tagbase}) {
239               $tag=$config{tagbase}."/".$tag;
240            }
241         
242            return $tag;
243         }
244         
245         
246         # Pass a URL and optional text associated with it.  This call turns
247         # it into fully-formatted HTML the same way Mediawiki would.
248         # Counter is used to number untitled links sequentially on the page.
249         # It should be set to 1 when you start parsing a new page.  This call
250         # increments it automatically.
251         sub generate_external_link
252         {
253            my $url = shift;
254            my $text = shift;
255            my $counter = shift;
256         
257            # Mediawiki trims off trailing commas.
258            # And apparently it does entity substitution first.
259            # Since we can't, we'll fake it.
260         
261            # trim any leading and trailing whitespace
262            $url =~ s/^\s+|\s+$//g;
263         
264            # url properly terminates on > but must special-case &gt;
265            my $trailer = "";
266            $url =~ s{(\&(?:gt|lt)\;.*)$}{ $trailer = $1, ''; }eg;
267         
268            # Trim some potential trailing chars, put them outside the link.
269            my $tmptrail = "";
270            $url =~ s{([,)]+)$}{ $tmptrail .= $1, ''; }eg;
271            $trailer = $tmptrail . $trailer;
272         
273            my $title = $url;
274            if(defined $text) {
275               if($text eq "") {
276                  $text = "[$$counter]";
277                  $$counter += 1;
278               }
279               $text =~ s/^\s+|\s+$//g;
280               $text =~ s/^\|//;
281            } else {
282               $text = $url;
283            }
284         
285            return "<a href='$url' title='$title'>$text</a>$trailer";
286         }
287         
288         
289         # Called to handle bookmarks like [[#heading]] or <span class="createlink"><a href="http://u32.net/cgi-bin/ikiwiki.cgi?page=%20text%20&amp;from=Mediawiki_Plugin%2Fmediawiki&amp;do=create" rel="nofollow">?</a>#a</span>
290         sub generate_fragment_link
291         {
292            my $url = shift;
293            my $text = shift;
294         
295            my $inurl = $url;
296            my $intext = $text;
297            $url = scrunch($url);
298         
299            if(defined($text) && $text ne "") {
300               $text = scrunch($text);
301            } else {
302               $text = $url;
303            }
304         
305            $url = underscorize($url);
306         
307            # For some reason Mediawiki puts blank titles on all its fragment links.
308            # I don't see why we would duplicate that behavior here.
309            return "<a href='$url'>$text</a>";
310         }
311         
312         
313         sub generate_internal_link
314         {
315            my($page, $inlink, $anchor, $title, $trailing, $proc) = @_;
316         
317            # Ikiwiki's link link plugin wrecks this line when displaying on the site.
318            # Until the code highlighter plugin can turn off link finding,
319            # always escape double brackets in double quotes: [[
320            if($inlink eq '..') {
321               # Mediawiki doesn't touch links like [[..#hi|ho]].
322               return "[[" . $inlink . ($anchor?"#$anchor":"") .
323                  ($title?"|$title":"") . "]]" . $trailing;
324            }
325         
326            my($linkpage, $linktext);
327            if($inlink =~ /^ (:?) \s* Category (\s* \: \s*) ([^\]]*) $/x) {
328               # Handle category links
329               my $sep = $2;
330               $inlink = $3;
331               $linkpage = IkiWiki::linkpage(translate_path($page, $inlink));
332               if($1) {
333                  # Produce a link but don't add this page to the given category.
334                  $linkpage = tagpage($linkpage);
335                  $linktext = ($title ? '' : "Category$sep") .
336                     linktext($page, $inlink, $anchor, $title, $trailing);
337                  $tags{$page}{$linkpage} = 1;
338               } else {
339                  # Add this page to the given category but don't produce a link.
340                  $tags{$page}{$linkpage} = 1;
341                  &$proc(tagpage($linkpage), $linktext, $anchor);
342                  return "";
343               }
344            } else {
345               # It's just a regular link
346               $linkpage = IkiWiki::linkpage(translate_path($page, $inlink));
347               $linktext = linktext($page, $inlink, $anchor, $title, $trailing);
348            }
349         
350            return &$proc($linkpage, $linktext, $anchor);
351         }
352         
353         
354         sub check_redirect
355         {
356            my %params=@_;
357         
358            my $page=$params{page};
359            my $destpage=$params{destpage};
360            my $content=$params{content};
361         
362            return "" if $page ne $destpage;
363         
364            if($content !~ /^ \s* \#REDIRECT \s* \[\[ ( [^\]]+ ) \]\]/x) {
365               # this page isn't a redirect, render it normally.
366               return undef;
367            }
368         
369            # The rest of this function is copied from the redir clause
370            # in meta::preprocess and actually handles the redirect.
371         
372            my $value = $1;
373            $value =~ s/^\s+|\s+$//g;
374         
375            my $safe=0;
376            if ($value !~ /^\w+:\/\//) {
377               # it's a local link
378               my ($redir_page, $redir_anchor) = split /\#/, $value;
379         
380               add_depends($page, $redir_page);
381               my $link=bestlink($page, underscorize(translate_path($page,$redir_page)));
382               if (! length $link) {
383                  return "<b>Redirect Error:</b> <nowiki>[[$redir_page]] not found.</nowiki>";
384               }
385         
386               $value=urlto($link, $page);
387               $value.='#'.$redir_anchor if defined $redir_anchor;
388               $safe=1;
389         
390               # redir cycle detection
391               $pagestate{$page}{mediawiki}{redir}=$link;
392               my $at=$page;
393               my %seen;
394               while (exists $pagestate{$at}{mediawiki}{redir}) {
395                  if ($seen{$at}) {
396                     return "<b>Redirect Error:</b> cycle found on <nowiki>[[$at]]</nowiki>";
397                  }
398                  $seen{$at}=1;
399                  $at=$pagestate{$at}{mediawiki}{redir};
400               }
401            } else {
402               # it's an external link
403               $value = encode_entities($value);
404            }
405         
406            my $redir="<meta http-equiv=\"refresh\" content=\"0; URL=$value\" />";
407            $redir=scrub($redir) if !$safe;
408            push @{$metaheaders{$page}}, $redir;
409         
410            return "Redirecting to $value ...";
411         }
412         
413         
414         # Feed this routine a string containing <nowiki>...</nowiki> sections,
415         # this routine calls your callback for every section not within nowikis,
416         # collecting its return values and returning the rewritten string.
417         sub skip_nowiki
418         {
419            my $content = shift;
420            my $proc = shift;
421         
422            my $result = "";
423            my $state = 0;
424         
425            for(split(/(<nowiki[^>]*>.*?<\/nowiki\s*>)/s, $content)) {
426               $result .= ($state ? $_ : &$proc($_));
427               $state = !$state;
428            }
429         
430            return $result;
431         }
432         
433         
434         # Converts all links in the page, wiki and otherwise.
435         sub linkify (@)
436         {
437            my %params=@_;
438         
439            my $page=$params{page};
440            my $destpage=$params{destpage};
441            my $content=$params{content};
442         
443            my $file=$pagesources{$page};
444            my $type=pagetype($file);
445            my $counter = 1;
446         
447            if($type ne 'mediawiki') {
448               return IkiWiki::Plugin::link::linkify(@_);
449            }
450         
451            my $redir = check_redirect(%params);
452            return $redir if defined $redir;
453         
454            # this code was copied from MediawikiFormat.pm.
455            # Heavily changed because MF.pm screws up escaping when it does
456            # this awful hack: $uricCheat =~ tr/://d;
457            my $schemas = [qw(http https ftp mailto gopher)];
458            my $re = join "|", map {qr/\Q$_\E/} @$schemas;
459            my $schemes = qr/(?:$re)/;
460            # And this is copied from URI:
461            my $reserved   = q(;/?@&=+$,);   # NOTE: no colon or [] !
462            my $uric       = quotemeta($reserved) . $URI::unreserved . "%#";
463         
464            my $result = skip_nowiki($content, sub {
465               $_ = shift;
466         
467               # Escape any anchors
468               #s/<(a[\s>\/])/&lt;$1/ig;
469               # Disabled because this appears to screw up the aggregate plugin.
470               # I guess we'll rely on Iki to post-sanitize this sort of stuff.
471         
472               # Replace external links, http://blah or [http://blah]
473               s{\b($schemes:[$uric][:$uric]+)|\[($schemes:[$uric][:$uric]+)([^\]]*?)\]}{
474                  generate_external_link($1||$2, $3, \$counter)
475               }eg;
476         
477               # Handle links that only contain fragments.
478               s{ \[\[ \s* (\#[^|\]'"<>&;]+) (?:\| ([^\]'"<>&;]*))? \]\] }{
479                  generate_fragment_link($1, $2)
480               }xeg;
481         
482               # Match all internal links
483               s{$link_regexp}{
484                  generate_internal_link($page, $1, $2, $3, $4, sub {
485                     my($linkpage, $linktext, $anchor) = @_;
486                     return htmllink($page, $destpage, $linkpage,
487                        linktext => $linktext,
488                        anchor => underscorize(scrunch($anchor)));
489                  });
490               }eg;
491            
492               return $_;
493            });
494         
495            return $result;
496         }
497         
498         
499         # Find all WikiLinks in the page.
500         sub scan (@)
501         {
502            my %params = @_;
503            my $page=$params{page};
504            my $content=$params{content};
505         
506            my $file=$pagesources{$page};
507            my $type=pagetype($file);
508         
509            if($type ne 'mediawiki') {
510               return IkiWiki::Plugin::link::scan(@_);
511            }
512         
513            skip_nowiki($content, sub {
514               $_ = shift;
515               while(/$link_regexp/g) {
516                  generate_internal_link($page, $1, '', '', '', sub {
517                     my($linkpage, $linktext, $anchor) = @_;
518                     push @{$links{$page}}, $linkpage;
519                     return undef;
520                  });
521               }
522               return '';
523            });
524         }
525         
526         
527         # Convert the page to HTML.
528         sub htmlize (@)
529         {
530            my %params=@_;
531            my $page = $params{page};
532            my $content = $params{content};
533         
534         
535            return $content if $markup_disabled;
536         
537            # Do a little preprocessing to babysit Text::MediawikiFormat
538            # If a line begins with tabs, T:MwF won't convert it into preformatted blocks.
539            $content =~ s/^\t/    /mg;
540         
541            my $ret = Text::MediawikiFormat::format($content, {
542         
543                allowed_tags    => [#HTML
544                         # MediawikiFormat default
545                         qw(b big blockquote br caption center cite code dd
546                            div dl dt em font h1 h2 h3 h4 h5 h6 hr i li ol p
547                            pre rb rp rt ruby s samp small strike strong sub
548                            sup table td th tr tt u ul var),
549                          # Mediawiki Specific
550                          qw(nowiki),
551                          # Our additions
552                          qw(del ins),   # These should have been added all along.
553                          qw(span),   # Mediawiki allows span but that's rather scary...?
554                          qw(a),      # this is unfortunate; should handle links after rendering the page.
555                        ],
556         
557                allowed_attrs   => [
558                         qw(title align lang dir width height bgcolor),
559                         qw(clear), # BR
560                         qw(noshade), # HR
561                         qw(cite), # BLOCKQUOTE, Q
562                         qw(size face color), # FONT
563                         # For various lists, mostly deprecated but safe
564                         qw(type start value compact),
565                         # Tables
566                         qw(summary width border frame rules cellspacing
567                            cellpadding valign char charoff colgroup col
568                            span abbr axis headers scope rowspan colspan),
569                         qw(id class name style), # For CSS
570                         # Our additions
571                         qw(href),
572                        ],
573         
574               }, {
575               extended => 0,
576               absolute_links => 0,
577               implicit_links => 0
578               });
579         
580            return $ret;
581         }
582         
583         
584         # This is only needed to support the check_redirect call.
585         sub pagetemplate (@)
586         {
587            my %params = @_;
588            my $page = $params{page};
589            my $destpage = $params{destpage};
590            my $template = $params{template};
591         
592            # handle metaheaders for redirects
593            if (exists $metaheaders{$page} && $template->query(name => "meta")) {
594            # avoid duplicate meta lines
595               my %seen;
596               $template->param(meta => join("\n", grep { (! $seen{$_}) && ($seen{$_}=1) } @{$metaheaders{$page}}));
597            }
598         
599            $template->param(tags => [
600               map {
601                  link => htmllink($page, $destpage, tagpage($_), rel => "tag")
602               }, sort keys %{$tags{$page}}
603            ]) if exists $tags{$page} && %{$tags{$page}} && $template->query(name => "tags");
604         
605            # It's an rss/atom template. Add any categories.
606            if ($template->query(name => "categories")) {
607               if (exists $tags{$page} && %{$tags{$page}}) {
608                  $template->param(categories => [map { category => $_ },
609                     sort keys %{$tags{$page}}]);
610               }
611            }
612         }
613         
614         1