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