1 // ================================================================================================
3 // MediaWikiのページをあらわすモデルクラスソース</summary>
5 // <copyright file="MediaWikiPage.cs" company="honeplusのメモ帳">
6 // Copyright (C) 2010 Honeplus. All rights reserved.</copyright>
9 // ================================================================================================
11 namespace Honememo.Wptscs.Models
14 using System.Collections.Generic;
17 using Honememo.Utilities;
20 /// MediaWikiのページをあらわすモデルクラスです。
22 public class MediaWikiPage : Page
29 public static readonly string NowikiTag = "nowiki";
34 public static readonly string Msgnw = "msgnw:";
43 private Link redirect;
52 /// <param name="website">ページが所属するウェブサイト。</param>
53 /// <param name="title">ページタイトル。</param>
54 /// <param name="text">ページの本文。</param>
55 /// <param name="timestamp">ページのタイムスタンプ。</param>
56 public MediaWikiPage(MediaWiki website, string title, string text, DateTime? timestamp)
57 : base(website, title, text, timestamp)
63 /// ページのタイムスタンプには<c>null</c>を設定。
65 /// <param name="website">ページが所属するウェブサイト。</param>
66 /// <param name="title">ページタイトル。</param>
67 /// <param name="text">ページの本文。</param>
68 public MediaWikiPage(MediaWiki website, string title, string text)
69 : base(website, title, text)
75 /// ページの本文, タイムスタンプには<c>null</c>を設定。
77 /// <param name="website">ページが所属するウェブサイト。</param>
78 /// <param name="title">ページタイトル。</param>
79 public MediaWikiPage(MediaWiki website, string title)
80 : base(website, title)
91 public new MediaWiki Website
95 return base.Website as MediaWiki;
100 base.Website = value;
107 public override string Text
119 // 本文格納のタイミングでリダイレクトページ(#REDIRECT等)かを判定
120 if (!String.IsNullOrEmpty(base.Text))
122 this.TryParseRedirect();
134 // Textが設定されている場合のみ有効
135 this.ValidateIncomplete();
136 return this.redirect;
141 this.redirect = value;
150 /// 渡されたテキストがnowikiブロックかを解析する。
152 /// <param name="text">解析するテキスト。</param>
153 /// <param name="nowiki">解析したnowikiブロック。</param>
154 /// <returns>nowikiブロックの場合<c>true</c>。</returns>
156 /// nowikiブロックと判定するには、1文字目が開始タグである必要がある。
157 /// ただし、後ろについては閉じタグが無ければ全て、あればそれ以降は無視する。
160 public static bool TryParseNowiki(string text, out string nowiki)
163 LazyXmlParser parser = new LazyXmlParser();
164 LazyXmlParser.SimpleElement element;
165 if (parser.TryParse(text, out element))
167 if (element.Name.ToLower() == MediaWikiPage.NowikiTag)
169 nowiki = element.OuterXml;
182 /// 指定された言語コードへの言語間リンクを返す。
184 /// <param name="code">言語コード。</param>
185 /// <returns>言語間リンク先の記事名。見つからない場合は空。</returns>
186 /// <remarks>言語間リンクが複数存在する場合は、先に発見したものを返す。</remarks>
187 public string GetInterWiki(string code)
189 // Textが設定されている場合のみ有効
190 this.ValidateIncomplete();
192 // 記事に存在する指定言語への言語間リンクを取得
193 for (int i = 0; i < this.Text.Length; i++)
195 char c = this.Text[i];
200 // コメント(<!--)またはnowiki区間の場合飛ばす
201 string subtext = this.Text.Substring(i);
203 if (LazyXmlParser.TryParseComment(subtext, out value))
205 i += value.Length - 1;
207 else if (MediaWikiPage.TryParseNowiki(subtext, out value))
209 i += value.Length - 1;
216 if (this.TryParseTemplate(this.Text.Substring(i), out link))
218 i += link.OriginalText.Length - 1;
220 // Documentationテンプレートがある場合は、その中を探索
221 string interWiki = this.GetDocumentationInterWiki(link, code);
222 if (!String.IsNullOrEmpty(interWiki))
232 if (this.TryParseLink(this.Text.Substring(i), out link))
234 i += link.OriginalText.Length - 1;
236 // 指定言語への言語間リンクの場合、内容を取得し、処理終了
237 if (link.Code == code && !link.IsColon)
252 /// ページがリダイレクトかをチェック。
254 /// <returns><c>true</c> リダイレクト。</returns>
255 public bool IsRedirect()
257 // Textが設定されている場合のみ有効
258 return this.Redirect != null;
262 /// ページがテンプレートかをチェック。
264 /// <returns><c>true</c> テンプレート。</returns>
265 public bool IsTemplate()
267 // 指定された記事名がカテゴリー(Category:等で始まる)かをチェック
268 return this.IsNamespacePage(this.Website.TemplateNamespace);
274 /// <returns><c>true</c> カテゴリー。</returns>
275 public bool IsCategory()
277 // 指定された記事名がカテゴリー(Category:等で始まる)かをチェック
278 return this.IsNamespacePage(this.Website.CategoryNamespace);
284 /// <returns><c>true</c> 画像。</returns>
287 // 指定されたページ名がファイル(Image:等で始まる)かをチェック
288 return this.IsNamespacePage(this.Website.FileNamespace);
292 /// ページが標準名前空間かをチェック。
294 /// <returns><c>true</c> 標準名前空間。</returns>
297 // 指定されたページ名が標準名前空間以外の名前空間(Wikipedia:等で始まる)かをチェック
298 string title = this.Title.ToLower();
299 foreach (IList<string> prefixes in this.Website.Namespaces.Values)
301 foreach (string prefix in prefixes)
303 if (title.StartsWith(prefix.ToLower() + ":"))
313 #region Linkクラスに移動したいメソッド
315 // TODO: 以下の各メソッドのうち、リンクに関するものはLinkクラスに移したい。
316 // また、余計な依存関係を持っているものを整理したい。
319 /// 渡されたWikipediaの内部リンクを解析。
321 /// <param name="text">[[で始まる文字列。</param>
322 /// <param name="link">解析したリンク。</param>
323 /// <returns>解析に成功した場合<c>true</c>。</returns>
324 public bool TryParseLink(string text, out Link link)
330 if (!text.StartsWith("[["))
335 // 構文を解析して、[[]]内部の文字列を取得
336 // ※構文はWikipediaのプレビューで色々試して確認、足りなかったり間違ってたりするかも・・・
337 string article = String.Empty;
338 string section = String.Empty;
339 IList<string> pipeTexts = new List<string>();
342 bool sharpFlag = false;
343 for (int i = 2; i < text.Length; i++)
348 if (StringUtils.StartsWith(text, "]]", i))
354 // | が含まれている場合、以降の文字列は表示名などとして扱う
358 pipeTexts.Add(String.Empty);
362 // 変数([[{{{1}}}]]とか)の再帰チェック
365 int index = this.ChkVariable(out variable, out dummy, text, i);
371 pipeTexts[pipeCounter - 1] += variable;
386 if (pipeCounter <= 0)
388 // 変数以外で { } または < > [ ] \n が含まれている場合、リンクは無効
389 if ((c == '<') || (c == '>') || (c == '[') || (c == ']') || (c == '{') || (c == '}') || (c == '\n'))
397 // #が含まれている場合、以降の文字列は見出しへのリンクとして扱う(1つめの#のみ有効)
418 string subtext = text.Substring(i);
420 if (LazyXmlParser.TryParseComment(subtext, out value))
422 // コメント(<!--)が含まれている場合、リンクは無効
425 else if (MediaWikiPage.TryParseNowiki(subtext, out value))
428 i += value.Length - 1;
429 pipeTexts[pipeCounter - 1] += value;
434 // リンク [[ {{ ([[image:xx|[[test]]の画像]]とか)の再帰チェック
436 index = this.ChkLinkText(out l, text, i);
440 pipeTexts[pipeCounter - 1] += l.OriginalText;
444 pipeTexts[pipeCounter - 1] += c;
454 // 解析に成功した場合、結果を出力値に設定
457 // 変数ブロックの文字列をリンクのテキストに設定
458 link.OriginalText = text.Substring(0, lastIndex + 1);
460 // 前後のスペースは削除(見出しは後ろのみ)
461 link.Title = article.Trim();
462 link.Section = section.TrimEnd();
465 link.PipeTexts = pipeTexts;
469 if (link.Title.StartsWith("/"))
471 link.IsSubpage = true;
473 else if (link.Title.StartsWith(":"))
477 link.Title = link.Title.TrimStart(':').TrimStart();
480 // 標準名前空間以外で[[xxx:yyy]]のようになっている場合、言語コード
481 if (link.Title.Contains(":") && new MediaWikiPage(this.Website, link.Title).IsMain())
483 // ※本当は、言語コード等の一覧を作り、其処と一致するものを・・・とすべきだろうが、
484 // メンテしきれないので : を含む名前空間以外を全て言語コード等と判定
485 link.Code = link.Title.Substring(0, link.Title.IndexOf(':')).TrimEnd();
486 link.Title = link.Title.Substring(link.Title.IndexOf(':') + 1).TrimStart();
493 /// 渡されたWikipediaのテンプレートを解析。
495 /// <param name="text">{{で始まる文字列。</param>
496 /// <param name="link">解析したテンプレートのリンク。</param>
497 /// <returns>解析に成功した場合<c>true</c>。</returns>
498 public bool TryParseTemplate(string text, out Link link)
504 if (!text.StartsWith("{{"))
509 // 構文を解析して、{{}}内部の文字列を取得
510 // ※構文はWikipediaのプレビューで色々試して確認、足りなかったり間違ってたりするかも・・・
511 string article = String.Empty;
512 IList<string> pipeTexts = new List<string>();
515 for (int i = 2; i < text.Length; i++)
520 if (StringUtils.StartsWith(text, "}}", i))
526 // | が含まれている場合、以降の文字列は引数などとして扱う
530 pipeTexts.Add(String.Empty);
534 // 変数([[{{{1}}}]]とか)の再帰チェック
537 int index = this.ChkVariable(out variable, out dummy, text, i);
543 pipeTexts[pipeCounter - 1] += variable;
554 if (pipeCounter <= 0)
556 // 変数以外で < > [ ] { } が含まれている場合、リンクは無効
557 if ((c == '<') || (c == '>') || (c == '[') || (c == ']') || (c == '{') || (c == '}'))
569 string subtext = text.Substring(i);
571 if (LazyXmlParser.TryParseComment(subtext, out value))
573 // コメント(<!--)が含まれている場合、リンクは無効
576 else if (MediaWikiPage.TryParseNowiki(subtext, out value))
579 i += value.Length - 1;
580 pipeTexts[pipeCounter - 1] += value;
585 // リンク [[ {{ ({{test|[[例]]}}とか)の再帰チェック
587 index = this.ChkLinkText(out l, text, i);
591 pipeTexts[pipeCounter - 1] += l.OriginalText;
595 pipeTexts[pipeCounter - 1] += c;
605 // 解析に成功した場合、結果を出力値に設定
607 link.IsTemplateTag = true;
609 // 変数ブロックの文字列をリンクのテキストに設定
610 link.OriginalText = text.Substring(0, lastIndex + 1);
612 // 前後のスペース・改行は削除(見出しは後ろのみ)
613 link.Title = article.Trim();
616 link.PipeTexts = pipeTexts;
620 if (link.Title.StartsWith("/") == true)
622 link.IsSubpage = true;
624 else if (link.Title.StartsWith(":"))
628 link.Title = link.Title.TrimStart(':').TrimStart();
632 link.IsMsgnw = link.Title.ToLower().StartsWith(Msgnw.ToLower());
635 link.Title = link.Title.Substring(Msgnw.Length);
639 if (article.TrimEnd(' ').EndsWith("\n"))
650 /// 渡されたテキストの指定された位置に存在するWikipediaの内部リンク・テンプレートをチェック。
652 /// <param name="link">解析したリンク。</param>
653 /// <param name="text">解析するテキスト。</param>
654 /// <param name="index">解析開始インデックス。</param>
655 /// <returns>正常時の戻り値には、]]の後ろの]の位置のインデックスを返す。異常時は-1。</returns>
656 public int ChkLinkText(out Link link, string text, int index)
659 if (StringUtils.StartsWith(text, "[[", index))
662 if (this.TryParseLink(text.Substring(index), out link))
664 return index + link.OriginalText.Length - 1;
667 else if (StringUtils.StartsWith(text, "{{", index))
670 if (this.TryParseTemplate(text.Substring(index), out link))
672 return index + link.OriginalText.Length - 1;
676 // 出力値初期化。リンク以外の場合、nullを返す
682 /// 渡されたテキストの指定された位置に存在する変数を解析。
684 /// <param name="variable">解析した変数。</param>
685 /// <param name="value">変数のパラメータ値。</param>
686 /// <param name="text">解析するテキスト。</param>
687 /// <param name="index">解析開始インデックス。</param>
688 /// <returns>正常時の戻り値には、変数の終了位置のインデックスを返す。異常時は-1。</returns>
689 public int ChkVariable(out string variable, out string value, string text, int index)
693 variable = String.Empty;
694 value = String.Empty;
697 if (!StringUtils.StartsWith(text, "{{{", index))
703 bool pipeFlag = false;
704 for (int i = index + 3; i < text.Length; i++)
707 if (StringUtils.StartsWith(text, "}}}", i))
716 if (LazyXmlParser.TryParseComment(text.Substring(i), out comment))
719 i += comment.Length - 1;
724 // | が含まれている場合、以降の文字列は代入された値として扱う
732 // ※Wikipediaの仕様上は、{{{1{|表示}}} のように変数名の欄に { を
733 // 含めることができるようだが、判別しきれないので、エラーとする
734 // (どうせ意図してそんなことする人は居ないだろうし・・・)
746 if (MediaWikiPage.TryParseNowiki(text.Substring(i), out nowiki))
749 i += nowiki.Length - 1;
755 // 変数({{{1|{{{2}}}}}}とか)の再帰チェック
758 int subindex = this.ChkVariable(out var, out dummy, text, i);
766 // リンク [[ {{ ({{{1|[[test]]}}}とか)の再帰チェック
768 subindex = this.ChkLinkText(out link, text, i);
772 value += link.OriginalText;
783 variable = text.Substring(index, lastIndex - index + 1);
787 // 正常な構文ではなかった場合、出力値をクリア
788 variable = String.Empty;
789 value = String.Empty;
797 #region 内部処理用インスタンスメソッド
800 /// ページが指定された番号の名前空間に所属するかをチェック。
802 /// <param name="id">名前空間のID。</param>
803 /// <returns><c>true</c> 所属する。</returns>
804 protected bool IsNamespacePage(int id)
806 // 指定された記事名がカテゴリー(Category:等で始まる)かをチェック
807 IList<string> prefixes = this.Website.Namespaces[id];
808 if (prefixes != null)
810 string title = this.Title.ToLower();
811 foreach (string prefix in prefixes)
813 if (title.StartsWith(prefix.ToLower() + ":"))
824 /// オブジェクトがメソッドの実行に不完全な状態でないか検証する。
827 /// <exception cref="InvalidOperationException">オブジェクトは不完全。</exception>
828 protected void ValidateIncomplete()
830 if (String.IsNullOrEmpty(this.Text))
832 // ページ本文が設定されていない場合不完全と判定
833 throw new InvalidOperationException("Text is unset");
838 /// 現在のページをリダイレクトとして解析する。
840 /// <returns>リダイレクトの場合<c>true</c>。</returns>
841 /// <remarks>リダイレクトの場合、転送先ページ名をプロパティに格納。</remarks>
842 private bool TryParseRedirect()
844 // 日本語版みたいに、#REDIRECTと言語固有の#転送みたいなのがあると思われるので、
845 // 翻訳元言語とデフォルトの設定でチェック
846 this.Redirect = null;
847 for (int i = 0; i < 2; i++)
849 string format = this.Website.Redirect;
852 format = Properties.Settings.Default.MediaWikiRedirect;
855 if (!String.IsNullOrEmpty(format)
856 && this.Text.ToLower().StartsWith(format.ToLower()))
859 if (this.TryParseLink(this.Text.Substring(format.Length).TrimStart(), out link))
861 this.Redirect = link;
871 /// 渡されたTemplate:Documentationの呼び出しから、指定された言語コードへの言語間リンクを返す。
873 /// <param name="link">テンプレート呼び出しのリンク。</param>
874 /// <param name="code">言語コード。</param>
875 /// <returns>言語間リンク先の記事名。見つからない場合またはパラメータが対象外の場合は空。</returns>
876 /// <remarks>言語間リンクが複数存在する場合は、先に発見したものを返す。</remarks>
877 private string GetDocumentationInterWiki(Link link, string code)
879 // テンプレートタグか、この言語にTemplate:Documentationの設定がされているかを確認
880 string docTitle = this.Website.DocumentationTemplate;
881 if (!link.IsTemplateTag || String.IsNullOrEmpty(docTitle))
886 // Documentationテンプレートのリンクかを確認
887 if (link.Title.ToLower() != docTitle.ToLower())
889 // 名前空間で一致していない可能性があるので、名前空間を取ってもう一度判定
890 int index = docTitle.IndexOf(':');
891 if (new MediaWikiPage(this.Website, docTitle).IsTemplate()
892 && index >= 0 && index + 1 < docTitle.Length)
894 docTitle = docTitle.Substring(docTitle.IndexOf(':') + 1);
897 if (link.Title.ToLower() != docTitle.ToLower())
899 // どちらでも一致しない場合は別のテンプレートなりなので無視
905 string subtitle = link.PipeTexts.ElementAtOrDefault(0);
906 if (String.IsNullOrWhiteSpace(subtitle) || subtitle.Contains('='))
908 // 指定されていない場合はデフォルトのページを探索
909 subtitle = this.Website.DocumentationTemplateDefaultPage;
912 if (String.IsNullOrEmpty(subtitle))
917 // サブページの場合、親ページのページ名を付加
918 // TODO: サブページの仕組みについては要再検討
919 if (subtitle.StartsWith("/"))
921 subtitle = this.Title + subtitle;
925 MediaWikiPage subpage = null;
928 // ※ 本当はここでの取得状況も画面に見せたいが、今のつくりで
929 // そうするとややこしくなるので隠蔽する。
930 subpage = this.Website.GetPage(subtitle) as MediaWikiPage;
934 System.Diagnostics.Debug.WriteLine(ex.StackTrace);
939 string interWiki = subpage.GetInterWiki(code);
940 if (!String.IsNullOrEmpty(interWiki))
955 /// Wikipediaのリンクの要素を格納するための構造体。
962 /// リンクのオブジェクト作成時の元テキスト([[~]])。
964 public string OriginalText
967 //// TODO: このクラスにParseを移動完了したら、protectedにする
974 /// <remarks>リンクに記載されていた記事名であり、名前空間の情報などは含まない可能性があるため注意。</remarks>
984 public string Section
993 public IList<string> PipeTexts
1000 /// 言語間または他プロジェクトへのリンクの場合、コード。
1009 /// テンプレートタグで書かれたリンク({{~}})か?
1012 /// 必ずしもリンク先がテンプレートであることを意味しない。
1013 /// 普通のページをこの書式でテンプレートのように使用することも可能である。
1015 public bool IsTemplateTag
1022 /// 記事名の先頭がサブページを示す / で始まるか?
1024 /// <remarks>※ 2010年9月現在、この処理には不足あり。</remarks>
1025 public bool IsSubpage
1027 // TODO: サブページには相対パスで[[../~]]や[[../../~]]というような書き方もある模様。
1028 // この辺りの処理は[[Help:サブページ]]を元に全面的に見直す必要あり
1034 /// リンクの先頭が : で始まるかを示すフラグ。
1043 /// テンプレートの場合に、テンプレートのソースをそのまま出力することを示す msgnw: が付加されているか?
1052 /// テンプレートの場合に、記事名の後で改行が入るか?
1061 /// リンクが表すテキスト([[~]])。
1068 StringBuilder b = new StringBuilder();
1071 string startSign = "[[";
1072 string endSign = "]]";
1073 if (this.IsTemplateTag)
1080 b.Append(startSign);
1088 // msgnw: (テンプレートを<nowiki>タグで挟む)の付加
1089 if (this.IsTemplateTag && this.IsMsgnw)
1091 b.Append(MediaWikiPage.Msgnw);
1094 // 言語コード・他プロジェクトコードの付加
1095 if (!String.IsNullOrEmpty(this.Code))
1097 b.Append(this.Code);
1101 if (!String.IsNullOrEmpty(this.Title))
1103 b.Append(this.Title);
1107 if (!String.IsNullOrEmpty(this.Section))
1110 b.Append(this.Section);
1120 if (this.PipeTexts != null)
1122 foreach (string s in this.PipeTexts)
1125 if (!String.IsNullOrEmpty(s))
1134 return b.ToString();
1143 /// このオブジェクトを表すリンク文字列を返す。
1145 /// <returns>オブジェクトを表すリンク文字列。</returns>
1146 public override string ToString()
1148 // リンクを表すテキスト、ならびに元テキストを返す
1149 return this.Text + "<!-- " + this.OriginalText + " -->";