Ejemplo n.º 1
0
        /// <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
        }
Ejemplo n.º 3
0
        // ----------------------------------------------------------------------------------------------------
        // 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);
        }