Официальная Wiki Ubuntu сидит на движке MoinMoin, мы же используем DokuWiki. Озадачившись вопросами конвертации синтаксиса нашёл лишь это, что только приблизительно позволяет преобразовать основные конструкции. Поэтому, ради спортивного интереса, набросал скриптик на перле (замените только в нём <!/code> на </code> в одном месте, иначе местный парсер ломается):
#!/usr/bin/perl -w # Usage: ./convert.pl moin_file doku_file [namespace] # namespace - for all links to images ({{namespace:image.png}}) use 5.010; # We need it # ----------------------------------------------------------------------------------------------- # # Convert text file with MoinMoin Wiki syntax to DokuWiki syntax # # by Malamut (malamut@ubuntu.ru) (2009) # # ----------------------------------------------------------------------------------------------- # # Hash for smileys replacement %smileys = ( #moin doku 'X-(' => ':-X', ':D' => ':-D', '<:(' => ':-?', ':o' => ':-O', ':(' => ':-(', ':)' => ':-)', 'B)' => '8-)', ':))' => ':-P', ';)' => ';-)', '/!\\' => ':!:', '<!>' => ':!:', '(!)' => ':!:', ':-?' => ':-P', ':\\' => ':-\\', '>:>' => '^_^', '|)' => ':-|', ':-(' => ':-(', ':-)' => ':-)', 'B-)' => '8-)', ':-))' => ':-P', ';-)' => ';-)', '|-)' => ':-|', '(./)' => 'LOL', '{OK}' => ':!:', '{X}' => ':!:', '{i}' => ':!:', '{1}' => '<1>', '{2}' => '<2>', '{3}' => '<3>', '{*}' => '<ubu>', '{o}' => '<circ>', ); # For links regex sub inv { given ($_[0]) { when ("{") { return "}" } when ("[") { return "]" } } return $_[0]; } # Links sub linkReplacement { die "Oops!\n" unless @_ == 5; my ($namespace,$br,$target,$text,$params) = @_; # Attachments links if ($target =~ /^(?:(?:attachment|drawing):)(?<att>.+)$/) { (my $att = $+{att}) =~ s{(.+)/}{}; # leave only filename, without namespaces if ($text) { return "{{$namespace:$att|$text}}" } else { return "{{$namespace:$att}}" } } # InterWiki links if ($target =~ m#^(.+?):(?!//)(.+)$#) { # all with : except internet links (containing ://) if ($text) { return "[[$1>$2|$text]]" } else { return "[[$1>$2]]" } } # All other links simply returned in doku format $target =~ s#/#:#g if $target !~ m#://#; # if target not an internet link replace all / to : (for namespaces) if ($text) { return "$br$br$target|$text" . inv($br) x 2 } else { return "$br$br$target" . inv($br) x 2 } } # Blocks sub blockReplacement { for (my $i = 0; $i < @_ && $_[$i] =~ /^\$*$/; $i++ ) { shift @_ } # delete empty lines for (my $i = -1; -$i <= @_ && $_[$i] =~ /^\$*$/; $i-- ) { pop @_ } # delete empty lines if (!@_) { unshift @_, "<code>\n" } elsif ($_[0] =~ /^\s*#!python/) { shift @_; unshift @_, "<code python>\n" } elsif ($_[0] =~ /^\s*#!cplusplus/) { shift @_; unshift @_, "<code cpp>\n" } elsif ($_[0] =~ /^\s*#!java/) { shift @_; unshift @_, "<code java>\n" } elsif ($_[0] =~ /^\s*#!pascal/) { shift @_; unshift @_, "<code pascal>\n" } elsif ($_[0] =~ /^\s*#.+/) { shift @_; unshift @_, "<code>\n" } else { unshift @_, "<code>\n" } push @_, "<!/code>\n"; return @_; } # Tables sub tableReplacement { @table = @_; for (@table) { s/\|\|(?:\s*(<.+?>))?\s*(.+?)\s*(?=\|\|)/ # delete all whitespaces if ($1) { "||$1$2" } else { "||$2" } /ge; s/((?:\|\|){2,})(.+?)(?=\|\|)/ # move few || to another side of cell and center "|| $2 " . '|' x (length($1) - 2) /ge; # Span # TODO # Aligment # TODO #s/\|\|.*?(?:(?:<\(>)|(?:<style="text-align: left">))(?=\|\|)//g; # left #s/<:>//g; # center #s/<\)>//g; # right # Ok, thats all s/\|\|(\s*)(?:<.+?>\s*?)+(.*?)(?=\|\|)/||$1$2/g; # remove all tags, we really don't need it s/\|\|/|/g; # finally replace || to | } # Remove all empty strings for (0..$#table) { if ($table[$_] =~ /^\|+$/) { splice @table, $_, 1 } } return @table; } # Convert moin file (first argument) to doku file (second argument), namespace for attachments is the third argument (default - (.)) sub ConvertMoinToDoku { die "ConvertMoinToDoku: I need 2 or 3 arguments!\n" if (@_ != 2 and @_ != 3); my $moin = shift @_; my $doku = shift @_; my $namespace = shift @_ // '.'; #/#3rd argument or (.) open INFILE, "<", $moin or die "Can't open '$moin' ($!)!\n"; open OUTFILE, ">", $doku or die "Can't open '$doku' ($!)!\n"; my $intend = 0; my $is_table = 0; my $is_block = 0; my $block_sep_len = 3; # length of block separator ({{{), in can be >= 3 my $is_list = 0; my @table = (); my @block = (); MOINSCAN: while (<INFILE>) { # First of all remove all end whitespaces except \n s/\s*$/\n/; # Tables if (/\s*(?<line>\|\|.+\|\|\n)/ && !$is_block) { $is_table = 1; $is_list = 0; $_ = $+{line}; } elsif ($is_table) { $is_table = 0; @table = tableReplacement(@table); print OUTFILE @table; @table = (); } # Code blocks parser s/\{{3}(.+?)}{3}/%%$1%%/g unless $is_block; # first remove all ignored blocks if (/(?<text>.*?)\s*(?<sep>\{{3,})(?<mod>.*\n)/ && !$is_block && !$is_table) { $block_sep_len = length $+{sep}; $is_block = 1; if ($is_list) { print OUTFILE "\n" } $is_list = 0; @block = ($+{mod}); next MOINSCAN unless ($+{text} && $+{text} !~ /^\s+$/); $_ = "$+{text}\n"; } elsif (/(?<data>.+?)?}{$block_sep_len}\s*(?<text>.*)\n/ && $is_block) { $is_block = 0; push @block, "$+{data}\n" if $+{data}; @block = blockReplacement(@block); print OUTFILE @block; @block = (); next MOINSCAN unless $+{text}; $_ = "$+{text}\n"; } elsif ($is_block) { push @block, $_; next MOINSCAN; } # Processing instructions s/##(.*)\n//; # comments s/#(pragma|format|redirect|refresh|language)(.*)\n//i; # remove all s/#deprecated(.*)\n/<note warning>This page is deprecated<note>\n/i; # deprecated # Other elements s/(<<BR>>)|(\[\[BR]])/\\\\ /g; # break s/^\s*-{4,}\s*$/----\n/g; # horizontal line s#^(.*)/\*(.+?)\*/(.*)\n#$1\n>$2\n$3\n#; # inline comments # Macros and another foolish - simply remove s/<<.+?>>//g; # macros # Headings my $s = "~!@>{"; s/^\s*=====\s*(.+?)\s*=====\s*$/$s$s $1 $s$s\n/g; # level 5 s/^\s*====\s*(.+?)\s*====\s*$/$s$s$s $1 $s$s$s\n/g; # level 4 s/^\s*===\s*(.+?)\s*===\s*$/$s$s$s$s $1 $s$s$s$s\n/g; # level 3 s/^\s*==\s*(.+?)\s*==\s*$/$s$s$s$s$s $1 $s$s$s$s$s\n/g; # level 2 s/^\s*=\s*(.+?)\s*=\s*$/$s$s$s$s$s$s $1 $s$s$s$s$s$s\n/g; # level 1 s/\Q$s\E/=/g; # Links s/ (?<br>[\[\{])\g{br} # opening brackets (?<target>[^\|]+?) # target (?:\|(?<text> # text (?(?=\{\{).+?}}|[^\|]+?) # test if text is an image link ))? (?:\|(?<params>[^\|]+?))? # parameters (??{ &inv($+{br}) x 2 }) # closing brackets / &linkReplacement($namespace,$+{br},$+{target},$+{text},$+{params}) /gxe; # CamelCase links my $camel = '(?<![\\[!:])\b[A-Z]+[a-z]+[A-Z][A-Za-z]*\b'; # CamelCase link regex s#($camel)/($camel)#[[$1:$2]]#g; # CamelCase to namespace s#\.\./($camel)#[[$1]]#g; # (very)strange CamelCase s#/($camel)#[[$namespace:$1]]#g; # CamelCase to subpage s#($camel)#[[$1]]#g; # simlpe CamelCase # Avoid automating linking - simply remove s{''''''}<>g; s{``}<>g; s{!([A-Z]\w+)}<$1>g; # Text formatting s{'''''(.+?)'''''}<**//$1//**>g; # bold and italic s{'''(.+?)'''}<**$1**>g; # bold s{''(.+?)''}<//$1//>g; # italic s{`(.+?)`}<''$1''>g; # monospaced s{,,(.+?),,}{<sub>$1</sub>}g; # sub index s{\^(.+?)\^}{<sup>$1</sup>}g; # sup index s{--\((.+?)\)--}{<del>$1</del>}g; # strike through text # Unsupported text formating - simply remove s{~-(.+?)-~}<$1>g; # smaller text s{~\+(.+?)\+~}<$1>g; # larger text # Lists and intends if (/^(?<intend>\s+)\*\s+(?<value>\S.*)\n/) { # dotted list if ($is_list) { print OUTFILE "\n" } print OUTFILE " "x(2*length($+{intend})),"* "; $_ = $+{value}; $intend = length $+{intend}; $is_list = 1; } elsif (/^(?<intend>\s+)[1aAiI]\.(#\d+)?\s+(?<value>\S.*)\n/) { # numeric list if ($is_list) { print OUTFILE "\n" } print OUTFILE " "x(2*length($+{intend})),"- "; $_ = $+{value}; $intend = length $+{intend}; $is_list = 1; } elsif (/^(?<intend>\s+)(?<key>\S[^\[\{]+)::\s+(?<value>\S.*)\n/) { # definition list if ($is_list) { print OUTFILE "\n" } print OUTFILE "**$+{key}**\n"; print OUTFILE " * "; $_ = $+{value}; $intend = length $+{intend}; $is_list = 1; } elsif (/^(?<intend>\s+)(?<key>\S[^\[\{]+)::\s*/) { # definition if ($is_list) { print OUTFILE "\n" } print OUTFILE "**$+{key}**\n"; $_=""; $is_list = 0; } elsif (/^(?<intend>\s+)::\s*(?<value>\S.*)\n/) { # description if ($is_list) { print OUTFILE "\n" } print OUTFILE " * "; $_ = $+{value}; $intend = length $+{intend}; $is_list = 1; } elsif (/^(?<intend>\s+)(\.\s+)?\S/) { # simple leading whitespaces my $curr_intend = length $+{intend}; if ($curr_intend != $intend and $is_list) { print OUTFILE "\n\n"; $is_list = 0; } elsif ($curr_intend != $intend and !$is_list) { print OUTFILE "\n" } elsif ($curr_intend == $intend and $is_list) { print OUTFILE " "; s/\n$//; } $intend = $curr_intend; s/^\s*(\.\s*)?(?=\S)//; } else { # string witout leading whitespaces if ($is_list && ! /^\n/) { print OUTFILE "\n\n" } elsif ($intend) { print OUTFILE "\n" } $intend = 0; $is_list = 0; } # Smileys ;) foreach $smile (keys %smileys) { s/(\s|\A)\Q$smile\E(\s)/ $smileys{$smile}$2/g; } # Ok, print all results of abracadabra to file if we need it if ($is_block && $is_list) { $_ .= "\n" } if (!$is_table) { print OUTFILE $_ } else { push @table, $_ } } if ($is_table) { @table = tableReplacement(@table); print OUTFILE @table; } if ($is_block) { @block = blockReplacement(@block); print OUTFILE @block; } close INFILE or die "error\n"; close OUTFILE or die "error\n"; } # ----------------------------- End of Convert Function --------------------------------------- # ConvertMoinToDoku @ARGV;
Используется это дело так:
./convert.pl moin_file doku_file [namespace]
namespace
- это пространство имён, которое надо пихать в ссылки на картинки, если его не указать будет использоваться '.'.
К сожалению, оно не совсем корректно работает с таблицами (не учитывает склейку и выравнивание, первое является достаточно критичным), возможно потом допишу. Если вы вдруг будете это использовать - напишите мне о найденных ошибках или хотя бы оставьте тут комментарий.
- таблицы
- http://moinmo.in/HelpOnEditing - проверить, всё ли учли
Абсолютно то же самое, только переписанное на Python, вдруг кому пригодится. Те же комментарии, таблицы обрабатываются не до конца и нужно не забыть заменить <!/code> на </code>:
#!/usr/bin/env python # coding=utf8 # Author: Vadim Nevorotin (malamut@ubuntu.ru) # License: GPLv3 # Rus: # Скрипт конвертации разметки MoinMoin в разметку DokuWiki # Использование: moin2doku.py moin_file doku_file [namespace] # namespace - это пространство имён для всех ссылок на вложения (картинки, например) # Скрипт не до конца обрабатывает таблицы, не учитывается склейка ячеек и выравнивание # Так же не добавлена конвертация новой возможности moin: {{{#!wiki caution }}} и прочих выделений (http://moinmo.in/HelpOnAdmonitions) # Всё остальное с http://moinmo.in/HelpOnEditing на 11.2009 конвертируется в полном объёме # Eng: # Usage: ./moin2doku.py moin_file doku_file [namespace] # namespace - for all links to images ({{namespace:image.png}}) import sys import re from os.path import isfile # Hash for smileys replacement smileys = { #moin doku 'X-(' : ':-X', ':D' : ':-D', '<:(' : ':-?', ':o' : ':-O', ':(' : ':-(', ':)' : ':-)', 'B)' : '8-)', ':))' : ':-P', ';)' : ';-)', '/!\\' : ':!:', '<!>' : ':!:', '(!)' : ':!:', ':-?' : ':-P', ':\\' : ':-\\', '>:>' : '^_^', '|)' : ':-|', ':-(' : ':-(', ':-)' : ':-)', 'B-)' : '8-)', ':-))' : ':-P', ';-)' : ';-)', '|-)' : ':-|', '(./)' : 'LOL', '{OK}' : ':!:', '{X}' : ':!:', '{i}' : ':!:', '{1}' : '<1>', '{2}' : '<2>', '{3}' : '<3>', '{*}' : '<ubu>', '{o}' : '<circ>', } # Convert functions def inv(br): if br == '{': return '}' if br == '[': return ']' return br def linkReplacement(namespace,br,target,text): if not target: print >> sys.stderr, "Fatal error" sys.exit(1) m_att = re.match('(?:(?:attachment|drawing):)(?P<att>.+)$',target) m_inter = re.match('(.+?):(?!//)(.+)$',target) # Attachments links if m_att: if m_att.group('att'): att = re.sub('(.+)/','',m_att.group('att')) # leave only filename, without namespaces else: att = m_att.groups('att') if text: return '{{'+namespace+':'+att+'|'+text+'}}' else: return '{{'+namespace+':'+att+'}}' # InterWiki links if m_inter: if text: return '[['+m_inter.group(1)+'>'+m_inter.group(2)+'|'+text+']]' else: return '[['+m_inter.group(1)+'>'+m_inter.group(2)+']]' # All other links simply returned in doku format if not re.search('://', target): # if target not an internet link replace all / to : (for namespaces) target = re.sub('/',':',target) if text: return br + br + target + '|' + text + inv(br) * 2 else: return br + br + target + inv(br) * 2 def tableReplacement(table): for i in range(len(table)): line = table[i] line = re.sub('\|\|(?:\s*(<.+?>))?\s*(.+?)\s*(?=\|\|)', lambda m: '||' + m.group(1) + m.group(2) if m.group(1) else '||' + m.group(2), line) line = re.sub('((?:\|\|){2,})(.+?)(?=\|\|)', lambda m: '|| '+m.group(2)+' ' + '|' * (len(m.group(1)) - 2), line) line = re.sub('\|\|(\s*)(?:<.+?>\s*?)+(.*?)(?=\|\|)','||\\1\\2', line) line = re.sub('\|\|','|', line) table[i] = line size = len(table) i = 0 while i < size: if re.match('\|+$',table[i]): table.pop(i) size=size-1 else: i=i+1 return table def blockReplacement(block): if block and not block[0].strip(): block.pop(0) if not block[-1].strip(): block.pop() if not block: block.append('<code>\n') elif re.match('\s*#!python',block[0]): block.pop(0) block.insert(0,'<code python>\n') elif re.match('\s*#!cplusplus',block[0]): block.pop(0) block.insert(0,'<code cpp>\n') elif re.match('\s*#!java',block[0]): block.pop(0) block.insert(0,'<code java>\n') elif re.match('\s*#!pascal',block[0]): block.pop(0) block.insert(0,'<code pascal>\n') elif re.match('\s*#!.+',block[0]): block.pop(0) block.insert(0,'<code>\n') else: block.insert(0,'<code>\n') block.append('<!/code>') return block def ConvertMoinToDoku(moin_file,doku_file,namespace): try: infile = open(moin_file,"r") outfile = open(doku_file,"w") except: print >> sys.stderr, "Open file error!" sys.exit(1) content=infile.readlines() infile.close() # Conversion st = '~@!' # For headings conversion camel = '(?<![\\[!:])\b[A-Z]+[a-z]+[A-Z][A-Za-z]*\b' # CamelCase regexp conversion = ( # Processing instructions ('^##(.*)\n',''), ('^#(pragma|format|redirect|refresh|language)(.*)\n',''), ('^#deprecated(.*)\n','<note warning>This page is deprecated<note>\n'), # Other elements ('(<<BR>>)|(\[\[BR]])','\\\\ '), ('^\s*-{4,}\s*$','----\n'), ('^(.*)/\*(.+?)\*/(.*)\n','\\1\n>\\2\n\\3\n'), # inline comments # Macros and another foolish - simply remove ('<<.+?>>',''), # Headings ('^\s*=====\s*(.+?)\s*=====\s*$',st*2 + ' \\1 ' + st*2 + '\n'), ('^\s*====\s*(.+?)\s*====\s*$',st*3 + ' \\1 ' + st*3 + '\n'), ('^\s*===\s*(.+?)\s*===\s*$',st*4 + ' \\1 ' + st*4 + '\n'), ('^\s*==\s*(.+?)\s*==\s*$',st*5 + ' \\1 ' + st*5 + '\n'), ('^\s*=\s*(.+?)\s*=\s*$',st*6 + ' \\1 ' + st*6 + '\n'), (st,'='), # CamelCase links ('('+camel+')/('+camel+')','[[\\1:\\2]]'), ('\.\./('+camel+')','[[\\1]]'), ('/('+camel+')','[['+namespace+':\\1]]'), ('('+camel+')','[[\\1]]'), # Avoid automating linking - simply remove ("''''''",''), ('``',''), ('!([A-Z]\w+)','\\1'), # Text formatting ("'''''(.+?)'''''",'**//\\1//**'), ("'''(.+?)'''",'**\\1**'), ("''(.+?)''",'//\\1//'), ('`(.+?)`',"''\\1''"), (',,(.+?),,','<sub>\\1</sub>'), ('\^(.+?)\^','<sup>\\1</sup>'), ('--\((.+?)\)--','<del>\\1</del>'), # Unsupported text formating - simply remove ('~-(.+?)-~','\\1'), ('~\+(.+?)\+~','\\1') ) # Values for conversion intend = 0 is_table = 0 is_block = 0 block_sep_len = 3 is_list = 0 table = [] block = [] for i in range(len(content)): line = content[i] # First of all remove all end whitespaces except \n line = re.sub('\s*$','\n',line) # Tables m = re.search('\s*(?P<line>\|\|.+\|\|\n)',line) if m and not is_block: is_table = 1 is_list = 0 line = m.group('line') elif is_table: is_table = 0 table = tableReplacement(table) outfile.writelines(table) table = [] # Code blocks parser if not is_block: line = re.sub('\{{3}(.+?)}{3}','%%\\1%%',line) m_open = re.search('(?P<text>.*?)\s*(?P<sep>\{{3,})(?P<mod>.*\n)',line) m_close = re.search('(?P<data>.+?)?}{'+str(block_sep_len)+'}\s*(?P<text>.*)\n',line) if m_open and not is_block and not is_table: block_sep_len = len(m_open.group('sep')) is_block = 1 if is_list: outfile.write('\n') is_list = 0 block = [m_open.group('mod')] if not ( m_open.group('text') and not re.match('\s+$',m_open.group('text')) ): continue line = m_open.group('text') + '\n' elif m_close and is_block: is_block = 0 if m_close.group('data'): block.append(m_close.group('data') + '\n') block = blockReplacement(block) outfile.writelines(block) block = [] if not m_close.group('text'): continue line = m_close.group('text') + '\n' elif is_block: block.append(line) continue # Links # Unfortunately, Python is not a Perl, so we can't convert links using only one regexp... link = '' s = '' br = '' is_link = 0 last = len(line)-1 for i in range(len(line)): if not is_link and i != last and line[i] == '[' and line[i+1] == '[': is_link = 1 link = br = '[' elif not is_link and i != last and line[i] == '{' and line[i+1] == '{': is_link = 1 link = br = '{' elif is_link and line[i] == inv(br) and line[i-1] == inv(br): is_link = 0 link = link + line[i] m = re.match('(?P<target>[^\|]+)(?:\|(?P<text>[^\|]+))?',link[2:-2]) link = linkReplacement(namespace,link[0],m.group('target'),m.group('text')) s = s + link elif is_link: link = link + line[i] else: s = s + line[i] line = s # Now convert syntax for r in conversion: reg=re.compile(r[0],re.I) line=reg.sub(r[1],line) # Lists and intends m_dotted = re.match('(?P<intend>\s+)\*\s+(?P<value>\S.*)\n',line) m_numeric = re.match('(?P<intend>\s+)[1aAiI]\.(#\d+)?\s+(?P<value>\S.*)\n',line) m_defs = re.match('(?P<intend>\s+)(?P<key>\S[^\[\{]+)::\s+(?P<value>\S.*)\n',line) m_def = re.match('(?P<intend>\s+)(?P<key>\S[^\[\{]+)::\s*',line) m_desc = re.match('(?P<intend>\s+)::\s*(?P<value>\S.*)\n',line) m_white = re.match('(?P<intend>\s+)(\.\s+)?\S',line) if m_dotted: if is_list: outfile.write('\n') outfile.write(" "*(2*len(m_dotted.group('intend')))+"* ") line = m_dotted.group('value') intend = len(m_dotted.group('intend')) is_list = 1 elif m_numeric: if is_list: outfile.write('\n') outfile.write(" "*(2*len(m_numeric.group('intend')))+"- ") line = m_numeric.group('value') intend = len(m_numeric.group('intend')) is_list = 1 elif m_defs: if is_list: outfile.write('\n') outfile.write('**'+m_defs.group('key')+'**\n') outfile.write(' * ') line = m_defs.group('value') intend = len(m_defs.group('intend')) is_list = 1 elif m_def: if is_list: outfile.write('\n') outfile.write('**'+m_def.group('key')+'**\n') line = '' is_list = 0 elif m_desc: if is_list: outfile.write('\n') outfile.write(' * ') line = m_desc.group('value') intend = len(m_desc.group('intend')) is_list = 1 elif m_white: curr_intend = len(m_white.groups('intend')) if curr_intend != intend and is_list: outfile.write('\n\n') is_list = 0 elif curr_intend != intend and not is_list: outfile.write('\n') elif curr_intend == intend and is_list: outfile.write(' ') line = line.rstrip() intend = curr_intend line = re.sub('^\s*(\.\s*)?(?=\S)','',line) else: if is_list and not re.match('\n',line): outfile.write('\n\n') elif intend: outfile.write('\n') intend = 0 is_list = 0 # Smileys $) for smile in smileys.keys(): line = re.sub('(\s|\A)'+re.escape(smile)+'(\s)',' '+smileys[smile]+'\\2',line) # Finally... if is_block and is_list: line += '\n' if not is_table: outfile.write(line) else: table.append(line) # If we haven't close some things if is_table: table = tableReplacement(table) outfile.writelines(table) if is_block: block = blockReplacement(block) outfile.writelines(block) outfile.close() # Main script... def PrintHelp(): print """Usage: moin2doku moin_file doku_file [namespace] Convert MoinMoin page to Doku, using namespace for pictures""" sys.exit(0) def PrintParameterError(): print >> sys.stderr, "Incorrect parameters! Use --help to read more information." sys.exit(1) def CheckParameters(moin_file,doku_file): if not isfile(moin_file): print >> sys.stderr, "Moin file doesn't exists!" sys.exit(1) if not isfile(doku_file): print >> sys.stderr, "Doku file doesn't exists!" sys.exit(1) if __name__ == '__main__': if len(sys.argv) > 1: if sys.argv[1] in ('-h', '--help'): PrintHelp() elif len(sys.argv) > 2: moin_file = sys.argv[1] doku_file = sys.argv[2] if len(sys.argv) > 3: namespace = sys.argv[3] else: namespace = '.' else: PrintParameterError() else: PrintParameterError() CheckParameters(moin_file,doku_file) ConvertMoinToDoku(moin_file,doku_file,namespace)
Улучшенная версия
Я ж ленивый, поэтому я написал 'обёртку' к представленной выше функции конвертации так, что бы её было удобно вызывать с различными аргументами и, кроме всего прочего, этот вариант умеет скачивать страницы и вложения из интернета. Функция конвертации здесь та же, просто она сопровождается удобным способом её вызывать.
Описание см. в POD в коде. Не забудьте заменить <!/code> на </code>.
#!/usr/bin/perl -w use 5.010; # Для всяких выкрутасов, очень удобно use File::Path; # Для управления каталогами use File::Spec::Functions; # Для склейки путей use Getopt::Long; # Аргументики, разбираем аргументики use Pod::Usage; # Для документации (всё равно пашет через пень-колоду) =begin comment Сначала идёт основная функция конвертации одного файла в другой, потом всякая ерунда, связанная с выкачкой вложений из интернета и разбором аргументов. Основную функцию как мог перевёл на аглицкий. Аргументы описываются ниже в основной программе, всё это написано just for fun, но если найдёте ошибку - сообщите мне, или, как вариант, исправьте прям в этом скрипте самостоятельно и не забудьте поместить его обратно на help.ubuntu.ru =end comment =cut # ----------------------------------------------------------------------------------------------- # # Convert text file with MoinMoin Wiki syntax to DokuWiki syntax # # by Malamut (malamut@ubuntu.ru) (2009) # # ----------------------------------------------------------------------------------------------- # # Hash for smileys replacement %smileys = ( #moin doku 'X-(' => ':-X', ':D' => ':-D', '<:(' => ':-?', ':o' => ':-O', ':(' => ':-(', ':)' => ':-)', 'B)' => '8-)', ':))' => ':-P', ';)' => ';-)', '/!\\' => ':!:', '<!>' => ':!:', '(!)' => ':!:', ':-?' => ':-P', ':\\' => ':-\\', '>:>' => '^_^', '|)' => ':-|', ':-(' => ':-(', ':-)' => ':-)', 'B-)' => '8-)', ':-))' => ':-P', ';-)' => ';-)', '|-)' => ':-|', '(./)' => 'LOL', '{OK}' => ':!:', '{X}' => ':!:', '{i}' => ':!:', '{1}' => '<1>', '{2}' => '<2>', '{3}' => '<3>', '{*}' => '<ubu>', '{o}' => '<circ>', ); # For links regex sub inv { given ($_[0]) { when ("{") { return "}" } when ("[") { return "]" } } return $_[0]; } # Links sub linkReplacement { die "Oops!\n" unless @_ == 5; my ($namespace,$br,$target,$text,$params) = @_; # Attachments links if ($target =~ /^(?:(?:attachment|drawing):)(?<att>.+)$/) { (my $att = $+{att}) =~ s{(.+)/}{}; # leave only filename, without namespaces if ($text) { return "{{$namespace:$att|$text}}" } else { return "{{$namespace:$att}}" } } # InterWiki links if ($target =~ m#^(.+?):(?!//)(.+)$#) { # all with : except internet links (containing ://) if ($text) { return "[[$1>$2|$text]]" } else { return "[[$1>$2]]" } } # All other links simply returned in doku format $target =~ s#/#:#g if $target !~ m#://#; # if target not an internet link replace all / to : (for namespaces) if ($text) { return "$br$br$target|$text" . inv($br) x 2 } else { return "$br$br$target" . inv($br) x 2 } } # Blocks sub blockReplacement { for (my $i = 0; $i < @_ && $_[$i] =~ /^\$*$/; $i++ ) { shift @_ } # delete empty lines for (my $i = -1; -$i <= @_ && $_[$i] =~ /^\$*$/; $i-- ) { pop @_ } # delete empty lines if (!@_) { unshift @_, "<code>\n" } elsif ($_[0] =~ /^\s*#!python/) { shift @_; unshift @_, "<code python>\n" } elsif ($_[0] =~ /^\s*#!cplusplus/) { shift @_; unshift @_, "<code cpp>\n" } elsif ($_[0] =~ /^\s*#!java/) { shift @_; unshift @_, "<code java>\n" } elsif ($_[0] =~ /^\s*#!pascal/) { shift @_; unshift @_, "<code pascal>\n" } elsif ($_[0] =~ /^\s*#.+/) { shift @_; unshift @_, "<code>\n" } else { unshift @_, "<code>\n" } push @_, "<!/code>\n"; return @_; } # Tables sub tableReplacement { @table = @_; for (@table) { s/\|\|(?:\s*(<.+?>))?\s*(.+?)\s*(?=\|\|)/ # delete all whitespaces if ($1) { "||$1$2" } else { "||$2" } /ge; s/((?:\|\|){2,})(.+?)(?=\|\|)/ # move few || to another side of cell and center "|| $2 " . '|' x (length($1) - 2) /ge; # Span # TODO # Aligment # TODO #s/\|\|.*?(?:(?:<\(>)|(?:<style="text-align: left">))(?=\|\|)//g; # left #s/<:>//g; # center #s/<\)>//g; # right # Ok, thats all s/\|\|(\s*)(?:<.+?>\s*?)+(.*?)(?=\|\|)/||$1$2/g; # remove all tags, we really don't need it s/\|\|/|/g; # finally replace || to | } # Remove all empty strings for (0..$#table) { if ($table[$_] =~ /^\|+$/) { splice @table, $_, 1 } } return @table; } # Convert moin file (first argument) to doku file (second argument), namespace for attachments is the third argument (default - (.)) sub ConvertMoinToDoku { die "ConvertMoinToDoku: I need 2 or 3 arguments!\n" if (@_ != 2 and @_ != 3); my $moin = shift @_; my $doku = shift @_; my $namespace = shift @_ // '.'; #/#3rd argument or (.) open INFILE, "<", $moin or die "Can't open '$moin' ($!)!\n"; open OUTFILE, ">", $doku or die "Can't open '$doku' ($!)!\n"; my $intend = 0; my $is_table = 0; my $is_block = 0; my $block_sep_len = 3; # length of block separator ({{{), in can be >= 3 my $is_list = 0; my @table = (); my @block = (); MOINSCAN: while (<INFILE>) { # First of all remove all end whitespaces except \n s/\s*$/\n/; # Tables if (/\s*(?<line>\|\|.+\|\|\n)/ && !$is_block) { $is_table = 1; $is_list = 0; $_ = $+{line}; } elsif ($is_table) { $is_table = 0; @table = tableReplacement(@table); print OUTFILE @table; @table = (); } # Code blocks parser s/\{{3}(.+?)}{3}/%%$1%%/g unless $is_block; # first remove all ignored blocks if (/(?<text>.*?)\s*(?<sep>\{{3,})(?<mod>.*\n)/ && !$is_block && !$is_table) { $block_sep_len = length $+{sep}; $is_block = 1; if ($is_list) { print OUTFILE "\n" } $is_list = 0; @block = ($+{mod}); next MOINSCAN unless ($+{text} && $+{text} !~ /^\s+$/); $_ = "$+{text}\n"; } elsif (/(?<data>.+?)?}{$block_sep_len}\s*(?<text>.*)\n/ && $is_block) { $is_block = 0; push @block, "$+{data}\n" if $+{data}; @block = blockReplacement(@block); print OUTFILE @block; @block = (); next MOINSCAN unless $+{text}; $_ = "$+{text}\n"; } elsif ($is_block) { push @block, $_; next MOINSCAN; } # Processing instructions s/##(.*)\n//; # comments s/#(pragma|format|redirect|refresh|language)(.*)\n//i; # remove all s/#deprecated(.*)\n/<note warning>This page is deprecated<note>\n/i; # deprecated # Other elements s/(<<BR>>)|(\[\[BR]])/\\\\ /g; # break s/^\s*-{4,}\s*$/----\n/g; # horizontal line s#^(.*)/\*(.+?)\*/(.*)\n#$1\n>$2\n$3\n#; # inline comments # Macros and another foolish - simply remove s/<<.+?>>//g; # macros # Headings my $s = "~!@>{"; s/^\s*=====\s*(.+?)\s*=====\s*$/$s$s $1 $s$s\n/g; # level 5 s/^\s*====\s*(.+?)\s*====\s*$/$s$s$s $1 $s$s$s\n/g; # level 4 s/^\s*===\s*(.+?)\s*===\s*$/$s$s$s$s $1 $s$s$s$s\n/g; # level 3 s/^\s*==\s*(.+?)\s*==\s*$/$s$s$s$s$s $1 $s$s$s$s$s\n/g; # level 2 s/^\s*=\s*(.+?)\s*=\s*$/$s$s$s$s$s$s $1 $s$s$s$s$s$s\n/g; # level 1 s/\Q$s\E/=/g; # Links s/ (?<br>[\[\{])\g{br} # opening brackets (?<target>[^\|]+?) # target (?:\|(?<text> # text (?(?=\{\{).+?}}|[^\|]+?) # test if text is an image link ))? (?:\|(?<params>[^\|]+?))? # parameters (??{ &inv($+{br}) x 2 }) # closing brackets / &linkReplacement($namespace,$+{br},$+{target},$+{text},$+{params}) /gxe; # CamelCase links my $camel = '(?<![\\[!:])\b[A-Z]+[a-z]+[A-Z][A-Za-z]*\b'; # CamelCase link regex s#($camel)/($camel)#[[$1:$2]]#g; # CamelCase to namespace s#\.\./($camel)#[[$1]]#g; # (very)strange CamelCase s#/($camel)#[[$namespace:$1]]#g; # CamelCase to subpage s#($camel)#[[$1]]#g; # simlpe CamelCase # Avoid automating linking - simply remove s{''''''}<>g; s{``}<>g; s{!([A-Z]\w+)}<$1>g; # Text formatting s{'''''(.+?)'''''}<**//$1//**>g; # bold and italic s{'''(.+?)'''}<**$1**>g; # bold s{''(.+?)''}<//$1//>g; # italic s{`(.+?)`}<''$1''>g; # monospaced s{,,(.+?),,}{<sub>$1</sub>}g; # sub index s{\^(.+?)\^}{<sup>$1</sup>}g; # sup index s{--\((.+?)\)--}{<del>$1</del>}g; # strike through text # Unsupported text formating - simply remove s{~-(.+?)-~}<$1>g; # smaller text s{~\+(.+?)\+~}<$1>g; # larger text # Lists and intends if (/^(?<intend>\s+)\*\s+(?<value>\S.*)\n/) { # dotted list if ($is_list) { print OUTFILE "\n" } print OUTFILE " "x(2*length($+{intend})),"* "; $_ = $+{value}; $intend = length $+{intend}; $is_list = 1; } elsif (/^(?<intend>\s+)[1aAiI]\.(#\d+)?\s+(?<value>\S.*)\n/) { # numeric list if ($is_list) { print OUTFILE "\n" } print OUTFILE " "x(2*length($+{intend})),"- "; $_ = $+{value}; $intend = length $+{intend}; $is_list = 1; } elsif (/^(?<intend>\s+)(?<key>\S[^\[\{]+)::\s+(?<value>\S.*)\n/) { # definition list if ($is_list) { print OUTFILE "\n" } print OUTFILE "**$+{key}**\n"; print OUTFILE " * "; $_ = $+{value}; $intend = length $+{intend}; $is_list = 1; } elsif (/^(?<intend>\s+)(?<key>\S[^\[\{]+)::\s*/) { # definition if ($is_list) { print OUTFILE "\n" } print OUTFILE "**$+{key}**\n"; $_=""; $is_list = 0; } elsif (/^(?<intend>\s+)::\s*(?<value>\S.*)\n/) { # description if ($is_list) { print OUTFILE "\n" } print OUTFILE " * "; $_ = $+{value}; $intend = length $+{intend}; $is_list = 1; } elsif (/^(?<intend>\s+)(\.\s+)?\S/) { # simple leading whitespaces my $curr_intend = length $+{intend}; if ($curr_intend != $intend and $is_list) { print OUTFILE "\n\n"; $is_list = 0; } elsif ($curr_intend != $intend and !$is_list) { print OUTFILE "\n" } elsif ($curr_intend == $intend and $is_list) { print OUTFILE " "; s/\n$//; } $intend = $curr_intend; s/^\s*(\.\s*)?(?=\S)//; } else { # string witout leading whitespaces if ($is_list && ! /^\n/) { print OUTFILE "\n\n" } elsif ($intend) { print OUTFILE "\n" } $intend = 0; $is_list = 0; } # Smileys ;) foreach $smile (keys %smileys) { s/(\s|\A)\Q$smile\E(\s)/ $smileys{$smile}$2/g; } # Ok, print all results of abracadabra to file if we need it if ($is_block && $is_list) { $_ .= "\n" } if (!$is_table) { print OUTFILE $_ } else { push @table, $_ } } if ($is_table) { @table = tableReplacement(@table); print OUTFILE @table; } if ($is_block) { @block = blockReplacement(@block); print OUTFILE @block; } close INFILE or die "error\n"; close OUTFILE or die "error\n"; } # ----------------------------- End of Convert Function --------------------------------------- # # ------------------------------------- Функции ----------------------------------------------- # # Создаёт директорию если её ещё нет (должна быть такая функция, но я не знаю :( ). Параметр - имя создаваемой директории sub CreateDir { die "CreateDir: нужен один аргумент!\n" unless @_ == 1; if (! -e -d $_[0]) { mkdir $_[0] or die "Не могу создать '$_[0]'! ($!)\n"; } } # Получает нужный файл из интернета с вики и пишет его в нужное местоъ # Первый параметр - адрес для скачивания # Второй параметр - файл, в который нужно сохранить результат sub GetUrlFile { die "GetUrlFile: Должно быть ровно два аргумента\n" unless @_ == 2; my ($url, $filename) = @_; my $command = "wget -nv --user-agent 'Opera' -O '$filename' '$url'"; if (system $command ) { warn "Невозможно получить или сохранить $url\n"; return undef; } } # Получает все вложения из указанного файла по указанному адресу # Первый параметр - имя файла с moin разметкой # Второй параметр - базовый интернет-адрес вики # Третий параметр - адрес страницы на вики вместе с неймспейсом если нужно # Четвёртый параметр - папка куда складывать все скачанные файлы sub GetAttachments { die "GetAttachments: Должно быть ровно четыре аргумента\n" unless @_ == 4; my ($moin,$base_url,$page_url,$atts_dir) = @_; open INFILE, "<", $moin or die "Не могу открыть файл '$moin'!\n"; while (<INFILE>) { if (my @atts = /([\{\[])\1(?:(?:attachment|drawing):)(.*?)(??{ &inv($1) x 2 })/g) { for (my $i = 1; $i < @atts; $i+=2) { my ($namespace, $file) = $atts[$i] =~ m#^(?:(.+)/)?([^/]+?)$#; my $url = $namespace ? "$base_url/$namespace?action=AttachFile&do=get&target=$file" : "$base_url/$page_url?action=AttachFile&do=get&target=$file"; CreateDir($atts_dir); GetUrlFile($url,catfile($atts_dir,$file)); } } } close INFILE; } # ---------------------------- Программа ------------------------------------------------------- # =head1 NAME moin2doku.pl - преобразует разметку из MoinMoin в DokuWiki =head1 SYNOPSIS moin2doku.pl [options] files|urls =cut # Всякие (не)нужные переменные... my $help = 0; # показать помощь my $urls = 0; # воспринимать аргументы как url, а не как файлы (соответствует ключу -u) my $verbose = 0; # подробный вывод my $working_dir = ""; # директория для сохранения всех результатов my $use_pagename_dir = 0; # использовать ли в качестве $working_dir имя страницы для каждой обработанной страницы my $attachments_dir = ""; # директория для всяких картинок и прочего из {{attachment}} my $links = 1; # преобразовывать ли внутренние ссылки my $wiki_adress = "localhost"; # адрес вики my $wiki_namespace = ""; # неймспейс на вики откуда брать страницы my $moin_file = ""; # имя файла с моин разметкой для закачки при -u, если не указано - используется ИмяСтраницы.moin my $doku_file = ""; # имя файла с доку разметкой, если не указано - используется ИмяСтраницы.txt или ИмяСтраницы.doku # Задаёт нужные параметры для русских статей с wiki.ubuntu.com sub ru_official_wiki { $wiki_adress = 'https://wiki.ubuntu.com'; $wiki_namespace = 'RussianDocumentation'; } Getopt::Long::Configure ("bundling"); # Конфигурирование getopt дабы воспринимать склейку коротких аргументов GetOptions( "verbose|v+" => \$verbose, # TODO: подробный вывод "delete-internal-links|l" => \$links, # TODO: удалять внутренние ссылки, а не преобразовывать "urls|u" => \$urls, # воспринимать параметры как интернет-ссылки "moin-file|m=s" => \$moin_file, # имя файла с moin разметкой, по умолчанию - ИмяСтраницы.moin "doku-file|d=s" => \$doku_file, # имя файла с doku разметкой, по умолчанию - ИмяСтраницы.txt или ИмяСтраницы.doku если занято "use-pagename-dir|n" => \$use_pagename_dir, # использовать для каждой страницы её имя в качестве working-directory "wiki-adress|w=s" => \$wiki_adress, # адрес вики, с которой качаем "wiki-namespace|N=s" => \$wiki_namespace, # неймспейс на вики, в котором искать страницы "working-directory|D=s" => \$working_dir, # директория для сохранения всех результатов "attachments-directory|A=s" => \$attachments_dir, # поддиректория для картинок и по совместительству неймспейс для вложений, по умолчанию - имя страницы "ru-official-wiki|r" => \&ru_official_wiki, # ага, всем лень, мне тоже "help|h" => \$help); # ну понятно =head1 OPTIONS =over 8 =item B<-A имя, --attachments-directory=имя> Имя директории для складывания всех скачанных вложений при B<-u>, а так же имя пространства имён, указываемого при конвертировании всех вложений. =item B<-d файл, --doku-file=файл> Файл для сохранения сконвертированного в DokuWiki разметку результата. По умолчанию используется ИмяСтраницы.txt или, если такой файл уже существует, ИмяСтраницы.doku =item B<-D директория, --working-directory=директория> Директория в которую будут сложены все результаты. По умолчанию всё складывается в текущую. =item B<-l, --delete-internal-links> Удалять все внутренние ссылки, а не преобразовывать их (не работает) =item B<-m файл, --moin-file=файл> Имя файла, в котором будет сохранён скачанный исходный код страницы в moin разметке. По умолчанию для каждой статьи используется ИмяСтатьи.moin =item B<-n, --use-pagename-dir> Использовать для каждой страницы её имя в качестве директории для сохранения всех результатов. =item B<-N имя, --wiki-namespace=имя> Путь до пространства имён, в котором будут искаться указанные статьи, от корня вики. =item B<-u, --urls> Воспринимать все агрументы как имена статей на вики по адресу, заданному ключами B<-N> и B<-w> =item B<-v, --verbose> Подробный вывод (не работает) =item B<-w адрес, --wiki-adress> Интернет-адрес вики, с которой брать страницы при указании ключа B<-u> =back =cut if ($help) { pod2usage(-verbose => 1); } unless (@ARGV) { pod2usage(-verbose => 1); } if (!$use_pagename_dir && ($moin_file || $doku_file) && @ARGV > 1) { warn "При указании жёстких имён для moin или doku файлов указанный файл будет использован для всех аргументов ". "и будет в итоге содержать текст только для последней обработанной страницы\n"; } if ($urls) { if ($working_dir && !$use_pagename_dir) { CreateDir($working_dir); chdir $working_dir or die "Невозможно сменить директорию на '$working_dir'! ($!)\n"; } for my $page (@ARGV) { if ($page =~ m<[/ ]>) { die "Имя страницы не может содержать символы '/', ' ' ($_)\n" } $wiki_adress =~ s#/$##; # Удаляем завершающий / если он имеется $wiki_namespace =~ s#/$##; # Удаляем завершающий / если он имеется if ($use_pagename_dir) { CreateDir($page); } # Получаем исходники страницы my $source_url = $wiki_namespace ? "$wiki_adress/$wiki_namespace/$page?action=raw" : "$wiki_adress/$page?action=raw"; my $moin_source = $moin_file ? $moin_file : "$page.moin"; $moin_source = catfile($page,$moin_source) if $use_pagename_dir; GetUrlFile($source_url,$moin_source); # Получаем вложения my $atts_dir = $attachments_dir ? $attachments_dir : $page; $atts_dir = catdir($page,$atts_dir) if $use_pagename_dir; GetAttachments($moin_source,$wiki_adress, $wiki_namespace ? "$wiki_namespace/$page" : $page, $atts_dir); # Теперь конвертируем my $doku_result = $doku_file ? $doku_file : "$page.txt"; $doku_result = catfile($page,$doku_result) if $use_pagename_dir; unless ($attachments_dir) { ConvertMoinToDoku($moin_source,$doku_result,$page) } else { ConvertMoinToDoku($moin_source,$doku_result,$attachments_dir) } } } else { for my $file (@ARGV) { # Берём имя страницы отбрасывая расширение файла my ($page) = $file =~ m#^(?:.*/)([^/]+?)(?:\.[^\./]+)?$#; # Выбираем имя выходного файла my $doku_result = $doku_file ? $doku_file : "$page.txt"; if ($use_pagename_dir) { CreateDir($page); $doku_result = catfile($page,$doku_result); } elsif ($working_dir) { CreateDir($working_dir); $doku_result = catfile($working_dir,$doku_result); } if (-e $doku_result) { $doku_result =~ s/\.txt$/.doku/ } # Конвертируем unless ($attachments_dir) { ConvertMoinToDoku($file,$doku_result,$page) } else { ConvertMoinToDoku($file,$doku_result,$attachments_dir) } } } __END__ =head1 DESCRIPTION описания пожалуй не будет - лень писать. Лучше всего использовать так: moin2doku -run статья для url и moin2doku -n файл для обычных файлов =head1 AUTHOR Скрипт написал Malamut just for fun :) Со всеми вопросами и пожеланиями обращайтесь: malamut@ubuntu.ru =head1 BUGS Некорректно работает параметр -h поскольку весь этот текст написан на русском, вообще что-то Pod::Usage как-то странно работает с русским текстом. На данный момент некорректно обрабатываются таблицы - не учитывается выравнивание и склейка через colspan и rowspan. Кроме того, не работают параметры B<-v> и B<-l>. =cut