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