OSDN Git Service

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