OSDN Git Service

8f91a34d49e6b88e1dcd7ea55758ad508b61eabd
[wptscs/wpts.git] / Wptscs / Logics / MediaWikiTranslator.cs
1 // ================================================================================================
2 // <summary>
3 //      Wikipedia用の翻訳支援処理実装クラスソース</summary>
4 //
5 // <copyright file="MediaWikiTranslator.cs" company="honeplusのメモ帳">
6 //      Copyright (C) 2012 Honeplus. All rights reserved.</copyright>
7 // <author>
8 //      Honeplus</author>
9 // ================================================================================================
10
11 namespace Honememo.Wptscs.Logics
12 {
13     using System;
14     using System.Collections.Generic;
15     using System.IO;
16     using System.Linq;
17     using System.Net;
18     using System.Text;
19     using System.Windows.Forms;
20     using Honememo.Models;
21     using Honememo.Parsers;
22     using Honememo.Utilities;
23     using Honememo.Wptscs.Models;
24     using Honememo.Wptscs.Parsers;
25     using Honememo.Wptscs.Properties;
26     using Honememo.Wptscs.Utilities;
27     using Honememo.Wptscs.Websites;
28
29     /// <summary>
30     /// MediaWiki用の翻訳支援処理実装クラスです。
31     /// </summary>
32     public class MediaWikiTranslator : Translator
33     {
34         #region private変数
35
36         /// <summary>
37         /// <see cref="Translator.ItemTable"/>用ロックオブジェクト。
38         /// </summary>
39         private LockObject itemTableLock = new LockObject();
40
41         #endregion
42
43         #region コンストラクタ
44
45         /// <summary>
46         /// MediaWikiでの翻訳支援処理を行うトランスレータを作成。
47         /// </summary>
48         /// <remarks>
49         /// 別途プロパティに必要なパラメータを設定する必要あり。
50         /// 通常は<see cref="Translator.Create"/>にて設定ファイルから作成する。
51         /// </remarks>
52         public MediaWikiTranslator()
53         {
54             // このクラス用のロガーと、デフォルトの確認処理としてメッセージダイアログ版を設定
55             this.Logger = new MediaWikiLogger();
56             this.IsContinueAtInterwikiExisted = this.IsContinueAtInterwikiExistedWithDialog;
57         }
58
59         #endregion
60
61         #region デリゲート
62
63         /// <summary>
64         /// 対象記事に言語間リンクが存在する場合の確認処理を表すデリゲート。
65         /// </summary>
66         /// <param name="interwiki">言語間リンク先記事。</param>
67         /// <returns>処理を続行する場合<c>true</c>。</returns>
68         public delegate bool IsContinueAtInterwikiExistedDelegate(string interwiki);
69
70         #endregion
71
72         #region プロパティ
73
74         /// <summary>
75         /// 翻訳元言語のサイト。
76         /// </summary>
77         public new MediaWiki From
78         {
79             get
80             {
81                 return base.From as MediaWiki;
82             }
83
84             set
85             {
86                 base.From = value;
87             }
88         }
89
90         /// <summary>
91         /// 翻訳先言語のサイト。
92         /// </summary>
93         public new MediaWiki To
94         {
95             get
96             {
97                 return base.To as MediaWiki;
98             }
99
100             set
101             {
102                 base.To = value;
103             }
104         }
105
106         /// <summary>
107         /// 対象記事に言語間リンクが存在する場合の確認処理。
108         /// </summary>
109         /// <remarks>確認を行わない場合<c>null</c>。</remarks>
110         public IsContinueAtInterwikiExistedDelegate IsContinueAtInterwikiExisted
111         {
112             get;
113             set;
114         }
115
116         #endregion
117
118         #region メイン処理メソッド
119
120         /// <summary>
121         /// 翻訳支援処理実行部の本体。
122         /// ※継承クラスでは、この関数に処理を実装すること
123         /// </summary>
124         /// <param name="name">記事名。</param>
125         /// <exception cref="ApplicationException">処理が中断された場合。中断の理由は<see cref="Translator.Logger"/>に出力される。</exception>
126         protected override void RunBody(string name)
127         {
128             // 対象記事を取得
129             MediaWikiPage article = this.GetTargetPage(name);
130             if (article == null)
131             {
132                 throw new ApplicationException("article is not found");
133             }
134
135             // 対象記事に言語間リンクが存在する場合、処理を継続するか確認
136             // ※ 言語間リンク取得中は、処理状態を解析中に変更
137             MediaWikiLink interlanguage;
138             using (var sm = this.StatusManager.Switch(Resources.StatusParsing))
139             {
140                 interlanguage = article.GetInterlanguage(this.To.Language.Code);
141             }
142
143             if (interlanguage != null)
144             {
145                 // 確認処理の最中は処理時間をカウントしない(ダイアログ等を想定するため)
146                 this.Stopwatch.Stop();
147                 if (this.IsContinueAtInterwikiExisted != null && !this.IsContinueAtInterwikiExisted(interlanguage.Title))
148                 {
149                     throw new ApplicationException("user canceled");
150                 }
151
152                 this.Stopwatch.Start();
153                 this.Logger.AddResponse(Resources.LogMessageTargetArticleHadInterWiki, interlanguage.Title);
154             }
155
156             // 冒頭部を作成
157             this.Text += this.CreateOpening(article.Title);
158
159             // 言語間リンク・定型句の変換、実行中は処理状態を解析中に設定
160             this.Logger.AddSeparator();
161             this.Logger.AddResponse(Resources.LogMessageStartParseAndReplace);
162             using (var sm = this.StatusManager.Switch(Resources.StatusParsing))
163             {
164                 IElement element;
165                 using (MediaWikiParser parser = new MediaWikiParser(this.From))
166                 {
167                     element = parser.Parse(article.Text);
168                 }
169
170                 this.Text += this.ReplaceElement(element, article).ToString();
171             }
172
173             // 記事の末尾に新しい言語間リンクと、コメントを追記
174             this.Text += this.CreateEnding(article);
175
176             // ダウンロードされるテキストがLFなので、最後にクライアント環境に合わせた改行コードに変換
177             // ※ダウンロード時に変換するような仕組みが見つかれば、そちらを使う
178             //   その場合、上のように\nをべたに吐いている部分を修正する
179             this.Text = this.Text.Replace("\n", Environment.NewLine);
180         }
181
182         #endregion
183
184         #region 他のクラスの処理をこのクラスにあわせて拡張したメソッド
185
186         /// <summary>
187         /// ログ出力によるエラー処理を含んだページ取得処理。
188         /// </summary>
189         /// <param name="title">ページタイトル。</param>
190         /// <param name="page">取得したページ。ページが存在しない場合は <c>null</c> を返す。</param>
191         /// <returns>処理が成功した(404も含む)場合<c>true</c>、失敗した(通信エラーなど)の場合<c>false</c>。</returns>
192         /// <exception cref="ApplicationException"><see cref="Translator.CancellationPending"/>が<c>true</c>の場合。</exception>
193         /// <remarks>
194         /// 本メソッドは、大きく3パターンの動作を行う。
195         /// <list type="number">
196         /// <item><description>正常にページが取得できた → <c>true</c>でページを設定、ログ出力無し</description></item>
197         /// <item><description>404など想定内の例外でページが取得できなかった → <c>true</c>でページ無し、ログ出力無し</description></item>
198         /// <item><description>想定外の例外でページが取得できなかった → <c>false</c>でページ無し、ログ出力有り
199         ///                    or <c>ApplicationException</c>で処理中断(アプリケーション設定のIgnoreErrorによる)。</description></item>
200         /// </list>
201         /// また、実行中は処理状態をサーバー接続中に更新する。
202         /// 実行前後には終了要求のチェックも行う。
203         /// </remarks>
204         protected bool TryGetPage(string title, out MediaWikiPage page)
205         {
206             // &amp; &nbsp; 等の特殊文字をデコードして、親クラスのメソッドを呼び出し
207             Page p;
208             bool success = base.TryGetPage(WebUtility.HtmlDecode(title), out p);
209             page = p as MediaWikiPage;
210             return success;
211         }
212
213         #endregion
214
215         #region 冒頭/末尾ブロックの生成メソッド
216
217         /// <summary>
218         /// 変換後記事冒頭用の「'''日本語記事名'''([[英語|英]]: '''{{Lang|en|英語記事名}}''')」みたいなのを作成する。
219         /// </summary>
220         /// <param name="title">翻訳支援対象の記事名。</param>
221         /// <returns>冒頭部のテキスト。</returns>
222         protected virtual string CreateOpening(string title)
223         {
224             string langPart = String.Empty;
225             IElement langLink = this.GetLanguageLink();
226             if (langLink != null)
227             {
228                 langPart = langLink.ToString() + ": ";
229             }
230
231             string langBody = this.To.FormatLang(this.From.Language.Code, title);
232             if (String.IsNullOrEmpty(langBody))
233             {
234                 langBody = title;
235             }
236
237             StringBuilder b = new StringBuilder("'''xxx'''");
238             b.Append(this.To.Language.FormatBracket(langPart + "'''" + langBody + "'''"));
239             b.Append("\n\n");
240             return b.ToString();
241         }
242
243         /// <summary>
244         /// 変換後記事末尾用の新しい言語間リンクとコメントを作成する。
245         /// </summary>
246         /// <param name="page">翻訳支援対象の記事。</param>
247         /// <returns>末尾部のテキスト。</returns>
248         protected virtual string CreateEnding(MediaWikiPage page)
249         {
250             MediaWikiLink link = new MediaWikiLink();
251             link.Title = page.Title;
252             link.Interwiki = this.From.Language.Code;
253             return "\n\n" + link.ToString() + "\n" + String.Format(
254                 Resources.ArticleFooter,
255                 FormUtils.ApplicationName(),
256                 this.From.Language.Code,
257                 page.Title,
258                 page.Timestamp.HasValue ? page.Timestamp.Value.ToString("U") : String.Empty) + "\n";
259         }
260
261         #endregion
262
263         #region 要素の変換メソッド
264
265         /// <summary>
266         /// 渡されたページ要素の変換を行う。
267         /// </summary>
268         /// <param name="element">ページ要素。</param>
269         /// <param name="parent">ページ要素を取得した変換元記事。</param>
270         /// <returns>変換後のページ要素。</returns>
271         protected virtual IElement ReplaceElement(IElement element, MediaWikiPage parent)
272         {
273             // ユーザーからの中止要求をチェック
274             this.ThrowExceptionIfCanceled();
275
276             // 要素の型に応じて、必要な置き換えを行う
277             if (element is MediaWikiTemplate)
278             {
279                 // テンプレート
280                 return this.ReplaceTemplate((MediaWikiTemplate)element, parent);
281             }
282             else if (element is MediaWikiLink)
283             {
284                 // 内部リンク
285                 return this.ReplaceLink((MediaWikiLink)element, parent);
286             }
287             else if (element is MediaWikiHeading)
288             {
289                 // 見出し
290                 return this.ReplaceHeading((MediaWikiHeading)element, parent);
291             }
292             else if (element is MediaWikiVariable)
293             {
294                 // 変数
295                 return this.ReplaceVariable((MediaWikiVariable)element, parent);
296             }
297             else if (element is ListElement)
298             {
299                 // 値を格納する要素
300                 return this.ReplaceListElement((ListElement)element, parent);
301             }
302
303             // それ以外は、特に何もせず元の値を返す
304             return element;
305         }
306
307         /// <summary>
308         /// 内部リンクを解析し、変換先言語の記事へのリンクに変換する。
309         /// </summary>
310         /// <param name="link">変換元リンク。</param>
311         /// <param name="parent">ページ要素を取得した変換元記事。</param>
312         /// <returns>変換済みリンク。</returns>
313         protected virtual IElement ReplaceLink(MediaWikiLink link, MediaWikiPage parent)
314         {
315             // 記事名が存在しないor自記事内の別セクションへのリンクの場合、記事名絡みの処理を飛ばす
316             if (!this.IsSectionLink(link, parent.Title))
317             {
318                 // 記事名の種類に応じて処理を実施
319                 MediaWikiPage article = new MediaWikiPage(this.From, link.Title);
320
321                 bool child = false;
322                 if (link.IsSubpage())
323                 {
324                     // サブページ(子)の場合だけ後で記事名を復元するので記録
325                     child = link.Title.StartsWith("/");
326                     
327                     // ページ名を完全な形に補完
328                     string title = parent.Normalize(link);
329                     if (parent.Title.StartsWith(title))
330                     {
331                         // サブページ(親)の場合、変換してもしょうがないのでセクションだけチェックして終了
332                         if (!String.IsNullOrEmpty(link.Section))
333                         {
334                             link.Section = this.ReplaceLinkSection(link.Section);
335                             link.ParsedString = null;
336                         }
337
338                         return link;
339                     }
340
341                     link.Title = title;
342                 }
343                 else if (!String.IsNullOrEmpty(link.Interwiki))
344                 {
345                     // 言語間リンク・姉妹プロジェクトへのリンクの場合、変換対象外とする
346                     // ただし、先頭が : でない、翻訳先言語への言語間リンクだけは削除
347                     return this.ReplaceLinkInterwiki(link);
348                 }
349                 else if (article.IsFile())
350                 {
351                     // 画像の場合、名前空間を翻訳先言語の書式に変換、パラメータ部を再帰的に処理
352                     return this.ReplaceLinkFile(link, parent);
353                 }
354                 else if (article.IsCategory() && !link.IsColon)
355                 {
356                     // カテゴリで記事へのリンクでない([[:Category:xxx]]みたいなリンクでない)場合、
357                     // カテゴリ用の変換を実施
358                     return this.ReplaceLinkCategory(link);
359                 }
360
361                 // 専用処理の無い内部リンクの場合、言語間リンクによる置き換えを行う
362                 string interWiki = this.GetInterlanguage(link);
363                 if (interWiki == null)
364                 {
365                     // 記事自体が存在しない(赤リンク)場合、リンクはそのまま
366                 }
367                 else if (interWiki == String.Empty)
368                 {
369                     // 言語間リンクが存在しない場合、可能なら{{仮リンク}}に置き換え
370                     if (!String.IsNullOrEmpty(this.To.LinkInterwikiFormat))
371                     {
372                         return this.ReplaceLinkLinkInterwiki(link);
373                     }
374
375                     // 設定が無ければ [[:en:xxx]] みたいな形式に置換
376                     link.Title = this.From.Language.Code + ':' + link.Title;
377                     link.IsColon = true;
378                 }
379                 else if (child)
380                 {
381                     // 言語間リンクが存在してサブページ(子)の場合、親ページ部分を消す
382                     // TODO: 兄弟や叔父のパターンも対処したい(ややこしいので現状未対応)
383                     link.Title = StringUtils.Substring(interWiki, interWiki.IndexOf('/'));
384                 }
385                 else
386                 {
387                     // 普通に言語間リンクが存在する場合、記事名を置き換え
388                     link.Title = interWiki;
389                 }
390
391                 if (link.PipeTexts.Count == 0 && interWiki != null)
392                 {
393                     // 表示名が存在しない場合、元の名前を表示名に設定
394                     // 元の名前にはあればセクションも含む
395                     link.PipeTexts.Add(
396                         new TextElement(new MediaWikiLink { Title = article.Title, Section = link.Section }
397                             .GetLinkString()));
398                 }
399             }
400
401             // セクション部分([[#関連項目]]とか)を変換
402             if (!String.IsNullOrEmpty(link.Section))
403             {
404                 link.Section = this.ReplaceLinkSection(link.Section);
405             }
406
407             link.ParsedString = null;
408             return link;
409         }
410
411         /// <summary>
412         /// テンプレートを解析し、変換先言語の記事へのテンプレートに変換する。
413         /// </summary>
414         /// <param name="template">変換元テンプレート。</param>
415         /// <param name="parent">ページ要素を取得した変換元記事。</param>
416         /// <returns>変換済みテンプレート。</returns>
417         protected virtual IElement ReplaceTemplate(MediaWikiTemplate template, MediaWikiPage parent)
418         {
419             // システム変数({{PAGENAME}}とか)の場合は対象外
420             if (this.From.IsMagicWord(template.Title))
421             {
422                 return template;
423             }
424
425             // テンプレートは通常名前空間が省略されているので補完する
426             string filledTitle = this.FillTemplateName(template, parent);
427
428             // リンクを辿り、対象記事の言語間リンクを取得
429             string interWiki = this.GetInterlanguage(new MediaWikiTemplate(filledTitle));
430             if (interWiki == null)
431             {
432                 // 記事自体が存在しない(赤リンク)場合、リンクはそのまま
433                 return template;
434             }
435             else if (interWiki == String.Empty)
436             {
437                 // 言語間リンクが存在しない場合、[[:en:Template:xxx]]みたいな普通のリンクに置換
438                 // おまけで、元のテンプレートの状態をコメントでつける
439                 ListElement list = new ListElement();
440                 MediaWikiLink link = new MediaWikiLink();
441                 link.IsColon = true;
442                 link.Title = this.From.Language.Code + ':' + filledTitle;
443                 list.Add(link);
444                 XmlCommentElement comment = new XmlCommentElement();
445                 comment.Raw = ' ' + template.ToString() + ' ';
446                 list.Add(comment);
447                 return list;
448             }
449             else
450             {
451                 // 言語間リンクが存在する場合、そちらを指すように置換
452                 template.Title = interWiki;
453                 if (new MediaWikiPage(this.To, interWiki).IsTemplate())
454                 {
455                     // 言語間リンク先がテンプレートの場合、: より前の部分を削除
456                     template.Title = interWiki.Substring(interWiki.IndexOf(':') + 1);
457                 }
458
459                 // | の後に内部リンクやテンプレートが書かれている場合があるので、再帰的に処理する
460                 template.PipeTexts = this.ReplaceElements(template.PipeTexts, parent);
461                 template.ParsedString = null;
462                 return template;
463             }
464         }
465
466         /// <summary>
467         /// 指定された見出しに対して、対訳表による変換を行う。
468         /// </summary>
469         /// <param name="heading">見出し。</param>
470         /// <param name="parent">ページ要素を取得した変換元記事。</param>
471         /// <returns>変換後の見出し。</returns>
472         protected virtual IElement ReplaceHeading(MediaWikiHeading heading, MediaWikiPage parent)
473         {
474             // 変換元ログ出力
475             this.Logger.AddSource(heading);
476
477             // 定型句変換
478             StringBuilder oldText = new StringBuilder();
479             foreach (IElement e in heading)
480             {
481                 oldText.Append(e.ToString());
482             }
483
484             string newText = this.GetHeading(oldText.ToString().Trim());
485             if (newText != null)
486             {
487                 // 対訳表による変換が行えた場合、変換先をログ出力し処理終了
488                 heading.Clear();
489                 heading.ParsedString = null;
490                 heading.Add(new XmlTextElement(newText));
491                 this.Logger.AddDestination(heading);
492                 return heading;
493             }
494
495             // 対訳表に存在しない場合、内部要素を通常の変換で再帰的に処理
496             return this.ReplaceListElement(heading, parent);
497         }
498
499         /// <summary>
500         /// 変数要素を再帰的に解析し、変換先言語の記事への要素に変換する。
501         /// </summary>
502         /// <param name="variable">変換元変数要素。</param>
503         /// <param name="parent">ページ要素を取得した変換元記事。</param>
504         /// <returns>変換済み変数要素。</returns>
505         protected virtual IElement ReplaceVariable(MediaWikiVariable variable, MediaWikiPage parent)
506         {
507             // 変数、これ自体は処理しないが、再帰的に探索
508             string old = variable.Value.ToString();
509             variable.Value = this.ReplaceElement(variable.Value, parent);
510             if (variable.Value.ToString() != old)
511             {
512                 // 内部要素が変化した(置き換えが行われた)場合、変換前のテキストを破棄
513                 variable.ParsedString = null;
514             }
515
516             return variable;
517         }
518
519         /// <summary>
520         /// 要素を再帰的に解析し、変換先言語の記事への要素に変換する。
521         /// </summary>
522         /// <param name="listElement">変換元要素。</param>
523         /// <param name="parent">ページ要素を取得した変換元記事。</param>
524         /// <returns>変換済み要素。</returns>
525         protected virtual IElement ReplaceListElement(ListElement listElement, MediaWikiPage parent)
526         {
527             // 値を格納する要素、これ自体は処理しないが、再帰的に探索
528             for (int i = 0; i < listElement.Count; i++)
529             {
530                 string old = listElement[i].ToString();
531                 listElement[i] = this.ReplaceElement(listElement[i], parent);
532                 if (listElement[i].ToString() != old)
533                 {
534                     // 内部要素が変化した(置き換えが行われた)場合、変換前のテキストを破棄
535                     listElement.ParsedString = null;
536                 }
537             }
538
539             return listElement;
540         }
541
542         #endregion
543
544         #region 対訳表アクセスメソッド
545
546         /// <summary>
547         /// 対訳表に指定された記事名の情報が登録されているか?
548         /// </summary>
549         /// <param name="title">記事名。</param>
550         /// <returns>指定した記事の情報が登録されている場合<c>true</c>。</returns>
551         /// <remarks>複数スレッドからのアクセスに対応する。また項目の対訳表が無い場合も動作する。</remarks>
552         protected bool ContainsAtItemTable(string title)
553         {
554             if (this.ItemTable == null)
555             {
556                 return false;
557             }
558
559             // 以下マルチスレッドで使われることも想定して対訳表へのアクセス時はロック
560             // ※ 対訳表へのアクセス時は記事名をデコードしておく
561             string decodedTitle = WebUtility.HtmlDecode(title);
562             lock (this.itemTableLock.GetObject(decodedTitle.ToLower()))
563             {
564                 // 対訳表へのキーとしてはHTMLデコードした記事名を使用する
565                 return this.ItemTable.ContainsKey(decodedTitle);
566             }
567         }
568         
569         /// <summary>
570         /// 指定されたコードでの見出しに相当する、別の言語での見出しを取得。
571         /// </summary>
572         /// <param name="heading">翻訳元言語での見出し。</param>
573         /// <returns>翻訳先言語での見出し。値が存在しない場合は<c>null</c>。</returns>
574         /// <remarks>見出しの対訳表が無い場合も動作する。</remarks>
575         protected string GetHeading(string heading)
576         {
577             if (this.HeadingTable == null)
578             {
579                 return null;
580             }
581
582             return this.HeadingTable.GetWord(heading);
583         }
584
585         #endregion
586
587         #region 言語間リンク取得メソッド
588         
589         /// <summary>
590         /// ロガーに取得結果を出力しつつ、指定された要素の記事の翻訳先言語への言語間リンクを返す。
591         /// </summary>
592         /// <param name="element">内部リンク要素。</param>
593         /// <returns>言語間リンク先の記事名。見つからない場合は空。ページ自体が存在しない場合は<c>null</c>。</returns>
594         /// <remarks>取得処理では対訳表を使用する。また新たな取得結果は対訳表に追加する。</remarks>
595         protected string GetInterlanguage(MediaWikiLink element)
596         {
597             // 翻訳元をロガーに出力
598             this.Logger.AddSource(element);
599             string title = element.Title;
600             TranslationDictionary.Item item;
601             if (this.ItemTable == null)
602             {
603                 // 対訳表が指定されていない場合は、使わずに言語間リンクを探索して終了
604                 return this.GetInterlanguageWithCreateCache(title, out item);
605             }
606
607             // 対訳表を使用して言語間リンクを探索。
608             // 以下マルチスレッドで使われることも想定して対訳表へのアクセス時はロック(記事名単位)。
609             // また、対訳表へのアクセス時は記事名をデコードしておく。
610             string decodedTitle = WebUtility.HtmlDecode(title);
611             lock (this.itemTableLock.GetObject(decodedTitle.ToLower()))
612             {
613                 if (this.ItemTable.TryGetValue(decodedTitle, out item))
614                 {
615                     // 存在する場合はその値を使用
616                     if (!String.IsNullOrWhiteSpace(item.Alias))
617                     {
618                         // リダイレクトがあれば、そのメッセージも表示
619                         this.Logger.AddAlias(new MediaWikiLink(item.Alias));
620                     }
621
622                     if (!String.IsNullOrEmpty(item.Word))
623                     {
624                         this.Logger.AddDestination(new MediaWikiLink(item.Word), true);
625                         return item.Word;
626                     }
627                     else
628                     {
629                         this.Logger.AddDestination(new TextElement(Resources.LogMessageInterWikiNotFound), true);
630                         return String.Empty;
631                     }
632                 }
633
634                 // 対訳表に存在しない場合は、普通に取得し表に記録
635                 // ※ こちらは内部でデコードしているためデコードした記事名を渡してはならない
636                 string interlanguage = this.GetInterlanguageWithCreateCache(title, out item);
637                 if (interlanguage != null)
638                 {
639                     // ページ自体が存在しない場合を除き、結果を対訳表に登録
640                     // ※ キャッシュとしては登録すべきかもしれないが、一応"対訳表"であるので
641                     this.ItemTable[decodedTitle] = item;
642                 }
643
644                 return interlanguage;
645             }
646         }
647
648         /// <summary>
649         /// ロガーに取得結果を出力しつつ、指定された記事の翻訳先言語への言語間リンクを返す。
650         /// キャッシュ用の処理結果情報も出力する。
651         /// </summary>
652         /// <param name="title">記事名。</param>
653         /// <param name="item">キャッシュ用の処理結果情報。</param>
654         /// <returns>言語間リンク先の記事名。見つからない場合は空。ページ自体が存在しない場合は<c>null</c>。</returns>
655         private string GetInterlanguageWithCreateCache(string title, out TranslationDictionary.Item item)
656         {
657             // 記事名から記事を探索
658             item = new TranslationDictionary.Item { Timestamp = DateTime.UtcNow };
659             MediaWikiPage page = this.GetDestinationPage(title);
660             if (page != null && page.IsRedirect())
661             {
662                 // リダイレクトの場合、リダイレクトである旨出力し、その先の記事を取得
663                 this.Logger.AddAlias(new MediaWikiLink(page.Redirect.Title));
664                 item.Alias = page.Redirect.Title;
665                 page = this.GetDestinationPage(page.Redirect.Title);
666             }
667
668             if (page == null)
669             {
670                 // ページ自体が存在しない場合はnull
671                 return null;
672             }
673
674             // 記事があればその言語間リンクを取得
675             MediaWikiLink interlanguage = page.GetInterlanguage(this.To.Language.Code);
676             if (interlanguage != null)
677             {
678                 item.Word = interlanguage.Title;
679                 this.Logger.AddDestination(interlanguage);
680             }
681             else
682             {
683                 // 見つからない場合は空
684                 item.Word = String.Empty;
685                 this.Logger.AddDestination(new TextElement(Resources.LogMessageInterWikiNotFound));
686             }
687
688             return item.Word;
689         }
690
691         /// <summary>
692         /// 変換先の記事を取得する。
693         /// </summary>
694         /// <param name="title">ページタイトル。</param>
695         /// <returns>取得したページ。ページが存在しない場合は <c>null</c> を返す。</returns>
696         /// <remarks>記事が無い場合、通信エラーなど例外が発生した場合は、エラーログを出力する。</remarks>
697         private MediaWikiPage GetDestinationPage(string title)
698         {
699             MediaWikiPage page;
700             if (this.TryGetPage(title, out page) && page == null)
701             {
702                 // 記事が存在しない場合だけ、変換先に「記事無し」を出力
703                 // ※ エラー時のログはTryGetPageが自動的に出力
704                 this.Logger.AddDestination(new TextElement(Resources.LogMessageLinkArticleNotFound));
705             }
706
707             return page;
708         }
709
710         #endregion
711
712         #region 要素の変換関連その他メソッド
713
714         /// <summary>
715         /// 同記事内の別のセクションを指すリンク([[#関連項目]]とか[[自記事#関連項目]]とか)か?
716         /// </summary>
717         /// <param name="link">判定する内部リンク。</param>
718         /// <param name="parent">内部リンクがあった記事。</param>
719         /// <returns>セクション部分のみ変換済みリンク。</returns>
720         private bool IsSectionLink(MediaWikiLink link, string parent)
721         {
722             // 記事名が指定されていない、または記事名が自分の記事名で
723             // 言語コード等も特に無く、かつセクションが指定されている場合
724             // (記事名もセクションも指定されていない・・・というケースもありえるが、
725             //   その場合他に指定できるものも思いつかないので通す)
726             return String.IsNullOrEmpty(link.Title)
727                 || (link.Title == parent && String.IsNullOrEmpty(link.Interwiki) && !String.IsNullOrEmpty(link.Section));
728         }
729
730         /// <summary>
731         /// 内部リンクのセクション部分([[#関連項目]]とか)の定型句変換を行う。
732         /// </summary>
733         /// <param name="section">セクション文字列。</param>
734         /// <returns>セクション部分のみ変換済みリンク。</returns>
735         private string ReplaceLinkSection(string section)
736         {
737             // セクションが指定されている場合、定型句変換を通す
738             string heading = this.GetHeading(section);
739             return heading != null ? heading : section;
740         }
741
742         /// <summary>
743         /// 言語間リンク指定の内部リンクを解析し、不要であれば削除する。
744         /// </summary>
745         /// <param name="link">変換元言語間リンク。</param>
746         /// <returns>変換済み言語間リンク。</returns>
747         private IElement ReplaceLinkInterwiki(MediaWikiLink link)
748         {
749             // 言語間リンク・姉妹プロジェクトへのリンクの場合、変換対象外とする
750             // ただし、先頭が : でない、翻訳先言語への言語間リンクだけは削除
751             if (!link.IsColon && link.Interwiki == this.To.Language.Code)
752             {
753                 return new TextElement();
754             }
755
756             return link;
757         }
758
759         /// <summary>
760         /// カテゴリ指定の内部リンクを解析し、変換先言語のカテゴリへのリンクに変換する。
761         /// </summary>
762         /// <param name="link">変換元カテゴリ。</param>
763         /// <returns>変換済みカテゴリ。</returns>
764         private IElement ReplaceLinkCategory(MediaWikiLink link)
765         {
766             // リンクを辿り、対象記事の言語間リンクを取得
767             string interWiki = this.GetInterlanguage(link);
768             if (interWiki == null)
769             {
770                 // 記事自体が存在しない(赤リンク)場合、リンクはそのまま
771                 return link;
772             }
773             else if (interWiki == String.Empty)
774             {
775                 // 言語間リンクが存在しない場合、コメントで元の文字列を保存した後
776                 // [[:en:xxx]]みたいな形式に置換。また | 以降は削除する
777                 XmlCommentElement comment = new XmlCommentElement();
778                 comment.Raw = ' ' + link.ToString() + ' ';
779
780                 link.Title = this.From.Language.Code + ':' + link.Title;
781                 link.IsColon = true;
782                 link.PipeTexts.Clear();
783                 link.ParsedString = null;
784
785                 ListElement list = new ListElement();
786                 list.Add(link);
787                 list.Add(comment);
788                 return list;
789             }
790             else
791             {
792                 // 普通に言語間リンクが存在する場合、記事名を置き換え
793                 link.Title = interWiki;
794                 link.ParsedString = null;
795                 return link;
796             }
797         }
798
799         /// <summary>
800         /// ファイル指定の内部リンクを解析し、変換先言語で参照可能なファイルへのリンクに変換する。
801         /// </summary>
802         /// <param name="link">変換元リンク。</param>
803         /// <param name="parent">ページ要素を取得した変換元記事。</param>
804         /// <returns>変換済みリンク。</returns>
805         private IElement ReplaceLinkFile(MediaWikiLink link, MediaWikiPage parent)
806         {
807             // 名前空間を翻訳先言語の書式に変換、またパラメータ部を再帰的に処理
808             link.Title = this.ReplaceLinkNamespace(link.Title, this.To.FileNamespace);
809             link.PipeTexts = this.ReplaceElements(link.PipeTexts, parent);
810             link.ParsedString = null;
811             return link;
812         }
813
814         /// <summary>
815         /// 記事名のうち名前空間部分の変換先言語への変換を行う。
816         /// </summary>
817         /// <param name="title">変換元記事名。</param>
818         /// <param name="id">名前空間のID。</param>
819         /// <returns>変換済み記事名。</returns>
820         private string ReplaceLinkNamespace(string title, int id)
821         {
822             // 名前空間だけ翻訳先言語の書式に変換
823             IgnoreCaseSet names;
824             if (!this.To.Namespaces.TryGetValue(id, out names))
825             {
826                 // 翻訳先言語に相当する名前空間が無い場合、何もしない
827                 return title;
828             }
829
830             // 記事名の名前空間部分を置き換えて返す
831             return names.FirstOrDefault() + title.Substring(title.IndexOf(':'));
832         }
833
834         /// <summary>
835         /// 内部リンクを他言語版への{{仮リンク}}等に変換する。。
836         /// </summary>
837         /// <param name="link">変換元言語間リンク。</param>
838         /// <returns>変換済み言語間リンク。</returns>
839         private IElement ReplaceLinkLinkInterwiki(MediaWikiLink link)
840         {
841             // 仮リンクにはセクションの指定が可能なので、存在する場合付加する
842             // ※ 渡されたlinkをそのまま使わないのは、余計なゴミが含まれる可能性があるため
843             MediaWikiLink title = new MediaWikiLink { Title = link.Title, Section = link.Section };
844             string langTitle = title.GetLinkString();
845             if (!String.IsNullOrEmpty(title.Section))
846             {
847                 // 変換先言語版のセクションは、セクションの変換を通したものにする
848                 title.Section = this.ReplaceLinkSection(title.Section);
849             }
850
851             // 表示名は、設定されていればその値を、なければ変換元言語の記事名を使用
852             string label = langTitle;
853             if (link.PipeTexts.Count > 0)
854             {
855                 label = link.PipeTexts.Last().ToString();
856             }
857
858             // 書式化した文字列を返す
859             // ※ {{仮リンク}}を想定しているが、やろうと思えば何でもできるのでテキストで処理
860             return new TextElement(this.To.FormatLinkInterwiki(title.GetLinkString(), this.From.Language.Code, langTitle, label));
861         }
862
863         /// <summary>
864         /// 渡された要素リストに対して<see cref="ReplaceElement"/>による変換を行う。
865         /// </summary>
866         /// <param name="elements">変換元要素リスト。</param>
867         /// <param name="parent">ページ要素を取得した変換元記事。</param>
868         /// <returns>変換済み要素リスト。</returns>
869         private IList<IElement> ReplaceElements(IList<IElement> elements, MediaWikiPage parent)
870         {
871             if (elements == null)
872             {
873                 return null;
874             }
875
876             IList<IElement> result = new List<IElement>();
877             foreach (IElement e in elements)
878             {
879                 result.Add(this.ReplaceElement(e, parent));
880             }
881
882             return result;
883         }
884
885         /// <summary>
886         /// テンプレート名に必要に応じて名前空間を補完する。
887         /// </summary>
888         /// <param name="template">テンプレート。</param>
889         /// <param name="parent">ページ要素を取得した変換元記事。</param>
890         /// <returns>補完済みのテンプレート名。</returns>
891         private string FillTemplateName(MediaWikiTemplate template, MediaWikiPage parent)
892         {
893             // プレフィックスが付いた記事名を作成
894             string filledTitle = parent.Normalize(template);
895             if (filledTitle == template.Title || template.IsSubpage())
896             {
897                 // 補完が不要な場合、またはサブページだった場合、ここで終了
898                 return filledTitle;
899             }
900
901             // プレフィックスが付いた記事名が実際に存在するかを確認
902             // ※ 不要かもしれないが、マジックワードの漏れ等の誤検出を減らしたいので
903             if (this.ContainsAtItemTable(filledTitle))
904             {
905                 // 対訳表に記事名が確認されている場合、既知の名前として確定
906                 return filledTitle;
907             }
908
909             // 実際に頭にプレフィックスを付けた記事名でアクセスし、存在するかをチェック
910             // TODO: GetInterWikiの方とあわせ、テンプレートでは2度GetPageが呼ばれている。可能であれば共通化する
911             MediaWikiPage page = null;
912             try
913             {
914                 // 記事が存在する場合、プレフィックスをつけた名前を使用
915                 page = this.From.GetPage(WebUtility.HtmlDecode(filledTitle)) as MediaWikiPage;
916                 return filledTitle;
917             }
918             catch (FileNotFoundException)
919             {
920                 // 記事が存在しない場合、元のページ名を使用
921                 return template.Title;
922             }
923             catch (Exception e)
924             {
925                 // 想定外の例外が発生した場合
926                 if (!Settings.Default.IgnoreError)
927                 {
928                     // エラーを無視しない場合、ここで翻訳支援処理を中断する
929                     this.Logger.AddError(e);
930                     throw new ApplicationException(e.Message, e);
931                 }
932
933                 // 続行する場合は、とりあえずプレフィックスをつけた名前で処理
934                 this.Logger.AddResponse(Resources.LogMessageTemplateNameUnidentified, template.Title, filledTitle, e.Message);
935                 return filledTitle;
936             }
937         }
938
939         #endregion
940
941         #region その他内部処理用メソッド
942
943         /// <summary>
944         /// 翻訳支援対象のページを取得。
945         /// </summary>
946         /// <param name="title">翻訳支援対象の記事名。</param>
947         /// <returns>取得したページ。取得失敗時は<c>null</c>。</returns>
948         /// <remarks>
949         /// ここで取得した記事のURIを、以後の翻訳支援処理で使用するRefererとして登録
950         /// (ここで処理しているのは、リダイレクトの場合のリダイレクト先への
951         /// アクセス時にもRefererを入れたかったから。
952         /// リダイレクトの場合は、最終的には転送先ページのURIとなる)。
953         /// </remarks>
954         private MediaWikiPage GetTargetPage(string title)
955         {
956             // 指定された記事をWikipediaから取得、リダイレクトの場合その先まで探索
957             // ※ この処理ではキャッシュは使用しない。
958             // ※ 万が一相互にリダイレクトしていると無限ループとなるが、特に判定はしない。
959             //    ユーザーが画面上から止めることを期待。
960             this.Logger.AddMessage(Resources.LogMessageGetTargetArticle, this.From.Location, title);
961             MediaWikiPage page;
962             for (string s = title; this.TryGetPage(s, out page); s = page.Redirect.Title)
963             {
964                 if (page == null)
965                 {
966                     // 記事が存在しない場合、メッセージを出力して終了
967                     this.Logger.AddResponse(Resources.LogMessageTargetArticleNotFound);
968                     break;
969                 }
970
971                 // 取得した記事のURIを以後のアクセスで用いるRefererとして登録
972                 this.From.WebProxy.Referer = page.Uri.AbsoluteUri;
973                 this.To.WebProxy.Referer = page.Uri.AbsoluteUri;
974
975                 if (!page.IsRedirect())
976                 {
977                     // リダイレクト以外ならこれで終了
978                     break;
979                 }
980
981                 // リダイレクトであれば、さらにその先の記事を取得
982                 this.Logger.AddResponse(Resources.LogMessageRedirect
983                     + " " + new MediaWikiLink(page.Redirect.Title).ToString());
984             }
985
986             return page;
987         }
988
989         /// <summary>
990         /// 指定した言語での言語名称を [[言語名称|略称]]の内部リンクで取得。
991         /// </summary>
992         /// <returns>
993         /// [[言語名称|略称]]の内部リンク。登録されていない場合<c>null</c>。
994         /// サーバーにそうした記事が存在しない場合、リンクではなく言語名称or略称の文字列を返す。
995         /// </returns>
996         private IElement GetLanguageLink()
997         {
998             // 言語情報を取得
999             Language.LanguageName name;
1000             if (!this.From.Language.Names.TryGetValue(this.To.Language.Code, out name))
1001             {
1002                 return null;
1003             }
1004
1005             // 略称を取得
1006             IElement shortName = null;
1007             if (!String.IsNullOrEmpty(name.ShortName))
1008             {
1009                 shortName = new TextElement(name.ShortName);
1010             }
1011
1012             if (this.To.HasLanguagePage)
1013             {
1014                 // サーバーにこの言語の記事が存在することが期待される場合、
1015                 // 内部リンクとして返す
1016                 MediaWikiLink link = new MediaWikiLink(name.Name);
1017                 if (shortName != null)
1018                 {
1019                     link.PipeTexts.Add(shortName);
1020                 }
1021
1022                 return link;
1023             }
1024             else if (shortName != null)
1025             {
1026                 // 存在しない場合、まずあれば略称を返す
1027                 return shortName;
1028             }
1029             else
1030             {
1031                 // 無ければ言語名を返す
1032                 return new TextElement(name.Name);
1033             }
1034         }
1035
1036         /// <summary>
1037         /// 対象記事に言語間リンクが存在する場合にメッセージダイアログでユーザーに確認する処理。
1038         /// </summary>
1039         /// <param name="interwiki">言語間リンク先記事。</param>
1040         /// <returns>処理を続行する場合<c>true</c>。</returns>
1041         private bool IsContinueAtInterwikiExistedWithDialog(string interwiki)
1042         {
1043             // 確認ダイアログを表示
1044             if (MessageBox.Show(
1045                         String.Format(Resources.QuestionMessageArticleExisted, interwiki),
1046                         Resources.QuestionTitle,
1047                         MessageBoxButtons.YesNo,
1048                         MessageBoxIcon.Question)
1049                    == DialogResult.No)
1050             {
1051                 // 中断の場合、同じメッセージをログにも表示
1052                 this.Logger.AddSeparator();
1053                 this.Logger.AddMessage(Resources.QuestionMessageArticleExisted, interwiki);
1054                 return false;
1055             }
1056
1057             return true;
1058         }
1059
1060         #endregion
1061     }
1062 }