OSDN Git Service

#27316 表示言語の切り替え機能・英語リソースの追加(ツールチップは除く),
[wptscs/wpts.git] / Wptscs / Websites / MediaWikiPage.cs
1 // ================================================================================================
2 // <summary>
3 //      MediaWikiのページをあらわすモデルクラスソース</summary>
4 //
5 // <copyright file="MediaWikiPage.cs" company="honeplusのメモ帳">
6 //      Copyright (C) 2012 Honeplus. All rights reserved.</copyright>
7 // <author>
8 //      Honeplus</author>
9 // ================================================================================================
10
11 namespace Honememo.Wptscs.Websites
12 {
13     using System;
14     using System.Collections.Generic;
15     using System.IO;
16     using System.Linq;
17     using Honememo.Models;
18     using Honememo.Parsers;
19     using Honememo.Utilities;
20     using Honememo.Wptscs.Parsers;
21
22     /// <summary>
23     /// MediaWikiのページをあらわすモデルクラスです。
24     /// </summary>
25     public class MediaWikiPage : Page
26     {
27         #region private変数
28
29         /// <summary>
30         /// リダイレクト先のページ名。
31         /// </summary>
32         private MediaWikiLink redirect;
33
34         /// <summary>
35         /// ページの本文をパーサーで要素単位に解析した結果。
36         /// </summary>
37         private IElement element;
38
39         #endregion
40
41         #region コンストラクタ
42
43         /// <summary>
44         /// コンストラクタ。
45         /// </summary>
46         /// <param name="website">ページが所属するウェブサイト。</param>
47         /// <param name="title">ページタイトル。</param>
48         /// <param name="text">ページの本文。</param>
49         /// <param name="timestamp">ページのタイムスタンプ。</param>
50         public MediaWikiPage(MediaWiki website, string title, string text, DateTime? timestamp)
51             : base(website, title, text, timestamp)
52         {
53         }
54
55         /// <summary>
56         /// コンストラクタ。
57         /// ページのタイムスタンプには<c>null</c>を設定。
58         /// </summary>
59         /// <param name="website">ページが所属するウェブサイト。</param>
60         /// <param name="title">ページタイトル。</param>
61         /// <param name="text">ページの本文。</param>
62         public MediaWikiPage(MediaWiki website, string title, string text)
63             : base(website, title, text)
64         {
65         }
66
67         /// <summary>
68         /// コンストラクタ。
69         /// ページの本文, タイムスタンプには<c>null</c>を設定。
70         /// </summary>
71         /// <param name="website">ページが所属するウェブサイト。</param>
72         /// <param name="title">ページタイトル。</param>
73         public MediaWikiPage(MediaWiki website, string title)
74             : base(website, title)
75         {
76         }
77
78         #endregion
79
80         #region プロパティ
81
82         /// <summary>
83         /// ページが所属するウェブサイト。
84         /// </summary>
85         public new MediaWiki Website
86         {
87             get
88             {
89                 return base.Website as MediaWiki;
90             }
91
92             protected set
93             {
94                 base.Website = value;
95             }
96         }
97
98         /// <summary>
99         /// ページの本文。
100         /// </summary>
101         public override string Text
102         {
103             get
104             {
105                 return base.Text;
106             }
107
108             protected set
109             {
110                 // 本文は普通に格納
111                 base.Text = value;
112                 this.redirect = null;
113                 this.element = null;
114
115                 // 本文格納のタイミングでリダイレクトページ(#REDIRECT等)かを判定
116                 if (!String.IsNullOrEmpty(base.Text))
117                 {
118                     IElement element;
119                     if (new MediaWikiRedirectParser(this.Website).TryParse(base.Text, out element))
120                     {
121                         this.redirect = element as MediaWikiLink;
122                     }
123                 }
124             }
125         }
126
127         /// <summary>
128         /// リダイレクト先へのリンク。
129         /// </summary>
130         /// <exception cref="InvalidOperationException"><see cref="Text"/>が<c>null</c>の場合。</exception>
131         public MediaWikiLink Redirect
132         {
133             get
134             {
135                 // Textが設定されている場合のみ有効
136                 this.ValidateIncomplete();
137                 return this.redirect;
138             }
139
140             protected set
141             {
142                 this.redirect = value;
143             }
144         }
145
146         /// <summary>
147         /// ページの本文をパーサーで要素単位に解析した結果。
148         /// </summary>
149         /// <exception cref="InvalidOperationException"><see cref="Text"/>が<c>null</c>の場合。</exception>
150         /// <remarks>get時にページの解析を行う。</remarks>
151         public IElement Element
152         {
153             get
154             {
155                 // Textが設定されている場合のみ有効
156                 this.ValidateIncomplete();
157                 if (this.element == null)
158                 {
159                     // ページサイズによっては時間がかかるので、必要な場合だけ実施
160                     this.element = new MediaWikiParser(this.Website).Parse(this.Text);
161                 }
162
163                 return this.element;
164             }
165
166             protected set
167             {
168                 this.element = value;
169             }
170         }
171
172         #endregion
173         
174         #region 公開メソッド
175
176         /// <summary>
177         /// 指定された言語コードへの言語間リンクを返す。
178         /// </summary>
179         /// <param name="code">言語コード。</param>
180         /// <returns>言語間リンク。見つからない場合は<c>null</c>。</returns>
181         /// <exception cref="InvalidOperationException"><see cref="Text"/>が<c>null</c>の場合。</exception>
182         /// <remarks>言語間リンクが複数存在する場合は、先に発見したものを返す。</remarks>
183         public MediaWikiLink GetInterlanguage(string code)
184         {
185             // Textが設定されている場合のみ有効
186             this.ValidateIncomplete();
187
188             // 記事を解析し、その結果から言語間リンクを探索
189             return this.GetInterlanguage(code, this.Element);
190         }
191
192         /// <summary>
193         /// ページがリダイレクトかをチェック。
194         /// </summary>
195         /// <returns><c>true</c> リダイレクト。</returns>
196         /// <exception cref="InvalidOperationException"><see cref="Text"/>が<c>null</c>の場合。</exception>
197         public bool IsRedirect()
198         {
199             // Textが設定されている場合のみ有効
200             return this.Redirect != null;
201         }
202
203         /// <summary>
204         /// ページがテンプレートかをチェック。
205         /// </summary>
206         /// <returns><c>true</c> テンプレート。</returns>
207         public bool IsTemplate()
208         {
209             // ページ名がカテゴリー(Category:等で始まる)かをチェック
210             return this.IsNamespacePage(this.Website.TemplateNamespace);
211         }
212
213         /// <summary>
214         /// ページがカテゴリーかをチェック。
215         /// </summary>
216         /// <returns><c>true</c> カテゴリー。</returns>
217         public bool IsCategory()
218         {
219             // ページ名がカテゴリー(Category:等で始まる)かをチェック
220             return this.IsNamespacePage(this.Website.CategoryNamespace);
221         }
222
223         /// <summary>
224         /// ページが画像かをチェック。
225         /// </summary>
226         /// <returns><c>true</c> 画像。</returns>
227         public bool IsFile()
228         {
229             // ページ名がファイル(Image:等で始まる)かをチェック
230             return this.IsNamespacePage(this.Website.FileNamespace);
231         }
232
233         /// <summary>
234         /// ページが標準名前空間かをチェック。
235         /// </summary>
236         /// <returns><c>true</c> 標準名前空間。</returns>
237         public bool IsMain()
238         {
239             // ページ名が標準名前空間以外のなんらかの名前空間かをチェック
240             return !this.Website.IsNamespace(this.Title);
241         }
242
243         /// <summary>
244         /// このページ内のリンクの記事名(サブページ等)を完全な記事名にする。
245         /// </summary>
246         /// <param name="link">このページ内のリンク。</param>
247         /// <returns>変換した記事名。</returns>
248         public string Normalize(MediaWikiLink link)
249         {
250             string title = StringUtils.DefaultString(link.Title);
251             if (link.IsSubpage())
252             {
253                 // サブページ関連の正規化
254                 title = this.NormalizeSubpage(title);
255             }
256             else if (link is MediaWikiTemplate)
257             {
258                 // テンプレート関連の正規化(サブページの場合は不要)
259                 title = this.NormalizeTemplate((MediaWikiTemplate)link);
260             }
261
262             return title;
263         }
264
265         #endregion
266
267         #region 内部処理用メソッド
268
269         /// <summary>
270         /// ページが指定された番号の名前空間に所属するかをチェック。
271         /// </summary>
272         /// <param name="id">名前空間のID。</param>
273         /// <returns>所属する場合<c>true</c>。</returns>
274         /// <remarks>大文字小文字は区別しない。</remarks>
275         protected bool IsNamespacePage(int id)
276         {
277             // 指定された記事名がカテゴリー(Category:等で始まる)かをチェック
278             int index = this.Title.IndexOf(':');
279             if (index < 0)
280             {
281                 return false;
282             }
283
284             string title = this.Title.Remove(index);
285             IgnoreCaseSet prefixes = this.Website.Namespaces[id];
286             return prefixes != null && prefixes.Contains(title);
287         }
288
289         /// <summary>
290         /// オブジェクトがメソッドの実行に不完全な状態でないか検証する。
291         /// 不完全な場合、例外をスローする。
292         /// </summary>
293         /// <exception cref="InvalidOperationException">オブジェクトは不完全。</exception>
294         protected void ValidateIncomplete()
295         {
296             if (String.IsNullOrEmpty(this.Text))
297             {
298                 // ページ本文が設定されていない場合不完全と判定
299                 throw new InvalidOperationException("Text is unset");
300             }
301         }
302
303         /// <summary>
304         /// 指定されたページ解析結果要素から言語間リンクを取得。
305         /// </summary>
306         /// <param name="code">言語コード。</param>
307         /// <param name="element">要素。</param>
308         /// <returns>言語間リンク。見つからない場合は<c>null</c>。</returns>
309         /// <remarks>言語間リンクが複数存在する場合は、先に発見したものを返す。</remarks>
310         private MediaWikiLink GetInterlanguage(string code, IElement element)
311         {
312             if (element is MediaWikiTemplate)
313             {
314                 // Documentationテンプレートがある場合は、その中を探索
315                 MediaWikiLink interlanguage = this.GetDocumentationInterlanguage((MediaWikiTemplate)element, code);
316                 if (interlanguage != null)
317                 {
318                     return interlanguage;
319                 }
320             }
321             else if (element is MediaWikiLink)
322             {
323                 // 指定言語への言語間リンクの場合、内容を取得し、処理終了
324                 MediaWikiLink link = (MediaWikiLink)element;
325                 if (link.Interwiki == code && !link.IsColon)
326                 {
327                     return link;
328                 }
329             }
330             else if (element is IEnumerable<IElement>)
331             {
332                 // 子要素を持つ場合、再帰的に探索
333                 foreach (IElement e in (IEnumerable<IElement>)element)
334                 {
335                     MediaWikiLink interlanguage = this.GetInterlanguage(code, e);
336                     if (interlanguage != null)
337                     {
338                         return interlanguage;
339                     }
340                 }
341             }
342
343             // 未発見の場合null
344             return null;
345         }
346
347         /// <summary>
348         /// 渡されたTemplate:Documentationの呼び出しから、指定された言語コードへの言語間リンクを返す。
349         /// </summary>
350         /// <param name="template">テンプレート呼び出しのリンク。</param>
351         /// <param name="code">言語コード。</param>
352         /// <returns>言語間リンク。見つからない場合またはパラメータが対象外の場合は<c>null</c>。</returns>
353         /// <remarks>言語間リンクが複数存在する場合は、先に発見したものを返す。</remarks>
354         private MediaWikiLink GetDocumentationInterlanguage(MediaWikiTemplate template, string code)
355         {
356             // Documentationテンプレートのリンクかを確認
357             if (!this.IsDocumentationTemplate(template.Title))
358             {
359                 return null;
360             }
361
362             // インライン・コンテンツの可能性があるため、先にパラメータを再帰的に探索
363             foreach (IElement e in template.PipeTexts)
364             {
365                 MediaWikiLink interlanguage = this.GetInterlanguage(code, e);
366                 if (interlanguage != null)
367                 {
368                     return interlanguage;
369                 }
370             }
371
372             // インラインでなさそうな場合、解説記事名を確認
373             string subtitle = ObjectUtils.ToString(template.PipeTexts.ElementAtOrDefault(0));
374             if (String.IsNullOrWhiteSpace(subtitle) || subtitle.Contains('='))
375             {
376                 // 指定されていない場合はデフォルトのページを探索
377                 subtitle = this.Website.DocumentationTemplateDefaultPage;
378             }
379
380             if (String.IsNullOrEmpty(subtitle))
381             {
382                 return null;
383             }
384
385             // ページ名を正規化しつつ、解説ページから言語間リンクを取得
386             MediaWikiPage subpage = null;
387             try
388             {
389                 // ※ ページ名を正規化するのはサブページへの対処
390                 // ※ 本当はここでの取得状況も画面に見せたいが、今のつくりで
391                 //    そうするとややこしくなるので隠蔽する。
392                 subpage = this.Website.GetPage(this.Normalize(new MediaWikiLink(subtitle))) as MediaWikiPage;
393             }
394             catch (FileNotFoundException)
395             {
396                 // 解説ページ無し
397             }
398             catch (Exception ex)
399             {
400                 // 想定外の例外だが、ここではデバッグログを吐いて終了する
401                 // ※ 他の処理と流れが違うため、うまい処理方法が思いつかないので
402                 System.Diagnostics.Debug.WriteLine(ex.ToString());
403             }
404
405             if (subpage != null)
406             {
407                 // サブページの言語間リンクを返す
408                 return subpage.GetInterlanguage(code);
409             }
410
411             // 未発見の場合null
412             return null;
413         }
414
415         /// <summary>
416         /// 渡されたテンプレート名がTemplate:Documentationのいずれかに該当するか?
417         /// </summary>
418         /// <param name="title">テンプレート名。</param>
419         /// <returns>該当する場合<c>true</c>。</returns>
420         private bool IsDocumentationTemplate(string title)
421         {
422             // Documentationテンプレートのリンクかを確認
423             string lowerTitle = title.ToLower();
424             foreach (string docTitle in this.Website.DocumentationTemplates)
425             {
426                 string lowerDocTitle = docTitle.ToLower();
427
428                 // 普通にテンプレート名を比較
429                 if (lowerTitle == lowerDocTitle)
430                 {
431                     return true;
432                 }
433
434                 // 名前空間で一致していない可能性があるので、名前空間を取ってもう一度判定
435                 int index = lowerDocTitle.IndexOf(':');
436                 if (new MediaWikiPage(this.Website, lowerDocTitle).IsTemplate()
437                     && index >= 0 && index + 1 < lowerDocTitle.Length)
438                 {
439                     lowerDocTitle = lowerDocTitle.Substring(lowerDocTitle.IndexOf(':') + 1);
440                 }
441
442                 if (lowerTitle == lowerDocTitle)
443                 {
444                     return true;
445                 }
446             }
447
448             return false;
449         }
450
451         /// <summary>
452         /// このページ内のリンクのサブページ形式の記事名を完全な記事名にする。
453         /// </summary>
454         /// <param name="subpage">サブページ形式の記事名。</param>
455         /// <returns>変換した記事名。</returns>
456         private string NormalizeSubpage(string subpage)
457         {
458             string title = subpage;
459             if (subpage.StartsWith("/"))
460             {
461                 // サブページ(子)へのリンクの場合、親の記事名を補填
462                 title = this.Title + subpage;
463             }
464             else if (subpage.StartsWith("../"))
465             {
466                 // サブページ(親・兄弟・おじ)へのリンクの場合、各階層の記事名を補填
467                 string subtitle = subpage;
468                 int count = 0;
469                 while (subtitle.StartsWith("../"))
470                 {
471                     // 階層をカウント
472                     ++count;
473                     subtitle = subtitle.Substring("../".Length);
474                 }
475
476                 // 指定された階層の記事名を補填
477                 string parent = this.Title;
478                 for (int i = 0; i < count; i++)
479                 {
480                     int index = parent.LastIndexOf('/');
481                     if (index < 0)
482                     {
483                         // 階層が足りない場合、補填できないので元の記事名を返す
484                         return subpage;
485                     }
486
487                     parent = parent.Remove(index);
488                 }
489
490                 // 親記事名と子記事名(あれば)を結合して完了
491                 title = parent;
492                 if (!String.IsNullOrEmpty(subtitle))
493                 {
494                     title += "/" + subtitle;
495                 }
496             }
497
498             // 末尾に / が付いている場合、表示名に関する指定なので除去
499             return title.TrimEnd('/');
500         }
501
502         /// <summary>
503         /// このページ内のテンプレート形式のリンクの記事名を完全な記事名にする。
504         /// </summary>
505         /// <param name="template">このページ内のテンプレート形式のリンク。</param>
506         /// <returns>変換した記事名。</returns>
507         private string NormalizeTemplate(MediaWikiTemplate template)
508         {
509             if (template.IsColon || this.Website.IsNamespace(template.Title)
510                 || this.Website.IsMagicWord(template.Title))
511             {
512                 // 標準名前空間が指定されている(先頭にコロン)
513                 // または何かしらの名前空間が指定されている、
514                 // またはテンプレート呼び出しではなくマジックナンバーの場合、補完不要
515                 return template.Title;
516             }
517
518             // 補完する必要がある場合、名前空間のプレフィックス(Template等)を取得
519             string prefix = this.GetTemplatePrefix();
520             if (String.IsNullOrEmpty(prefix))
521             {
522                 // 名前空間の設定が存在しない場合、何も出来ないため終了
523                 return template.Title;
524             }
525
526             // 頭にプレフィックスを付けた記事名を返す
527             return prefix + ":" + template.Title;
528         }
529
530         /// <summary>
531         /// テンプレート名前空間のプレフィックスを取得。
532         /// </summary>
533         /// <returns>プレフィックス。取得できない場合<c>null</c></returns>
534         private string GetTemplatePrefix()
535         {
536             ISet<string> prefixes = this.Website.Namespaces[this.Website.TemplateNamespace];
537             if (prefixes != null)
538             {
539                 return prefixes.FirstOrDefault();
540             }
541
542             return null;
543         }
544
545         #endregion
546     }
547 }