1 // ================================================================================================
3 // MediaWikiのページをあらわすモデルクラスソース</summary>
5 // <copyright file="MediaWikiPage.cs" company="honeplusのメモ帳">
6 // Copyright (C) 2012 Honeplus. All rights reserved.</copyright>
9 // ================================================================================================
11 namespace Honememo.Wptscs.Websites
14 using System.Collections.Generic;
17 using Honememo.Models;
18 using Honememo.Parsers;
19 using Honememo.Utilities;
20 using Honememo.Wptscs.Parsers;
23 /// MediaWikiのページをあらわすモデルクラスです。
25 public class MediaWikiPage : Page
32 private MediaWikiLink redirect;
39 /// 指定されたMediaWikiの渡されたタイトル, 本文, タイムスタンプのページを作成。
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)
54 /// 指定されたMediaWikiの渡されたタイトル, 本文のページを作成。
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)
68 /// 指定されたMediaWikiの渡されたタイトルのページを作成。
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)
87 public new MediaWiki Website
91 return base.Website as MediaWiki;
103 public override string Text
114 this.redirect = null;
116 // 本文格納のタイミングでリダイレクトページ(#REDIRECT等)かを判定
117 if (!String.IsNullOrEmpty(base.Text))
120 using (MediaWikiRedirectParser parser = new MediaWikiRedirectParser(this.Website))
122 if (parser.TryParse(base.Text, out element))
124 this.redirect = element as MediaWikiLink;
134 /// <exception cref="InvalidOperationException"><see cref="Text"/>が<c>null</c>の場合。</exception>
135 public MediaWikiLink Redirect
139 // Textが設定されている場合のみ有効
140 this.ValidateIncomplete();
141 return this.redirect;
146 this.redirect = value;
155 /// 指定された言語コードへの言語間リンクを返す。
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)
163 // Textが設定されている場合のみ有効
164 this.ValidateIncomplete();
167 // ※ 自ページの解析なのでnoincludeとして前処理を行う
168 return this.GetInterlanguage(code, MediaWikiPreparser.PreprocessByNoinclude(this.Text));
172 /// ページがリダイレクトかをチェック。
174 /// <returns><c>true</c> リダイレクト。</returns>
175 /// <exception cref="InvalidOperationException"><see cref="Text"/>が<c>null</c>の場合。</exception>
176 public bool IsRedirect()
178 // Textが設定されている場合のみ有効
179 return this.Redirect != null;
183 /// ページがテンプレートかをチェック。
185 /// <returns><c>true</c> テンプレート。</returns>
186 public bool IsTemplate()
188 // ページ名がカテゴリー(Category:等で始まる)かをチェック
189 return this.IsNamespacePage(this.Website.TemplateNamespace);
195 /// <returns><c>true</c> カテゴリー。</returns>
196 public bool IsCategory()
198 // ページ名がカテゴリー(Category:等で始まる)かをチェック
199 return this.IsNamespacePage(this.Website.CategoryNamespace);
205 /// <returns><c>true</c> 画像。</returns>
208 // ページ名がファイル(Image:等で始まる)かをチェック
209 return this.IsNamespacePage(this.Website.FileNamespace);
213 /// ページが標準名前空間かをチェック。
215 /// <returns><c>true</c> 標準名前空間。</returns>
218 // ページ名が標準名前空間以外のなんらかの名前空間かをチェック
219 return !this.Website.IsNamespace(this.Title);
223 /// このページ内のリンクの記事名(サブページ等)を完全な記事名にする。
225 /// <param name="link">このページ内のリンク。</param>
226 /// <returns>変換した記事名。</returns>
227 public virtual string Normalize(MediaWikiLink link)
229 string title = StringUtils.DefaultString(link.Title);
230 if (link.IsSubpage())
233 title = this.NormalizeSubpage(title);
235 else if (link is MediaWikiTemplate)
237 // テンプレート関連の正規化(サブページの場合は不要)
238 title = this.NormalizeTemplate((MediaWikiTemplate)link);
249 /// ページが指定された番号の名前空間に所属するかをチェック。
251 /// <param name="id">名前空間のID。</param>
252 /// <returns>所属する場合<c>true</c>。</returns>
253 /// <remarks>大文字小文字は区別しない。</remarks>
254 protected bool IsNamespacePage(int id)
256 // 指定された記事名がカテゴリー(Category:等で始まる)かをチェック
257 int index = this.Title.IndexOf(':');
263 string title = this.Title.Remove(index);
264 IgnoreCaseSet prefixes = this.Website.Namespaces[id];
265 return prefixes != null && prefixes.Contains(title);
269 /// オブジェクトがメソッドの実行に不完全な状態でないか検証する。
272 /// <exception cref="InvalidOperationException"><see cref="Text"/>が<c>null</c>の場合。</exception>
273 protected virtual void ValidateIncomplete()
275 if (this.Text == null)
277 // ページ本文が設定されていない場合不完全と判定
278 throw new InvalidOperationException("Text is unset");
283 /// 指定されたページテキストから言語間リンクを取得。
285 /// <param name="code">言語コード。</param>
286 /// <param name="text">ページテキスト。</param>
287 /// <returns>言語間リンク。見つからない場合は<c>null</c>。</returns>
290 /// 言語間リンクが複数存在する場合は、先に発見したものを返す。
293 /// 稀に障害なのか<see cref="MediaWiki.MetaApi"/>から<c>interwikimap</c>
295 /// (API構文ミスなどでなく、それまで動いていたはずのものが)。
296 /// その場合、そのままでは言語すら判別できないので、念のためこの処理では
297 /// 実行前に強制的に翻訳元/先のコードを
298 /// <see cref="MediaWiki.InterwikiPrefixs"/>に追加している
299 /// (一時的な追加。例外にした方がよいのかもしれないが、それだとその間全く
300 /// 処理が行えないため。かつトランスレータ側なら余計なログが出るぐらいで
301 /// あまり影響がなくても、こちらは解析自体が失敗してしまうため)。
304 private MediaWikiLink GetInterlanguage(string code, string text)
306 // interwikimapに強制的に翻訳元/先のコードを一時的に追加
307 // ※ 2012年2月現在、キャッシュのセットが返ってくるので単純にそこに追加
308 this.Website.InterwikiPrefixs.Add(this.Website.Language.Code);
309 this.Website.InterwikiPrefixs.Add(code);
311 // 渡されたテキストを要素単位に解析し、その結果から言語間リンクを探索する
313 using (MediaWikiParser parser = new MediaWikiParser(this.Website))
315 element = parser.Parse(text);
318 return this.GetInterlanguage(code, element);
322 /// 指定されたページ解析結果要素から言語間リンクを取得。
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)
330 if (element is MediaWikiTemplate)
332 // Documentationテンプレートがある場合は、その中を探索
333 MediaWikiLink interlanguage = this.GetDocumentationInterlanguage((MediaWikiTemplate)element, code);
334 if (interlanguage != null)
336 return interlanguage;
339 else if (element is MediaWikiLink)
341 // 指定言語への言語間リンクの場合、内容を取得し、処理終了
342 MediaWikiLink link = (MediaWikiLink)element;
343 if (link.Interwiki == code && !link.IsColon)
348 else if (element is IEnumerable<IElement>)
351 foreach (IElement e in (IEnumerable<IElement>)element)
353 MediaWikiLink interlanguage = this.GetInterlanguage(code, e);
354 if (interlanguage != null)
356 return interlanguage;
366 /// 渡されたTemplate:Documentationの呼び出しから、指定された言語コードへの言語間リンクを返す。
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)
374 // Documentationテンプレートのリンクかを確認
375 if (!this.IsDocumentationTemplate(template.Title))
380 // インライン・コンテンツの可能性があるため、先にパラメータを再帰的に探索
381 foreach (IElement e in template.PipeTexts)
383 MediaWikiLink interlanguage = this.GetInterlanguage(code, e);
384 if (interlanguage != null)
386 return interlanguage;
390 // インラインでなさそうな場合、解説記事名を確認
391 string subtitle = ObjectUtils.ToString(template.PipeTexts.ElementAtOrDefault(0));
392 if (String.IsNullOrWhiteSpace(subtitle) || subtitle.Contains('='))
394 // 指定されていない場合はデフォルトのページを探索
395 subtitle = this.Website.DocumentationTemplateDefaultPage;
398 if (String.IsNullOrEmpty(subtitle))
403 // ページ名を正規化しつつ、解説ページから言語間リンクを取得
404 MediaWikiPage subpage = null;
407 // ※ ページ名を正規化するのはサブページへの対処
408 // ※ 本当はここでの取得状況も画面に見せたいが、今のつくりで
409 // そうするとややこしくなるので隠蔽する。
410 subpage = this.Website.GetPage(this.Normalize(new MediaWikiLink(subtitle))) as MediaWikiPage;
412 catch (FileNotFoundException)
418 // 想定外の例外だが、ここではデバッグログを吐いて終了する
419 // ※ 他の処理と流れが違うため、うまい処理方法が思いつかないので
420 System.Diagnostics.Debug.WriteLine(ex.ToString());
426 // ※ テンプレート呼び出しなのでincludeとして前処理を行う
427 return subpage.GetInterlanguage(
429 MediaWikiPreparser.PreprocessByInclude(subpage.Text));
437 /// 渡されたテンプレート名がTemplate:Documentationのいずれかに該当するか?
439 /// <param name="title">テンプレート名。</param>
440 /// <returns>該当する場合<c>true</c>。</returns>
441 private bool IsDocumentationTemplate(string title)
443 // Documentationテンプレートのリンクかを確認
444 string lowerTitle = title.ToLower();
445 foreach (string docTitle in this.Website.DocumentationTemplates)
447 string lowerDocTitle = docTitle.ToLower();
450 if (lowerTitle == lowerDocTitle)
455 // 名前空間で一致していない可能性があるので、名前空間を取ってもう一度判定
456 int index = lowerDocTitle.IndexOf(':');
457 if (new MediaWikiPage(this.Website, lowerDocTitle).IsTemplate()
458 && index >= 0 && index + 1 < lowerDocTitle.Length)
460 lowerDocTitle = lowerDocTitle.Substring(lowerDocTitle.IndexOf(':') + 1);
463 if (lowerTitle == lowerDocTitle)
473 /// このページ内のリンクのサブページ形式の記事名を完全な記事名にする。
475 /// <param name="subpage">サブページ形式の記事名。</param>
476 /// <returns>変換した記事名。</returns>
477 private string NormalizeSubpage(string subpage)
479 string title = subpage;
480 if (subpage.StartsWith("/"))
482 // サブページ(子)へのリンクの場合、親の記事名を補填
483 title = this.Title + subpage;
485 else if (subpage.StartsWith("../"))
487 // サブページ(親・兄弟・おじ)へのリンクの場合、各階層の記事名を補填
488 string subtitle = subpage;
490 while (subtitle.StartsWith("../"))
494 subtitle = subtitle.Substring("../".Length);
498 string parent = this.Title;
499 for (int i = 0; i < count; i++)
501 int index = parent.LastIndexOf('/');
504 // 階層が足りない場合、補填できないので元の記事名を返す
508 parent = parent.Remove(index);
511 // 親記事名と子記事名(あれば)を結合して完了
513 if (!String.IsNullOrEmpty(subtitle))
515 title += "/" + subtitle;
519 // 末尾に / が付いている場合、表示名に関する指定なので除去
520 return title.TrimEnd('/');
524 /// このページ内のテンプレート形式のリンクの記事名を完全な記事名にする。
526 /// <param name="template">このページ内のテンプレート形式のリンク。</param>
527 /// <returns>変換した記事名。</returns>
528 private string NormalizeTemplate(MediaWikiTemplate template)
530 if (template.IsColon || this.Website.IsNamespace(template.Title)
531 || this.Website.IsMagicWord(template.Title))
533 // 標準名前空間が指定されている(先頭にコロン)
534 // または何かしらの名前空間が指定されている、
535 // またはテンプレート呼び出しではなくマジックナンバーの場合、補完不要
536 return template.Title;
539 // 補完する必要がある場合、名前空間のプレフィックス(Template等)を取得
540 string prefix = this.GetTemplatePrefix();
541 if (String.IsNullOrEmpty(prefix))
543 // 名前空間の設定が存在しない場合、何も出来ないため終了
544 return template.Title;
547 // 頭にプレフィックスを付けた記事名を返す
548 return prefix + ":" + template.Title;
552 /// テンプレート名前空間のプレフィックスを取得。
554 /// <returns>プレフィックス。取得できない場合<c>null</c></returns>
555 private string GetTemplatePrefix()
557 ISet<string> prefixes = this.Website.Namespaces[this.Website.TemplateNamespace];
558 if (prefixes != null)
560 return prefixes.FirstOrDefault();