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