/// <summary> /// 文字列から <see cref="HtmlContentCollection"/> クラスの新しいインスタンスを初期化します。 /// </summary> /// <param name="text"> /// ソースの文字列 /// </param> public HtmlContentCollection(string text) : this() { // NEW input text var input = new StringBuilder(text); // for End Tag string endTag = null; // NEW Contents var contents = HtmlContentCollection.Parse(ref input, null, ref endTag); // Validation(s) if (input.Length != 0) { // ERROR throw new FormatException(Resources.HtmlElementInvalidFormatErrorMessage); } // ADD Content(s) this.AddRange(contents); }
/// <summary> /// <inheritdoc cref="HtmlElement.HtmlElement(string)"/> /// </summary> /// <param name="startTag"> /// このインスタンスの開始タグを指定します。 /// <note type="implement"> /// 省略する場合は、空文字 (<c>""</c>, <see cref="string.Empty"/>) ではなく <c>null</c> を指定します。 /// </note> /// </param> /// <param name="contents"> /// このインスタンスが内包するコンテンツ (<see cref="HtmlContentCollection"/>) を指定します。 /// <note type="important"> /// 開始タグが <c>/></c> で終了している場合のみ <c>null</c> を指定できます。 /// その他の場合で、コンテンツがない場合は、長さ <c>0</c> の <see cref="HtmlContentCollection"/> を指定してください。 /// </note> /// </param> /// <param name="endTag"> /// このインスタンスの終了タグを指定します。 /// <note type="implement"> /// 省略する場合は、空文字 (<c>""</c>, <see cref="string.Empty"/>) ではなく <c>null</c> を指定します。 /// </note> /// </param> public HtmlElement(string startTag, HtmlContentCollection contents, string endTag) { #if DEBUG Debug.WriteLine("[START]", DebugInfo.ShortName); #endif // Validation (Null Check) if (string.IsNullOrWhiteSpace(startTag) && string.IsNullOrWhiteSpace(endTag)) { throw new ArgumentNullException(nameof(startTag)); } // Validation (Empty Check) if (startTag != null && startTag.Length == 0) { throw new ArgumentException(Resources.InvalidArgumentErrorMessage, nameof(startTag)); } if (endTag != null && endTag.Length == 0) { throw new ArgumentException(Resources.InvalidArgumentErrorMessage, nameof(endTag)); } // Validation (Self-closing Start Tag Check) if (Regex.IsMatch(startTag, "/>$", RegexOptions.Singleline)) { // Self-closing: if (!string.IsNullOrWhiteSpace(endTag)) { throw new ArgumentException(Resources.InvalidArgumentErrorMessage, nameof(endTag)); } else if (contents != null) { throw new ArgumentException(Resources.InvalidArgumentErrorMessage, nameof(contents)); } } else { // NOT Self-closing: if (contents is null) { throw new ArgumentNullException(nameof(contents)); } } // Validation (Tag Name in Start Tag and End Tag) if (!string.IsNullOrEmpty(startTag) && !string.IsNullOrEmpty(endTag) && string.Compare(HtmlElement.GetTagName(startTag), HtmlElement.GetTagName(endTag), StringComparison.OrdinalIgnoreCase) != 0) { // ERROR throw new FormatException(Resources.HtmlElementInvalidFormatErrorMessage); } // SET value(s) this.StartTag = startTag; this.EndTag = endTag; this.TagName = !string.IsNullOrEmpty(startTag) ? HtmlElement.GetTagName(startTag) : HtmlElement.GetTagName(endTag); this.Attributes = HtmlElement.GetAttributes(startTag); this.Contents = contents; #if DEBUG Debug.WriteLine($"Start Tag = " + (this.StartTag is null ? "(null)" : $"\"{this.StartTag}\""), DebugInfo.ShortName); Debug.WriteLine($"Contents.Text = " + (this.Contents is null || this.Contents.Text is null ? "(null)" : $"\"{this.Contents.Text}\""), DebugInfo.ShortName); Debug.WriteLine($"End Tag = " + (this.EndTag is null ? "(null)" : $"\"{this.EndTag}\""), DebugInfo.ShortName); Debug.WriteLine("[END]", DebugInfo.ShortName); #endif }
// ---------------------------------------------------------------------------------------------------- // Static Method(s) // ---------------------------------------------------------------------------------------------------- /// <summary> /// 入力文字列を HTML として構文解析します。 /// </summary> /// <param name="input"> /// 入力文字列を指定します。<br/> /// また、入力文字列が最後まで構文解析されなかった場合は、このメソッドからの復帰時に、構文解析されていない文字列が格納されます。 /// 入力文字列が最後まで構文解析された場合は <c>null</c> が設定されます。 /// </param> /// <param name="startTag"> /// 入力文字列よりも前に、対応する終了タグが出現していない開始タグ (閉じていない開始タグ) がある場合に、その開始タグの文字列を設定します。 /// 通常は <c>null</c> を設定してください。 /// </param> /// <param name="endTag"> /// </param> /// <returns> /// 入力文字列を HTML として構文解析した結果を返します。 /// <note type="important"> /// このメソッドからの復帰時に、<paramref name="input"/> に <c>null</c> ではない文字列が格納されている場合は、入力文字列は最後まで構文解析されていません。 /// その場合の構文解析されていない文字列は <paramref name="input"/> に格納されています。 /// </note> /// </returns> /// <remarks> /// <note type="important"> /// 対応する終了タグのない HTML 要素は、その種類にかかわらず、終了タグが省略されたものとして解釈します。 /// </note> /// </remarks> private static HtmlContentCollection Parse(ref StringBuilder input, string startTag, ref string endTag) { #if DEBUG Debug.WriteLine("[START]", DebugInfo.ShortName); #endif // Validation (Null Check) if (input == null) { throw new ArgumentNullException(nameof(input)); } // Empty Validation is NOT reuqired because Empty input is allowed. // if (input.Length == 0) { throw new ArgumentException(Resources.InvalidArgumentErrorMessage, nameof(input)); } // NEW parsing Content(s) var contents = new HtmlContentCollection(); // NEW RawTexts for parsing Content(s) var raw_texts = new StringBuilder(); // for RegEx Match match; // Local Function: // Update parsing Content(s) void updateContents(string contentText, ref StringBuilder inputText, HtmlContentType type) { // Check WhiteSpace if (Regex.IsMatch(contentText, @"^[\t\r\n ]+$", RegexOption)) { // ADD WhiteSpace (NON-HTML) contents.Add(new HtmlWhiteSpace(contentText)); } else { // ADD Content (if NOT Empty) switch (type) { case HtmlContentType.HtmlElement: // ADD Content (HTML) contents.Add(new HtmlElement(contentText)); break; case HtmlContentType.HtmlText: // ADD Content (NON-HTML) contents.Add(new HtmlText(contentText)); break; case HtmlContentType.HtmlComment: // ADD Comment (NON-HTML) contents.Add(new HtmlComment(contentText)); break; default: break; } } // UPDATE RawTexts for parsing Content(s) raw_texts.Append(contentText); // REMOVE content text (= Heading Tag) from current text inputText.Remove(0, contentText.Length); } // MAIN LOOP while (input.Length > 0) { // Check Comment if ((match = Regex.Match(input.ToString(), "^<!--.*?-->", RegexOption)).Success) { // Validation if (Regex.IsMatch(match.Value, "^<!--((>|->).*|.*(<!--|--!>).*|.*<!-)-->$", RegexOption)) { // ERROR throw new FormatException(Resources.HtmlElementInvalidFormatErrorMessage); } // UPDATE rawText and inputText (parsing Content(s) is NOT updated.) updateContents(match.Value, ref input, HtmlContentType.HtmlComment); #if DEBUG Debug.WriteLine($"COMMENT: \"{match.Value}\" is removed.", DebugInfo.ShortName); #endif } // Check !DOCTYPE if ((match = Regex.Match(input.ToString(), @"^<!DOCTYPE[\t\r\n ]+html([\t\r\n ]+(PUBLIC|SYSTEM)[\t\r\n ]*[^<>]*)?[\t\r\n ]*>", RegexOption)).Success) { // !DOCTYPE is found; // UPDATE parsing Content(s) (HTML) updateContents(match.Value, ref input, HtmlContentType.HtmlElement); } // Check Heading Tag if (!(match = Regex.Match(input.ToString(), "^<[!/]?[^<>]+>", RegexOption)).Success) { // Any Heading Tag does NOT exist; // GET text before any tag if ((match = Regex.Match(input.ToString(), "^[^<>]*<", RegexOption)).Success) { // There are some text before "<" // UPDATE parsing Content(s) (NON-HTML) updateContents(match.Value.Substring(0, match.Value.Length - 1), ref input, HtmlContentType.HtmlText); } else if (Regex.IsMatch(input.ToString(), "^[^<>]*$", RegexOption)) { // There are NO any tags. // UPDATE parsing Content(s) (NON-HTML) updateContents(input.ToString(), ref input, HtmlContentType.HtmlText); } else { // ERROR throw new FormatException(Resources.HtmlElementInvalidFormatErrorMessage); } } else { // Some Heading Tag exists; // GET Heading Tag var heading_tag = match.Value; // GET Tag Name (from Heading Tag) var tag_name = string.IsNullOrEmpty(heading_tag) ? null : HtmlElement.GetTagName(heading_tag); // SET RegEx Pattern in Start Tag & End Tag var pattern_in_startTag = $"!?{tag_name}+([\t\r\n ]+[0-9A-Za-z:.-]+(=[\"']?[^\"'=<>]*[\"']?)*)*[\t\r\n ]*"; var pattern_in_endTag = $"{tag_name}[\t\r\n ]*"; // Check Heading Tag if (Regex.IsMatch(heading_tag, $"^<{pattern_in_startTag}/>$", RegexOption)) { // Heading Tag is Self-Closing Start Tag; // UPDATE parsing Content(s) (HTML) updateContents(heading_tag, ref input, HtmlContentType.HtmlElement); } else if (Regex.IsMatch(heading_tag, $"^<{pattern_in_startTag}>$", RegexOption)) { // Heading Tag is Start Tag, NOT Self-Closing; // Check text following Heading Tag if ((match = Regex.Match(input.ToString(), $"^<{pattern_in_startTag}>[^<>]*?</{pattern_in_endTag}>", RegexOption)).Success) { // Appropriate End Tag is neighbored; // UPDATE parsing Content(s) (HTML) updateContents(match.Value, ref input, HtmlContentType.HtmlElement); } else { // Appropriate End Tag is NOT neighbored; // DO NOT ADD Content // DO NOT UPDATE RawText for parsing Content(s) // REMOVE content text (= Heading Tag) from current text input.Remove(0, heading_tag.Length); // ************************************************** // RECURSE: ADD Content(s); to be Child or Siblings // ************************************************** var subsequents = HtmlContentCollection.Parse(ref input, heading_tag, ref endTag); // ADD subsequent Content(s) to parsing Content(s) subsequents.ForEach(subsequent => contents.Add(subsequent)); // UPDATE RawTexts for parsing Content(s) raw_texts.Append(subsequents.RawText); } } else if ((match = Regex.Match(input.ToString(), $"^</{pattern_in_endTag}>", RegexOption)).Success) { // Heading Tag is End Tag; // GET End Tag endTag = match.Value; // REMOVE content text (= End Tag) from current text input.Remove(0, endTag.Length); } else { // ERROR throw new FormatException(Resources.HtmlElementInvalidFormatErrorMessage); } } // Check End Tag if (!string.IsNullOrEmpty(endTag)) { // Check Tag Name if (string.Compare(HtmlElement.GetTagName(startTag), HtmlElement.GetTagName(endTag), StringComparison.OrdinalIgnoreCase) == 0) { // Tag Name matches; // NEW Child Content var child = new HtmlElement(startTag, contents, endTag); // RESET End Tag endTag = null; // ******** // RETURN // ******** return(new HtmlContentCollection() { child }); } else { // Tag Name Does NOT match; // UPDATE RawTexts for parsing Content(s) raw_texts.Insert(0, startTag); // UPDATE or INSERT 1st Content to parsing Content(s) var first_element_index = contents.FindIndex(content => content.Type == HtmlContentType.HtmlElement); var first_text_index = contents.FindIndex(content => content.Type == HtmlContentType.HtmlText); if ((contents.Count < 1) || (first_element_index > -1) && (first_element_index < first_text_index)) { // INSERT Content (HTML) contents.Insert(0, new HtmlElement(startTag)); } else { // UPDATE Content from NON-HTML to HTML contents[0] = new HtmlElement(startTag + contents[0].RawText); } // ******** // RETURN // ******** return(contents); } } } // MAIN LOOP // Validation if (!string.IsNullOrEmpty(startTag) || !string.IsNullOrEmpty(endTag)) { // ERROR throw new FormatException(Resources.HtmlElementInvalidFormatErrorMessage); } #if DEBUG Debug.WriteLine("[END]", DebugInfo.ShortName); #endif // RETURN return(contents); }