1 // ================================================================================================
3 // MediaWikiのウェブサイト(システム)をあらわすモデルクラスソース</summary>
5 // <copyright file="MediaWiki.cs" company="honeplusのメモ帳">
6 // Copyright (C) 2013 Honeplus. All rights reserved.</copyright>
9 // ================================================================================================
11 namespace Honememo.Wptscs.Websites
14 using System.Collections.Generic;
18 using System.Xml.Linq;
19 using System.Xml.Serialization;
20 using Honememo.Models;
21 using Honememo.Utilities;
22 using Honememo.Wptscs.Models;
23 using Honememo.Wptscs.Properties;
24 using Honememo.Wptscs.Utilities;
27 /// MediaWikiのウェブサイト(システム)をあらわすモデルクラスです。
29 public class MediaWiki : Website, IXmlSerializable
34 /// 名前空間情報取得用にアクセスするAPI。
36 private string metaApi;
39 /// MediaWiki記事データ取得用にアクセスするAPI。
41 private string contentApi;
44 /// MediaWiki言語間リンク取得用にアクセスするAPI。
46 private string interlanguageApi;
51 private int? templateNamespace;
56 private int? categoryNamespace;
61 private int? fileNamespace;
64 /// MediaWiki書式のシステム定義変数。
66 private ISet<string> magicWords;
69 /// MediaWikiの名前空間の情報。
71 private IDictionary<int, IgnoreCaseSet> namespaces;
74 /// MediaWikiのウィキ間リンクのプレフィックス情報。
76 private IgnoreCaseSet interwikiPrefixs;
79 /// MediaWikiのウィキ間リンクのプレフィックス情報(APIから取得した値と設定値の集合)。
81 private IgnoreCaseSet interwikiPrefixCaches;
84 /// <see cref="InitializeByMetaApi"/>同期用ロックオブジェクト。
86 private object lockLoadMetaApi = new object();
93 /// 指定された言語, サーバーのMediaWikiを表すインスタンスを作成。
95 /// <param name="language">ウェブサイトの言語。</param>
96 /// <param name="location">ウェブサイトの場所。</param>
97 /// <exception cref="ArgumentNullException"><paramref name="language"/>または<paramref name="location"/>が<c>null</c>の場合。</exception>
98 /// <exception cref="ArgumentException"><paramref name="location"/>が空の文字列の場合。</exception>
99 public MediaWiki(Language language, string location)
100 : base(language, location)
105 /// 指定された言語のWikipediaを表すインスタンスを作成。
107 /// <param name="language">ウェブサイトの言語。</param>
108 /// <exception cref="ArgumentNullException"><c>null</c>が指定された場合。</exception>
109 public MediaWiki(Language language)
111 // 親で初期化していないのは、languageのnullチェックの前にnull参照でエラーになってしまうから
112 this.Language = language;
113 this.Location = string.Format(Settings.Default.WikipediaLocation, language.Code);
117 /// 空のインスタンスを作成(シリアライズ or 拡張用)。
119 protected MediaWiki()
125 #region 設定ファイルに初期値を持つプロパティ
128 /// MediaWikiメタ情報取得用にアクセスするAPI。
130 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
131 public string MetaApi
135 if (string.IsNullOrEmpty(this.metaApi))
137 return Settings.Default.MediaWikiMetaApi;
145 this.metaApi = value;
150 /// MediaWiki記事データ取得用にアクセスするAPI。
152 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
153 public string ContentApi
157 if (string.IsNullOrEmpty(this.contentApi))
159 return Settings.Default.MediaWikiContentApi;
162 return this.contentApi;
167 this.contentApi = value;
172 /// MediaWiki言語間リンク取得用にアクセスするAPI。
174 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
175 public string InterlanguageApi
179 if (string.IsNullOrEmpty(this.interlanguageApi))
181 return Settings.Default.MediaWikiInterlanguageApi;
184 return this.interlanguageApi;
189 this.interlanguageApi = value;
194 /// テンプレートの名前空間を示す番号。
196 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
197 public int TemplateNamespace
201 return this.templateNamespace ?? Settings.Default.MediaWikiTemplateNamespace;
206 this.templateNamespace = value;
213 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
214 public int CategoryNamespace
218 return this.categoryNamespace ?? Settings.Default.MediaWikiCategoryNamespace;
223 this.categoryNamespace = value;
230 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
231 public int FileNamespace
235 return this.fileNamespace ?? Settings.Default.MediaWikiFileNamespace;
240 this.fileNamespace = value;
245 /// MediaWiki書式のシステム定義変数。
248 /// 値が指定されていない場合、デフォルト値を返す。
251 public ISet<string> MagicWords
255 if (this.magicWords == null)
257 // ※ 初期値は http://www.mediawiki.org/wiki/Help:Magic_words 等を参考に設定。
258 // APIからも取得できるが、2012年2月現在 #expr でなければ認識されないものが
259 // exprで返ってきたりとアプリで使うには情報が足りないため人力で対応。
260 return new HashSet<string>(Settings.Default.MediaWikiMagicWords.Cast<string>());
263 return this.magicWords;
268 this.magicWords = value;
274 #region サーバーから値を取得するプロパティ
277 /// MediaWikiの名前空間の情報。
280 /// サーバーから情報を取得。大文字小文字を区別しない。
282 public IDictionary<int, IgnoreCaseSet> Namespaces
286 // 値が設定されていない場合、サーバーから取得して初期化する
287 // ※ コンストラクタ等で初期化していないのは、通信の準備が整うまで行えないため
288 // ※ 余計なロック・通信をしないよう、ロックの前後に値のチェックを行う
289 if (this.namespaces != null)
291 return this.namespaces;
294 lock (this.lockLoadMetaApi)
296 if (this.namespaces != null)
298 return this.namespaces;
301 this.InitializeByMetaApi();
304 return this.namespaces;
309 this.namespaces = value;
314 /// MediaWikiのウィキ間リンクのプレフィックス情報。
317 /// 値が設定されていない場合デフォルト値とサーバーから、
318 /// 設定されている場合その内容とサーバーから取得した情報を使用する。
321 public IgnoreCaseSet InterwikiPrefixs
325 // 値が準備されていない場合、サーバーと設定ファイルから取得して初期化する
326 // ※ コンストラクタ等で初期化していないのは、通信の準備が整うまで行えないため
327 // ※ 余計なロック・通信をしないよう、ロックの前後に値のチェックを行う
328 if (this.interwikiPrefixCaches != null)
330 return this.interwikiPrefixCaches;
333 lock (this.lockLoadMetaApi)
335 if (this.interwikiPrefixCaches != null)
337 return this.interwikiPrefixCaches;
340 this.InitializeByMetaApi();
343 return this.interwikiPrefixCaches;
349 this.interwikiPrefixs = value;
350 this.interwikiPrefixCaches = null;
359 /// Template:仮リンク(他言語へのリンク)で書式化するためのフォーマット。
361 /// <remarks>空の場合、その言語版にはこれに相当する機能は無いor使用しないものとして扱う。</remarks>
362 public string LinkInterwikiFormat
369 /// Template:Langで書式化するためのフォーマット。
371 /// <remarks>空の場合、その言語版にはこれに相当する機能は無いor使用しないものとして扱う。</remarks>
372 public string LangFormat
379 /// 言語名の記事が存在するようなサイトか?
382 /// そうした記事が存在する場合<c>true</c>。
383 /// Wikipediaには[[日本語]]といった記事が存在するが、Wikitravelであればそうした記事は存在し得ない。
384 /// 言語名をリンクにするかといった判断に用いる。
386 public bool HasLanguagePage
399 /// <param name="title">ページタイトル。</param>
400 /// <returns>取得したページ。</returns>
401 /// <exception cref="InvalidDataException">APIから取得したデータが想定外。</exception>
402 /// <exception cref="NullReferenceException">APIから取得したデータが想定外。</exception>
403 /// <exception cref="FileNotFoundException">ページが存在しない場合。</exception>
405 /// ページの取得に失敗した場合(通信エラーなど)は、その状況に応じた例外を投げる。
406 /// このメソッドでは記事本文や日時といった情報は取得しない。
407 /// そうした情報は、実際にアクセスされたタイミングで動的に取得する。
409 public override Page GetPage(string title)
411 // fileスキームの場合、記事名からファイルに使えない文字をエスケープ
412 // ※ 仕組み的な処理はWebsite側に置きたいが、向こうではタイトルだけを抽出できないので
413 string escapeTitle = title;
414 if (new Uri(this.Location).IsFile)
416 escapeTitle = FormUtils.ReplaceInvalidFileNameChars(title);
420 Uri uri = new Uri(new Uri(this.Location), StringUtils.FormatDollarVariable(this.InterlanguageApi, escapeTitle));
422 // ページの言語間リンク情報XMLデータをMediaWikiサーバーから取得
424 using (Stream reader = this.WebProxy.GetStream(uri))
426 doc = XElement.Load(reader);
434 qe = (from n in doc.Elements("query")
437 catch (InvalidOperationException)
439 throw new InvalidOperationException("parse failed : api/query element is not found");
442 // クエリーからページ情報を読み込み返す
443 // ※ ページが無い場合などは、例外が投げられる
444 return MediaWikiPage.GetFromQuery(this, uri, qe);
450 /// <param name="title">ページタイトル。</param>
451 /// <returns>取得したページ。</returns>
452 /// <exception cref="InvalidDataException">APIから取得したデータが想定外。</exception>
453 /// <exception cref="NullReferenceException">APIから取得したデータが想定外。</exception>
454 /// <exception cref="FileNotFoundException">ページが存在しない場合。</exception>
455 /// <remarks>ページの取得に失敗した場合(通信エラーなど)は、その状況に応じた例外を投げる。</remarks>
456 public Page GetPageBodyAndTimestamp(string title)
458 // fileスキームの場合、記事名からファイルに使えない文字をエスケープ
459 // ※ 仕組み的な処理はWebsite側に置きたいが、向こうではタイトルだけを抽出できないので
460 string escapeTitle = title;
461 if (new Uri(this.Location).IsFile)
463 escapeTitle = FormUtils.ReplaceInvalidFileNameChars(title);
467 Uri uri = new Uri(new Uri(this.Location), StringUtils.FormatDollarVariable(this.ContentApi, escapeTitle));
469 // ページのXMLデータをMediaWikiサーバーから取得
471 using (Stream reader = this.WebProxy.GetStream(uri))
473 doc = XElement.Load(reader);
477 // ※ この問い合わせでは、ページが無い場合も要素自体は毎回ある模様
478 // 一件しか返らないはずなので先頭データを対象とする
482 pe = (from query in doc.Elements("query")
483 from pages in query.Elements("pages")
484 from n in pages.Elements("page")
487 catch (InvalidOperationException)
489 throw new InvalidOperationException("parse failed : query/pages/page element is not found");
493 if (pe.Attribute("missing") != null)
495 // missing属性が存在する場合、ページ無し
496 throw new FileNotFoundException("page not found");
500 var re = (from revisions in pe.Elements("revisions")
501 from n in revisions.Elements("rev")
505 return new MediaWikiPage(
507 XmlUtils.Value(pe.Attribute("title"), title),
509 new DateTime?(DateTime.Parse(re.Attribute("timestamp").Value)),
514 /// 指定された文字列がMediaWikiのシステム変数に相当かを判定。
516 /// <param name="text">チェックする文字列。</param>
517 /// <returns>システム変数に相当する場合<c>true</c>。</returns>
518 /// <remarks>大文字小文字は区別する。</remarks>
519 public bool IsMagicWord(string text)
521 // {{CURRENTYEAR}}や{{ns:1}}みたいなパターンがある
522 string s = StringUtils.DefaultString(text);
523 foreach (string variable in this.MagicWords)
525 if (s == variable || s.StartsWith(variable + ":"))
535 /// 指定されたリンク文字列がMediaWikiのウィキ間リンクかを判定。
537 /// <param name="link">チェックするリンク文字列。</param>
538 /// <returns>ウィキ間リンクに該当する場合<c>true</c>。</returns>
539 /// <remarks>大文字小文字は区別しない。</remarks>
540 public bool IsInterwiki(string link)
542 // ※ ウィキ間リンクには入れ子もあるが、ここでは意識する必要はない
543 string s = StringUtils.DefaultString(link);
545 // 名前空間と被る場合はそちらが優先、ウィキ間リンクと判定しない
546 if (this.IsNamespace(link))
551 // 文字列がいずれかのウィキ間リンクのプレフィックスで始まるか
552 int index = s.IndexOf(':');
558 return this.InterwikiPrefixs.Contains(s.Remove(index));
562 /// 指定されたリンク文字列がMediaWikiのいずれかの名前空間に属すかを判定。
564 /// <param name="link">チェックするリンク文字列。</param>
565 /// <returns>いずれかの名前空間に該当する場合<c>true</c>。</returns>
566 /// <remarks>大文字小文字は区別しない。</remarks>
567 public bool IsNamespace(string link)
569 // 文字列がいずれかの名前空間のプレフィックスで始まるか
570 string s = StringUtils.DefaultString(link);
571 int index = s.IndexOf(':');
577 string prefix = s.Remove(index);
578 foreach (IgnoreCaseSet prefixes in this.Namespaces.Values)
580 if (prefixes.Contains(prefix))
590 /// <see cref="LinkInterwikiFormat"/> を渡された記事名, 言語, 他言語版記事名, 表示名で書式化した文字列を返す。
592 /// <param name="title">記事名。</param>
593 /// <param name="lang">言語。</param>
594 /// <param name="langTitle">他言語版記事名。</param>
595 /// <param name="label">表示名。</param>
596 /// <returns>書式化した文字列。<see cref="LinkInterwikiFormat"/>が未設定の場合<c>null</c>。</returns>
597 public string FormatLinkInterwiki(string title, string lang, string langTitle, string label)
599 if (string.IsNullOrEmpty(this.LinkInterwikiFormat))
604 return StringUtils.FormatDollarVariable(this.LinkInterwikiFormat, title, lang, langTitle, label);
608 /// <see cref="LangFormat"/> を渡された言語, 文字列で書式化した文字列を返す。
610 /// <param name="lang">言語。</param>
611 /// <param name="text">文字列。</param>
612 /// <returns>書式化した文字列。<see cref="LangFormat"/>が未設定の場合<c>null</c>。</returns>
614 /// この<paramref name="lang"/>と<see cref="Language"/>のコードは、厳密には一致しないケースがあるが
615 /// (例、simple→en)、2012年2月現在の実装ではそこまで正確さは要求していない。
617 public string FormatLang(string lang, string text)
619 if (string.IsNullOrEmpty(this.LangFormat))
624 return StringUtils.FormatDollarVariable(this.LangFormat, lang, text);
629 #region XMLシリアライズ用メソッド
632 /// シリアライズするXMLのスキーマ定義を返す。
634 /// <returns>XML表現を記述する<see cref="System.Xml.Schema.XmlSchema"/>。</returns>
635 public System.Xml.Schema.XmlSchema GetSchema()
641 /// XMLからオブジェクトをデシリアライズする。
643 /// <param name="reader">デシリアライズ元の<see cref="XmlReader"/></param>
644 public void ReadXml(XmlReader reader)
646 XmlDocument xml = new XmlDocument();
650 // ※ 以下、基本的に無かったらNGの部分はいちいちチェックしない。例外飛ばす
651 XmlElement siteElement = xml.DocumentElement;
652 this.Location = siteElement.SelectSingleNode("Location").InnerText;
654 using (XmlReader r = XmlReader.Create(
655 new StringReader(siteElement.SelectSingleNode("Language").OuterXml), reader.Settings))
657 this.Language = new XmlSerializer(typeof(Language)).Deserialize(r) as Language;
660 this.MetaApi = XmlUtils.InnerText(siteElement.SelectSingleNode("MetaApi"));
661 this.ContentApi = XmlUtils.InnerText(siteElement.SelectSingleNode("ContentApi"));
662 this.InterlanguageApi = XmlUtils.InnerText(siteElement.SelectSingleNode("InterlanguageApi"));
665 if (int.TryParse(XmlUtils.InnerText(siteElement.SelectSingleNode("TemplateNamespace")), out namespaceId))
667 this.TemplateNamespace = namespaceId;
670 if (int.TryParse(XmlUtils.InnerText(siteElement.SelectSingleNode("CategoryNamespace")), out namespaceId))
672 this.CategoryNamespace = namespaceId;
675 if (int.TryParse(XmlUtils.InnerText(siteElement.SelectSingleNode("FileNamespace")), out namespaceId))
677 this.FileNamespace = namespaceId;
681 ISet<string> variables = new HashSet<string>();
682 foreach (XmlNode variableNode in siteElement.SelectNodes("MagicWords/Variable"))
684 variables.Add(variableNode.InnerText);
687 if (variables.Count > 0)
690 this.MagicWords = variables;
694 IgnoreCaseSet prefixs = new IgnoreCaseSet();
695 foreach (XmlNode prefixNode in siteElement.SelectNodes("InterwikiPrefixs/Prefix"))
697 prefixs.Add(prefixNode.InnerText);
700 if (prefixs.Count > 0)
703 this.InterwikiPrefixs = prefixs;
706 this.LinkInterwikiFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LinkInterwikiFormat"));
707 this.LangFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LangFormat"));
708 bool hasLanguagePage;
709 if (bool.TryParse(XmlUtils.InnerText(siteElement.SelectSingleNode("HasLanguagePage")), out hasLanguagePage))
711 this.HasLanguagePage = hasLanguagePage;
716 /// オブジェクトをXMLにシリアライズする。
718 /// <param name="writer">シリアライズ先の<see cref="XmlWriter"/></param>
719 public void WriteXml(XmlWriter writer)
721 writer.WriteElementString("Location", this.Location);
722 new XmlSerializer(this.Language.GetType()).Serialize(writer, this.Language);
725 // ※ 設定ファイルに初期値を持つものは、プロパティではなく値から出力
726 writer.WriteElementString("MetaApi", this.metaApi);
727 writer.WriteElementString("ContentApi", this.contentApi);
728 writer.WriteElementString("InterlanguageApi", this.interlanguageApi);
729 writer.WriteElementString(
731 this.templateNamespace.HasValue ? this.templateNamespace.ToString() : string.Empty);
732 writer.WriteElementString(
734 this.templateNamespace.HasValue ? this.categoryNamespace.ToString() : string.Empty);
735 writer.WriteElementString(
737 this.templateNamespace.HasValue ? this.fileNamespace.ToString() : string.Empty);
740 writer.WriteStartElement("MagicWords");
741 if (this.magicWords != null)
743 foreach (string variable in this.magicWords)
745 writer.WriteElementString("Variable", variable);
750 writer.WriteEndElement();
751 writer.WriteStartElement("InterwikiPrefixs");
752 if (this.interwikiPrefixs != null)
754 foreach (string prefix in this.interwikiPrefixs)
756 writer.WriteElementString("Prefix", prefix);
760 writer.WriteEndElement();
761 writer.WriteElementString("LinkInterwikiFormat", this.LinkInterwikiFormat);
762 writer.WriteElementString("LangFormat", this.LangFormat);
763 writer.WriteElementString("HasLanguagePage", this.HasLanguagePage.ToString());
771 /// <see cref="MetaApi"/>を使用してサーバーからメタ情報を取得する。
773 /// <exception cref="System.Net.WebException">通信エラー等が発生した場合。</exception>
774 /// <exception cref="InvalidDataException">APIから取得した情報が想定外のフォーマットの場合。</exception>
775 private void InitializeByMetaApi()
777 // APIのXMLデータをMediaWikiサーバーから取得
778 XmlDocument xml = new XmlDocument();
779 using (Stream reader = this.WebProxy.GetStream(new Uri(new Uri(this.Location), this.MetaApi)))
784 // ルートエレメントまで取得し、フォーマットをチェック
785 XmlElement rootElement = xml["api"];
786 if (rootElement == null)
788 // XMLは取得できたが空 or フォーマットが想定外
789 throw new InvalidDataException("parse failed : api element is not found");
793 XmlElement queryElement = rootElement["query"];
794 if (queryElement == null)
797 throw new InvalidDataException("parse failed : query element is not found");
800 // クエリー内のネームスペース・ネームスペースエイリアス、ウィキ間リンクを読み込み
801 this.namespaces = this.LoadNamespacesElement(queryElement);
802 this.interwikiPrefixCaches = this.LoadInterwikimapElement(queryElement);
804 // ウィキ間リンクは読み込んだ後に設定ファイルorプロパティの分をマージ
805 // ※ 設定ファイルの初期値は下記より作成。
806 // http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/maintenance/interwiki.sql?view=markup
807 // APIに加えて設定ファイルも持っているのは、2012年2月現在APIから返ってこない
808 // 項目(wikipediaとかcommonsとか)が存在するため。
809 this.interwikiPrefixCaches.UnionWith(
810 this.interwikiPrefixs == null
811 ? Settings.Default.MediaWikiInterwikiPrefixs.Cast<string>()
812 : this.interwikiPrefixs);
816 /// <see cref="MetaApi"/>から取得したXMLのうち、ネームスペースに関する部分を読み込む。
818 /// <param name="queryElement">APIから取得したXML要素のうち、api→query部分のエレメント。</param>
819 /// <returns>読み込んだネームスペース情報。</returns>
820 /// <exception cref="InvalidDataException">namespacesエレメントが存在しない場合。</exception>
821 private IDictionary<int, IgnoreCaseSet> LoadNamespacesElement(XmlElement queryElement)
823 // ネームスペースブロックを取得、ネームスペースブロックまでは必須
824 XmlElement namespacesElement = queryElement["namespaces"];
825 if (namespacesElement == null)
828 throw new InvalidDataException("parse failed : namespaces element is not found");
832 IDictionary<int, IgnoreCaseSet> namespaces = new Dictionary<int, IgnoreCaseSet>();
833 foreach (XmlNode node in namespacesElement.ChildNodes)
835 XmlElement namespaceElement = node as XmlElement;
836 if (namespaceElement != null)
840 int id = decimal.ToInt16(decimal.Parse(namespaceElement.GetAttribute("id")));
841 IgnoreCaseSet values = new IgnoreCaseSet();
842 values.Add(namespaceElement.InnerText);
843 namespaces[id] = values;
846 string canonical = namespaceElement.GetAttribute("canonical");
847 if (!string.IsNullOrEmpty(canonical))
849 values.Add(canonical);
854 // キャッチしているのは、万が一想定外の書式が返された場合に、完璧に動かなくなるのを防ぐため
855 System.Diagnostics.Debug.WriteLine("MediaWiki.LoadNamespacesElement > 例外発生 : " + e);
860 // ネームスペースエイリアスブロックを取得、無い場合も想定
861 XmlElement aliasesElement = queryElement["namespacealiases"];
862 if (aliasesElement != null)
865 foreach (XmlNode node in aliasesElement.ChildNodes)
867 XmlElement namespaceElement = node as XmlElement;
868 if (namespaceElement != null)
872 int id = decimal.ToInt16(decimal.Parse(namespaceElement.GetAttribute("id")));
873 ISet<string> values = new HashSet<string>();
874 if (namespaces.ContainsKey(id))
876 values = namespaces[id];
879 values.Add(namespaceElement.InnerText);
883 // キャッチしているのは、万が一想定外の書式が返された場合に、完璧に動かなくなるのを防ぐため
884 System.Diagnostics.Debug.WriteLine("MediaWiki.LoadNamespacesElement > 例外発生 : " + e);
894 /// <see cref="MetaApi"/>から取得したXMLのうち、ウィキ間リンクに関する部分を読み込む。
896 /// <param name="queryElement">APIから取得したXML要素のうち、api→query部分のエレメント。</param>
897 /// <returns>読み込んだウィキ間リンク情報。</returns>
898 /// <exception cref="InvalidDataException">interwikimapエレメントが存在しない場合。</exception>
899 private IgnoreCaseSet LoadInterwikimapElement(XmlElement queryElement)
901 // ウィキ間リンクブロックを取得、ウィキ間リンクブロックまでは必須
902 XmlElement interwikimapElement = queryElement["interwikimap"];
903 if (interwikimapElement == null)
906 throw new InvalidDataException("parse failed : interwikimap element is not found");
910 IgnoreCaseSet interwikiPrefixs = new IgnoreCaseSet();
911 foreach (XmlNode node in interwikimapElement.ChildNodes)
913 XmlElement interwikiElement = node as XmlElement;
914 if (interwikiElement != null)
916 string prefix = interwikiElement.GetAttribute("prefix");
917 if (!string.IsNullOrWhiteSpace(prefix))
919 interwikiPrefixs.Add(prefix);
924 return interwikiPrefixs;