OSDN Git Service

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