OSDN Git Service

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