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;
35 /// ページの本文をパーサーで要素単位に解析した結果。
37 private IElement element;
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)
57 /// ページのタイムスタンプには<c>null</c>を設定。
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)
69 /// ページの本文, タイムスタンプには<c>null</c>を設定。
71 /// <param name="website">ページが所属するウェブサイト。</param>
72 /// <param name="title">ページタイトル。</param>
73 public MediaWikiPage(MediaWiki website, string title)
74 : base(website, title)
85 public new MediaWiki Website
89 return base.Website as MediaWiki;
101 public override string Text
112 this.redirect = null;
115 // 本文格納のタイミングでリダイレクトページ(#REDIRECT等)かを判定
116 if (!String.IsNullOrEmpty(base.Text))
119 if (new MediaWikiRedirectParser(this.Website).TryParse(base.Text, out element))
121 this.redirect = element as MediaWikiLink;
130 /// <exception cref="InvalidOperationException"><see cref="Text"/>が<c>null</c>の場合。</exception>
131 public MediaWikiLink Redirect
135 // Textが設定されている場合のみ有効
136 this.ValidateIncomplete();
137 return this.redirect;
142 this.redirect = value;
147 /// ページの本文をパーサーで要素単位に解析した結果。
149 /// <exception cref="InvalidOperationException"><see cref="Text"/>が<c>null</c>の場合。</exception>
150 /// <remarks>get時にページの解析を行う。</remarks>
151 public IElement Element
155 // Textが設定されている場合のみ有効
156 this.ValidateIncomplete();
157 if (this.element == null)
159 // ページサイズによっては時間がかかるので、必要な場合だけ実施
160 this.element = new MediaWikiParser(this.Website).Parse(this.Text);
168 this.element = value;
177 /// 指定された言語コードへの言語間リンクを返す。
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)
185 // Textが設定されている場合のみ有効
186 this.ValidateIncomplete();
188 // 記事を解析し、その結果から言語間リンクを探索
189 return this.GetInterlanguage(code, this.Element);
193 /// ページがリダイレクトかをチェック。
195 /// <returns><c>true</c> リダイレクト。</returns>
196 /// <exception cref="InvalidOperationException"><see cref="Text"/>が<c>null</c>の場合。</exception>
197 public bool IsRedirect()
199 // Textが設定されている場合のみ有効
200 return this.Redirect != null;
204 /// ページがテンプレートかをチェック。
206 /// <returns><c>true</c> テンプレート。</returns>
207 public bool IsTemplate()
209 // ページ名がカテゴリー(Category:等で始まる)かをチェック
210 return this.IsNamespacePage(this.Website.TemplateNamespace);
216 /// <returns><c>true</c> カテゴリー。</returns>
217 public bool IsCategory()
219 // ページ名がカテゴリー(Category:等で始まる)かをチェック
220 return this.IsNamespacePage(this.Website.CategoryNamespace);
226 /// <returns><c>true</c> 画像。</returns>
229 // ページ名がファイル(Image:等で始まる)かをチェック
230 return this.IsNamespacePage(this.Website.FileNamespace);
234 /// ページが標準名前空間かをチェック。
236 /// <returns><c>true</c> 標準名前空間。</returns>
239 // ページ名が標準名前空間以外のなんらかの名前空間かをチェック
240 return !this.Website.IsNamespace(this.Title);
244 /// このページ内のリンクの記事名(サブページ等)を完全な記事名にする。
246 /// <param name="link">このページ内のリンク。</param>
247 /// <returns>変換した記事名。</returns>
248 public string Normalize(MediaWikiLink link)
250 string title = StringUtils.DefaultString(link.Title);
251 if (link.IsSubpage())
254 title = this.NormalizeSubpage(title);
256 else if (link is MediaWikiTemplate)
258 // テンプレート関連の正規化(サブページの場合は不要)
259 title = this.NormalizeTemplate((MediaWikiTemplate)link);
270 /// ページが指定された番号の名前空間に所属するかをチェック。
272 /// <param name="id">名前空間のID。</param>
273 /// <returns>所属する場合<c>true</c>。</returns>
274 /// <remarks>大文字小文字は区別しない。</remarks>
275 protected bool IsNamespacePage(int id)
277 // 指定された記事名がカテゴリー(Category:等で始まる)かをチェック
278 int index = this.Title.IndexOf(':');
284 string title = this.Title.Remove(index);
285 IgnoreCaseSet prefixes = this.Website.Namespaces[id];
286 return prefixes != null && prefixes.Contains(title);
290 /// オブジェクトがメソッドの実行に不完全な状態でないか検証する。
293 /// <exception cref="InvalidOperationException">オブジェクトは不完全。</exception>
294 protected void ValidateIncomplete()
296 if (String.IsNullOrEmpty(this.Text))
298 // ページ本文が設定されていない場合不完全と判定
299 throw new InvalidOperationException("Text is unset");
304 /// 指定されたページ解析結果要素から言語間リンクを取得。
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)
312 if (element is MediaWikiTemplate)
314 // Documentationテンプレートがある場合は、その中を探索
315 MediaWikiLink interlanguage = this.GetDocumentationInterlanguage((MediaWikiTemplate)element, code);
316 if (interlanguage != null)
318 return interlanguage;
321 else if (element is MediaWikiLink)
323 // 指定言語への言語間リンクの場合、内容を取得し、処理終了
324 MediaWikiLink link = (MediaWikiLink)element;
325 if (link.Interwiki == code && !link.IsColon)
330 else if (element is IEnumerable<IElement>)
333 foreach (IElement e in (IEnumerable<IElement>)element)
335 MediaWikiLink interlanguage = this.GetInterlanguage(code, e);
336 if (interlanguage != null)
338 return interlanguage;
348 /// 渡されたTemplate:Documentationの呼び出しから、指定された言語コードへの言語間リンクを返す。
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)
356 // Documentationテンプレートのリンクかを確認
357 if (!this.IsDocumentationTemplate(template.Title))
362 // インライン・コンテンツの可能性があるため、先にパラメータを再帰的に探索
363 foreach (IElement e in template.PipeTexts)
365 MediaWikiLink interlanguage = this.GetInterlanguage(code, e);
366 if (interlanguage != null)
368 return interlanguage;
372 // インラインでなさそうな場合、解説記事名を確認
373 string subtitle = ObjectUtils.ToString(template.PipeTexts.ElementAtOrDefault(0));
374 if (String.IsNullOrWhiteSpace(subtitle) || subtitle.Contains('='))
376 // 指定されていない場合はデフォルトのページを探索
377 subtitle = this.Website.DocumentationTemplateDefaultPage;
380 if (String.IsNullOrEmpty(subtitle))
385 // ページ名を正規化しつつ、解説ページから言語間リンクを取得
386 MediaWikiPage subpage = null;
389 // ※ ページ名を正規化するのはサブページへの対処
390 // ※ 本当はここでの取得状況も画面に見せたいが、今のつくりで
391 // そうするとややこしくなるので隠蔽する。
392 subpage = this.Website.GetPage(this.Normalize(new MediaWikiLink(subtitle))) as MediaWikiPage;
394 catch (FileNotFoundException)
400 // 想定外の例外だが、ここではデバッグログを吐いて終了する
401 // ※ 他の処理と流れが違うため、うまい処理方法が思いつかないので
402 System.Diagnostics.Debug.WriteLine(ex.ToString());
408 return subpage.GetInterlanguage(code);
416 /// 渡されたテンプレート名がTemplate:Documentationのいずれかに該当するか?
418 /// <param name="title">テンプレート名。</param>
419 /// <returns>該当する場合<c>true</c>。</returns>
420 private bool IsDocumentationTemplate(string title)
422 // Documentationテンプレートのリンクかを確認
423 string lowerTitle = title.ToLower();
424 foreach (string docTitle in this.Website.DocumentationTemplates)
426 string lowerDocTitle = docTitle.ToLower();
429 if (lowerTitle == lowerDocTitle)
434 // 名前空間で一致していない可能性があるので、名前空間を取ってもう一度判定
435 int index = lowerDocTitle.IndexOf(':');
436 if (new MediaWikiPage(this.Website, lowerDocTitle).IsTemplate()
437 && index >= 0 && index + 1 < lowerDocTitle.Length)
439 lowerDocTitle = lowerDocTitle.Substring(lowerDocTitle.IndexOf(':') + 1);
442 if (lowerTitle == lowerDocTitle)
452 /// このページ内のリンクのサブページ形式の記事名を完全な記事名にする。
454 /// <param name="subpage">サブページ形式の記事名。</param>
455 /// <returns>変換した記事名。</returns>
456 private string NormalizeSubpage(string subpage)
458 string title = subpage;
459 if (subpage.StartsWith("/"))
461 // サブページ(子)へのリンクの場合、親の記事名を補填
462 title = this.Title + subpage;
464 else if (subpage.StartsWith("../"))
466 // サブページ(親・兄弟・おじ)へのリンクの場合、各階層の記事名を補填
467 string subtitle = subpage;
469 while (subtitle.StartsWith("../"))
473 subtitle = subtitle.Substring("../".Length);
477 string parent = this.Title;
478 for (int i = 0; i < count; i++)
480 int index = parent.LastIndexOf('/');
483 // 階層が足りない場合、補填できないので元の記事名を返す
487 parent = parent.Remove(index);
490 // 親記事名と子記事名(あれば)を結合して完了
492 if (!String.IsNullOrEmpty(subtitle))
494 title += "/" + subtitle;
498 // 末尾に / が付いている場合、表示名に関する指定なので除去
499 return title.TrimEnd('/');
503 /// このページ内のテンプレート形式のリンクの記事名を完全な記事名にする。
505 /// <param name="template">このページ内のテンプレート形式のリンク。</param>
506 /// <returns>変換した記事名。</returns>
507 private string NormalizeTemplate(MediaWikiTemplate template)
509 if (template.IsColon || this.Website.IsNamespace(template.Title)
510 || this.Website.IsMagicWord(template.Title))
512 // 標準名前空間が指定されている(先頭にコロン)
513 // または何かしらの名前空間が指定されている、
514 // またはテンプレート呼び出しではなくマジックナンバーの場合、補完不要
515 return template.Title;
518 // 補完する必要がある場合、名前空間のプレフィックス(Template等)を取得
519 string prefix = this.GetTemplatePrefix();
520 if (String.IsNullOrEmpty(prefix))
522 // 名前空間の設定が存在しない場合、何も出来ないため終了
523 return template.Title;
526 // 頭にプレフィックスを付けた記事名を返す
527 return prefix + ":" + template.Title;
531 /// テンプレート名前空間のプレフィックスを取得。
533 /// <returns>プレフィックス。取得できない場合<c>null</c></returns>
534 private string GetTemplatePrefix()
536 ISet<string> prefixes = this.Website.Namespaces[this.Website.TemplateNamespace];
537 if (prefixes != null)
539 return prefixes.FirstOrDefault();