001package Torello.HTML;
002
003import java.util.*;
004import java.util.regex.*;
005import java.util.stream.*;
006import java.util.function.*;
007
008import Torello.Java.StringParse;
009import Torello.Java.StrCmpr;
010import Torello.Java.StrFilter;
011import Torello.Java.Additional.EffectivelyFinal;
012
013
014import Torello.HTML.parse.HTMLRegEx;
015import Torello.HTML.NodeSearch.CSSStrException;
016import Torello.HTML.NodeSearch.TextComparitor;
017
018import Torello.Java.Shell.C;
019
020/**
021 * Represents an HTML Element Tag, and is the flagship class of the Java-HTML Library.
022 * 
023 * <EMBED CLASS="external-html" DATA-FILE-ID=TAG_NODE>
024 * 
025 * <EMBED CLASS="external-html" DATA-FILE-ID=HTML_NODE_SUB_IMG>
026 * 
027 * @see TextNode
028 * @see CommentNode
029 * @see HTMLNode
030 */
031@Torello.HTML.Tools.JavaDoc.JDHeaderBackgroundImg(EmbedTagFileID="HTML_NODE_SUBCLASS")
032public final class TagNode 
033    extends HTMLNode 
034    implements CharSequence, java.io.Serializable, Cloneable, Comparable<TagNode>
035{
036    /** <EMBED CLASS="external-html" DATA-FILE-ID="SVUID"> */
037    public static final long serialVersionUID = 1;
038
039
040    // ********************************************************************************************
041    // ********************************************************************************************
042    // NON-STATIC FIELDS
043    // ********************************************************************************************
044    // ********************************************************************************************
045
046
047    /** <EMBED CLASS="external-html" DATA-FILE-ID=TAGNODE_TOK> */
048    public final String tok;
049
050    /** <EMBED CLASS="external-html" DATA-FILE-ID=TAGNODE_IS_CLOSING> */
051    public final boolean isClosing;
052
053
054    // ********************************************************************************************
055    // ********************************************************************************************
056    // Constructors
057    // ********************************************************************************************
058    // ********************************************************************************************
059
060
061    /**
062     * <EMBED CLASS='external-html' DATA-FILE-ID=TN_C_DESC_1>
063     * 
064     * @param s Any valid HTML tag, for instance: {@code <H1>, <A HREF="somoe url">,
065     * <DIV ID="some id">} etc...
066     * 
067     * @throws MalformedTagNodeException If the passed {@code String} wasn't valid - meaning <I>it
068     * did not match the regular-expression {@code parser}.</I> 
069     * 
070     * @throws HTMLTokException If the {@code String} found where the usual HTML token-element is
071     * situated <I>is not a valid HTML element</I> then the {@code HTMLTokException} will be
072     * thrown.
073     * 
074     * @see HTMLTags#getTag_MEM_HEAP_CHECKOUT_COPY(String)
075     */
076    public TagNode(String s)
077    {
078        super(s);
079
080        // If the second character of the string is a forward-slash, this must be a closing-element
081        // For Example: </SPAN>, </DIV>, </A>, etc...
082
083        isClosing = s.charAt(1) == '/';
084
085        // This is the Element & Attribute Matcher used by the RegEx Parser.  If this Matcher
086        // doesn't find a match, the parameter 's' cannot be a valid HTML Element.  NOTE: The
087        // results of this matcher are also used to retrieve attribute-values, but here below,
088        // its results are ignored.
089
090        Matcher m = HTMLRegEx.P1.matcher(s);
091
092        if (! m.find()) throw new MalformedTagNodeException(
093            "The parser's regular-expression did not match the constructor-string.\n" +
094            "The exact input-string was: [" + s + "]\n" +
095            "NOTE:  The parameter-string is included as a field (ex.str) to this Exception.", s
096        );
097
098        // MINOR/MAJOR IMPROVEMENT... REUSE THE "ALLOCATED STRING TOKEN" from HTMLTag's class
099        // THINK: Let the Garbage Collector take out as many duplicate-strings as is possible..
100        // AND SOONER.  DECEMBER 2019: "Optimization" or ... "Improvement"
101
102        String tokTEMP = m.group(1).toLowerCase();
103
104        if ((m.start() != 0) || (m.end() != s.length()))
105
106            throw new MalformedTagNodeException(
107                "The parser's regular-expression did not match the entire-string-length of the " +
108                "string-parameter to this constructor: m.start()=" + m.start() + ", m.end()=" + 
109                m.end() + ".\nHowever, the length of the Input-Parameter String was " +
110                '[' + s.length() + "]\nThe exact input-string was: [" + s + "]\nNOTE: The " +
111                "parameter-string is included as a field (ex.str) to this Exception.", s
112            );
113
114
115        // Get a copy of the 'tok' string that was already allocated on the heap; (OPTIMIZATON)
116        // NOTE: There are already myriad strings for the '.str' field.
117        // ALSO: Don't pay much attention to this line if it doesn't make sense... it's not
118        //       that important.  If the HTML Token found was not a valid HTML5 token, this field
119        //       will be null.
120        // Java 14+ has String.intern() - that's what this is....
121
122        this.tok = HTMLTags.getTag_MEM_HEAP_CHECKOUT_COPY(tokTEMP);
123
124        // Now do the usual error check.
125        if (this.tok == null) throw new HTMLTokException(
126            "The HTML Tag / Token Element that is specified by the input string " +
127            "[" + tokTEMP + "] is not a valid HTML Element Name.\n" +
128            "The exact input-string was: [" + s + "]"
129        );
130    }
131
132    // USED-INTERNALLY - bypasses all checks.  used when creating new HTML Element-Names
133    // ONLY: class 'HTMLTags' via method 'addTag(...)' shall ever invoke this constructor.
134    // NOTE: This only became necessary because of the MEM_COPY_HEAP optimization.  This
135    //       optimization expects that there is already a TagNode with element 'tok' in
136    //       the TreeSet, which is always OK - except for the method that CREATES NEW HTML
137    //       TAGS... a.k.a. HTMLTags.addTag(String).
138    TagNode(String token, TC openOrClosed)
139    {
140        super("<" + ((openOrClosed == TC.ClosingTags) ? "/" : "") + token + ">");
141
142        // ONLY CHANGE CASE HERE, NOT IN PREVIOUS-LINE.  PAY ATTENTION.  
143        this.tok = token.toLowerCase();
144
145        this.isClosing = (openOrClosed == TC.ClosingTags) ? true : false;
146    }
147
148    /**
149     * Convenience Constructor.
150     * <BR />Invokes: {@link #TagNode(String, Properties, Iterable, SD, boolean)}
151     * <BR />Passes: null to the Boolean / Key-Only Attributes {@code Iterable}
152     */
153    public TagNode(String tok, Properties attributes, SD quotes, boolean addEndingForwardSlash) 
154    { this(tok, attributes, null /* keyOnlyAttributes */, quotes, addEndingForwardSlash); }
155
156    /**
157     * <EMBED CLASS='external-html' DATA-FILE-ID=TN_C_DESC_2>
158     *
159     * @param tok <EMBED CLASS='external-html' DATA-FILE-ID=TN_C_TOK>
160     * @param attributes <EMBED CLASS='external-html' DATA-FILE-ID=TN_C_ATTRIBUTES>
161     * @param keyOnlyAttributes <EMBED CLASS='external-html' DATA-FILE-ID=TN_C_KO_ATTRIBUTES>
162     * @param quotes <EMBED CLASS='external-html' DATA-FILE-ID=TN_C_QUOTES>
163     * @param addEndingForwardSlash <EMBED CLASS='external-html' DATA-FILE-ID=TN_C_AEFS>
164     * 
165     * @throws InnerTagKeyException <EMBED CLASS="external-html" DATA-FILE-ID="ITKEYEXPROP">
166     * @throws QuotesException <EMBED CLASS="external-html" DATA-FILE-ID="QEX">
167     * @throws HTMLTokException if an invalid HTML 4 or 5 token is not present
168     * <B>(check is {@code CASE_INSENSITIVE})</B>
169     * 
170     * @see InnerTagKeyException#check(String, String)
171     * @see QuotesException#check(String, SD, String)
172     * @see #generateElementString(String, Properties, Iterable, SD, boolean)
173     */
174    public TagNode(
175            String tok, Properties attributes, Iterable<String> keyOnlyAttributes,
176            SD quotes, boolean addEndingForwardSlash
177        )
178    {
179        this(
180            generateElementString
181                (tok, attributes, keyOnlyAttributes, quotes, addEndingForwardSlash));
182    }
183
184    /**
185     * This builds an HTML Element as a {@code String.}  This {@code String} may be passed to the
186     * standard HTML {@code TagNode} Constructor that accepts a {@code String} as input.
187     * 
188     * @param tok <EMBED CLASS='external-html' DATA-FILE-ID=TN_C_TOK>
189     * @param attributes <EMBED CLASS='external-html' DATA-FILE-ID=TN_C_ATTRIBUTES>
190     * @param keyOnlyAttributes <EMBED CLASS='external-html' DATA-FILE-ID=TN_C_KO_ATTRIBUTES>
191     * @param quotes <EMBED CLASS='external-html' DATA-FILE-ID=TN_C_QUOTES>
192     * @param addEndingForwardSlash <EMBED CLASS='external-html' DATA-FILE-ID=TN_C_AEFS>
193     * 
194     * @throws InnerTagKeyException <EMBED CLASS="external-html" DATA-FILE-ID="ITKEYEXPROP">
195     * @throws QuotesException <EMBED CLASS="external-html" DATA-FILE-ID="QEX">
196     * @throws HTMLTokException if an invalid HTML 4 or 5 token is not present
197     * <B>{@code CASE_INSENSITIVE}</B>
198     * 
199     * @return This method returns an HTML Element, as a {@code String}.
200     * 
201     * @see HTMLTokException#check(String[])
202     * @see InnerTagKeyException#check(String, String)
203     * @see QuotesException#check(String, SD, String)
204     */
205    protected static String generateElementString(
206            String tok, Properties attributes, Iterable<String> keyOnlyAttributes,
207            SD quotes, boolean addEndingForwardSlash
208        )
209    {
210        String computedQuote = (quotes == null) ? "" : ("" + quotes.quote);
211
212        HTMLTokException.check(tok);
213
214        // The HTML Element is "built" using a StringBuilder
215        StringBuilder sb = new StringBuilder();
216        sb.append("<" + tok);
217
218        // If there are any Inner-Tag Key-Value pairs, insert them first.
219        if ((attributes != null) && (attributes.size() > 0))
220
221            for (String key : attributes.stringPropertyNames())
222            {
223                String value = attributes.getProperty(key);
224
225                InnerTagKeyException.check(key, value);
226
227                QuotesException.check(
228                    value, quotes,
229                    "parameter 'Properties' contains:\nkey:\t" + key + "\nvalue:\t" + value + "\n"
230                );
231
232                sb.append(" " + key + '=' + computedQuote + value + computedQuote);
233            }
234
235        // If there are any Key-Only Inner-Tags (Boolean Attributes), insert them next.
236        if (keyOnlyAttributes != null)
237
238            for (String keyOnlyAttribute : keyOnlyAttributes) 
239            {
240                InnerTagKeyException.check(keyOnlyAttribute);
241                sb.append(" " + keyOnlyAttribute);
242            }
243
244        // Add a closing forward-slash
245        sb.append(addEndingForwardSlash ? " />" : ">");
246
247        // Build the String, using the StringBuilder, and return the newly-constructed HTML Element
248        return sb.toString();
249    }
250
251
252    // ********************************************************************************************
253    // ********************************************************************************************
254    // isTag
255    // ********************************************************************************************
256    // ********************************************************************************************
257
258
259    /**
260     * This method identifies that {@code 'this'} instance of (abstract parent-class)
261     * {@link HTMLNode} is, indeed, an instance of sub-class {@code TagNode}.
262     *
263     * @return This method shall always return <B>TRUE</B>  It overrides the parent-class
264     * {@code HTMLNode} method {@link #isTagNode()}, which always returns <B>FALSE</B>.
265     */
266    @Override
267    public boolean isTagNode() { return true; }
268
269    /**
270     * <EMBED CLASS='external-html' DATA-FILE-ID=TN_IF_TN_DESC>
271     * @return <EMBED CLASS='external-html' DATA-FILE-ID=TN_IF_TN_RET>
272     */
273    @Override
274    public TagNode ifTagNode() { return this; }
275
276    /**
277     * <EMBED CLASS='external-html' DATA-FILE-ID=TN_OPENTAG_PWA_DESC>
278     * @return <EMBED CLASS='external-html' DATA-FILE-ID=TN_OPENTAG_PWA_RET>
279     */
280    @Override
281    public TagNode openTagPWA()
282    {
283        // Closing TagNode's simply may not have attributes
284        if (this.isClosing) return null;
285
286        // A TagNode whose '.str' field is not AT LEAST 5 characters LONGER than the length of the
287        // HTML-Tag / Token, simply cannot have an attribute.
288        //
289        // NOTE: Below is the shortest possible HTML tag that could have an attribute-value pair.
290        // COMPUTE: '<' + TOK.LENGTH + SPACE + 'c' + '=' + '>'
291
292        if (this.str.length() < (this.tok.length() + 5)) return null;
293
294        // This TagNode is an opening HTML tag (like <DIV ...>, rather than </DIV>),
295        // and there are at least two additional characters after the token, such as: <DIV A...>
296        // It is not guaranteed that this tag has attributes, but it is possibly - based on these
297        /// optimization methods, and further investigation would have merit.
298
299        return this;
300    }
301
302    /**
303     * This is a loop-optimization method that makes finding opening {@code TagNode's} - <B>with
304     * attribute-values</B> - quites a bit faster.  All {@code HTMLNode} subclasses implement this
305     * method, but only {@code TagNode} instances will ever return a non-null value.
306     * 
307     * @return Returns null if and only if {@code 'this'} instance' {@link #isClosing} field is
308     * false.  When a non-null return-value is acheived, that value will always be {@code 'this'}
309     * instance.
310     */
311    @Override
312    public TagNode openTag()
313    { return isClosing ? null : this; }
314
315    /**
316     * Receives a list of html-elements which the {@code this.tok} field must match.  
317     * This method returns <B>TRUE</B> if any match is found.
318     * 
319     * <BR /><BR /><IMG SRC='doc-files/img/isTag.png' CLASS=JDIMG ALT='example'>
320     * 
321     * @param possibleTags This non-null list of potential HTML tags.
322     * @return <B>TRUE</B> If {@code this.tok} matches at least one of these tags.
323     * @see #tok
324     */
325    public boolean isTag(String... possibleTags)
326    { 
327        for (String htmlTag : possibleTags) if (htmlTag.equalsIgnoreCase(this.tok)) return true;
328        
329        return false;
330    }
331
332    /**
333     * Receives a list of html-elements which {@code this.tok} field <B>MAY NOT</B> match.
334     * This method returns <B>FALSE</B> if any match is found.
335     * 
336     * @param possibleTags This must be a non-null list of potential HTML tags.
337     * 
338     * @return <B>FALSE</B> If {@code this.tok} matches any one of these tags, and <B>TRUE</B>
339     * otherwise.
340     * 
341     * @see #tok
342     * @see #isTag(String[])
343     */
344    public boolean isTagExcept(String... possibleTags)
345    { 
346        for (String htmlTag : possibleTags) if (htmlTag.equalsIgnoreCase(this.tok)) return false;
347        
348        return true;
349    }
350
351    /**
352     * Receives two "criteria-specifier" parameters.  This method shall return <B>TRUE</B> if:
353     *
354     * <BR /><BR /><UL CLASS="JDUL">
355     * <LI>Field {@code 'isClosing'} is equal-to / consistent-with {@code TC tagCriteria}</LI>
356     * <LI>Field {@code 'tok'} is equal to at least one of the {@code 'possibleTags'}</LI>
357     * </UL>
358     * 
359     * <BR /><BR /><IMG SRC='doc-files/img/isTag2.png' CLASS=JDIMG ALT='example'>
360     * 
361     * @param tagCriteria This ought to be either {@code 'TC.OpeningTags'} or
362     * {@code TC.ClosingTags'}.  This parameter specifies what {@code 'this'} instance of
363     * {@code TagNode} is expected to contain, as {@code this.isClosing} field shall be compared
364     * against it.
365     * 
366     * @param possibleTags This is presumed to be a non-zero-length, and non-null-valued list of
367     * html tokens.
368     * 
369     * @return <B>TRUE</B> If {@code 'this'} matches the specified criteria, and <B>FALSE</B> 
370     * otherwise.
371     * 
372     * @see TC
373     * @see #isClosing
374     * @see #tok
375     */
376    public boolean isTag(TC tagCriteria, String... possibleTags)
377    {
378        // Requested an "OpeningTag" but this is a "ClosingTag"
379        if ((tagCriteria == TC.OpeningTags) && this.isClosing) return false;
380
381        // Requested a "ClosingTag" but this is an "OpeningTag"
382        if ((tagCriteria == TC.ClosingTags) && ! this.isClosing) return false;
383
384        for (int i=0; i < possibleTags.length; i++)
385
386            if (this.tok.equalsIgnoreCase(possibleTags[i]))
387            
388                // Found a TOKEN match, return TRUE immediately
389                return true;
390
391        // None of the elements in 'possibleTags' equalled tn.tok
392        return false;
393    }
394
395    /**
396     * Receives a {@code TagNode} and then two "criteria-specifier" parameters.  This method shall
397     * return <B>FALSE</B> if:
398     * 
399     * <BR /><BR /><UL CLASS="JDUL">
400     * <LI> Field {@code 'isClosing'} is <B><I>not</I></B> equal-to / 
401     *      <B><I>not</I></B> consistent-with {@code TC tagCriteria}</LI>
402     * <LI> Field {@code 'tok'} is <B><I>equal-to</I></B> any of the {@code 'possibleTags'}</LI>
403     * </UL>
404     *
405     * @param tagCriteria tagCriteria This ought to be either {@code 'TC.OpeningTags'} or
406     * {@code TC.ClosingTags'} This parameter specifies what {@code 'this'} instance of
407     * {@code TagNode} is expected to contain, as {@code this.isClosing} field shall be compared
408     * against it.
409     * 
410     * @param possibleTags This is presumed to be a non-zero-length, and non-null-valued list of
411     * html tokens.
412     * 
413     * @return <B>TRUE</B> If this {@code TagNode 'n'} matches the specified criteria explained
414     * above, and <B>FALSE</B> otherwise.
415     * 
416     * @see TC
417     * @see #tok
418     * @see #isClosing
419     */
420    public boolean isTagExcept(TC tagCriteria, String... possibleTags)
421    {
422        // Requested an "OpeningTag" but this is a "ClosingTag"
423        if ((tagCriteria == TC.OpeningTags) && this.isClosing) return false;
424
425        // Requested a "ClosingTag" but this is an "OpeningTag"
426        if ((tagCriteria == TC.ClosingTags) && ! this.isClosing) return false;
427
428        for (int i=0; i < possibleTags.length; i++)
429
430            if (this.tok.equalsIgnoreCase(possibleTags[i]))
431
432                // The Token of the input node was a match with one of the 'possibleTags'
433                // Since this is "Except" - we must return 'false'
434
435                return false;
436
437        // None of the elements in 'possibleTags' equalled tn.tok
438        // since this is "Except" - return 'true'
439
440        return true;
441    }
442
443
444    // ********************************************************************************************
445    // ********************************************************************************************
446    // Main Method 'AV'
447    // ********************************************************************************************
448    // ********************************************************************************************
449
450
451    /**
452     * <EMBED CLASS="external-html" DATA-FILE-ID=TN_AV_DESC>
453     * <EMBED CLASS="external-html" DATA-FILE-ID=TN_AV_DESC_EXAMPLE>
454     * @param innerTagAttribute <EMBED CLASS="external-html" DATA-FILE-ID=TN_AV_ITA>
455     * @return <EMBED CLASS="external-html" DATA-FILE-ID=TN_AV_RET>
456     * @see #isClosing
457     * @see #str
458     * @see #tok
459     * @see StringParse#ifQuotesStripQuotes(String)
460     * @see AttrRegEx#KEY_VALUE_REGEX
461     */
462    public String AV(String innerTagAttribute)
463    {
464        // All HTML element tags that start like: </DIV> with a front-slash.
465        // They may not legally contain inner-tag attributes.
466
467        if (this.isClosing) return null;    
468
469        // All HTML element tags that contain only <TOK> (TOK <==> Tag-Name) in their 'str' field
470        // Specifically: '<', TOKEN, '>',  (Where TOKEN is 'div', 'span', 'table', 'ul', etc...)
471        // are TOO SHORT to have the attribute, so don't check... return null.
472
473        if (this.str.length() < 
474            (3 + this.tok.length() + (innerTagAttribute = innerTagAttribute.trim()).length()))
475            return null;
476
477        // Matches "Attribute / Inner-Tag Key-Value" Pairs.
478        Matcher m = AttrRegEx.KEY_VALUE_REGEX.matcher(this.str);
479
480        // This loop iterates the KEY_VALUE PAIRS THAT HAVE BEEN FOUND.
481        /// NOTE: The REGEX Matches on Key-Value Pairs.
482
483        while (m.find())
484
485            // m.group(2) is the "KEY" of the Attribute KEY-VALUE Pair
486            // m.group(3) is the "VALUE" of the Attribute.
487            if (m.group(2).equalsIgnoreCase(innerTagAttribute))
488                return StringParse.ifQuotesStripQuotes(m.group(3));
489
490        // This means the attribute name provided to parameter 'innerTagAttribute' was not found.
491        return null;
492    }
493
494    /**
495     * <SPAN STYLE="color: red;"><B>OPT: Optimized</B></SPAN>
496     * 
497     * <BR /><BR /> This is an "optimized" version of method {@link #AV(String)}.  This method does
498     * the exact same thing as {@code AV(...)}, but leaves out parameter-checking and
499     * error-checking. This is used internally (repeatedly) by the NodeSearch Package Search Loops.
500     * 
501     * @param innerTagAttribute This is the inner-tag / attribute <B STYLE="color: red;">name</B>
502     * whose <B STYLE="color: red;">value</B> is hereby being requested.
503     * 
504     * @return {@code String}-<B STYLE="color: red;">value</B> of this inner-tag / attribute.
505     * 
506     * @see StringParse#ifQuotesStripQuotes(String)
507     * @see #str
508     * @see TagNode.AttrRegEx#KEY_VALUE_REGEX
509     */
510    public String AVOPT(String innerTagAttribute)
511    {
512        // COPIED DIRECTLY FROM class TagNode, leaves off initial tests.
513
514        // Matches "Attribute / Inner-Tag Key-Value" Pairs.
515        Matcher m = AttrRegEx.KEY_VALUE_REGEX.matcher(this.str);
516
517        // This loop iterates the KEY_VALUE PAIRS THAT HAVE BEEN FOUND.
518        /// NOTE: The REGEX Matches on Key-Value Pairs.
519
520        while (m.find())
521
522            // m.group(2) is the "KEY" of the Attribute KEY-VALUE Pair
523            // m.group(3) is the "VALUE" of the Attribute.
524
525            if (m.group(2).equalsIgnoreCase(innerTagAttribute))
526                return StringParse.ifQuotesStripQuotes(m.group(3));
527
528        // This means the attribute name provided to parameter 'innerTagAttribute' was not found.
529        return null;
530    }
531
532
533    // ********************************************************************************************
534    // ********************************************************************************************
535    // Attribute Modify-Value methods
536    // ********************************************************************************************
537    // ********************************************************************************************
538
539
540    /**
541     * <EMBED CLASS="external-html" DATA-FILE-ID=TN_SET_AV_DESC>
542     * @param attribute <EMBED CLASS="external-html" DATA-FILE-ID=TN_SET_AV_ATTR>
543     * @param value Any valid attribute-<B STYLE="color: red;">value</B>.  This parameter may not
544     * be null, or a {@code NullPointerException} will throw.
545     * @param quote <EMBED CLASS="external-html" DATA-FILE-ID=TN_SET_AV_QUOTE>
546     * @throws InnerTagKeyException <EMBED CLASS="external-html" DATA-FILE-ID="ITKEYEX2">
547     * @throws QuotesException <EMBED CLASS="external-html" DATA-FILE-ID="QEX">
548     * @throws ClosingTagNodeException <EMBED CLASS="external-html" DATA-FILE-ID=CTNEX>
549     * @throws HTMLTokException If an invalid HTML 4 or 5 token is not present 
550     * (<B>{@code CASE_INSENSITIVE}</B>).
551     * @return <EMBED CLASS="external-html" DATA-FILE-ID=TN_SET_AV_RET>
552     * @see ClosingTagNodeException#check(TagNode)
553     * @see #generateElementString(String, Properties, Iterable, SD, boolean)
554     * @see #setAV(Properties, SD)
555     * @see #tok
556     * @see #str
557     * @see #isClosing
558     */
559    public TagNode setAV(String attribute, String value, SD quote)
560    {
561        ClosingTagNodeException.check(this);
562
563        if (attribute == null) throw new NullPointerException(
564            "You have passed 'null' to the 'attribute' (attribute-name) String-parameter, " +
565            "but this is not allowed here."
566        );
567
568        if (value == null) throw new NullPointerException(
569            "You have passed 'null' to the 'attribute' (attribute-value) String-parameter, " +
570            "but this is not allowed here."
571        );
572
573        // Retrieve all "Key-Only" (Boolean) Attributes from 'this' (the original) TagNode
574        // Use Java Streams to filter out any that match the newly-added attribute key-value pair.
575        // SAVE: Save the updated / shortened list to a List<String>
576
577        List<String> prunedOriginalKeyOnlyAttributes = allKeyOnlyAttributes(true)
578            .filter((String originalKeyOnlyAttribute) -> 
579                ! originalKeyOnlyAttribute.equalsIgnoreCase(attribute))
580            .collect(Collectors.toList());
581
582        // Retrieve all Inner-Tag Key-Value Pairs.  Preserve the Case of the Attributes.  Preserve
583        // the Quotation-Marks.
584
585        Properties  p                       = allAV(true, true);
586        String      originalValueWithQuotes = null;
587        String      computedQuote           = null;
588
589        // NOTE, there should only be ONE instance of an attribute in an HTML element, but
590        // malformed HTML happens all the time, so to keep this method safe, it checks
591        // (and removes) the entire attribute-list for matches - not just the first found instance.
592
593        for (String key : p.stringPropertyNames())
594
595            if (key.equalsIgnoreCase(attribute))
596            {
597                Object temp = p.remove(key);
598                if (temp instanceof String) originalValueWithQuotes = (String) temp;
599            }
600
601        // If the user does not wish to "change" the original quote choice, then find out what
602        // the original-quote choice was...
603
604        if (
605                (quote == null) 
606            &&  (originalValueWithQuotes != null)
607            &&  (originalValueWithQuotes.length() >= 2)
608        )
609        {
610            char s = originalValueWithQuotes.charAt(0);
611            char e = originalValueWithQuotes.charAt(originalValueWithQuotes.length() - 1);
612
613            if ((s == e) && (s == '\''))        computedQuote = "" + SD.SingleQuotes.quote;
614
615            else if ((s == e) && (s == '"'))    computedQuote = "" + SD.DoubleQuotes.quote;
616
617            else                                computedQuote = "";
618        }
619        else if (quote == null)                 computedQuote = "";
620
621        else                                    computedQuote = "" + quote.quote;
622
623        p.put(attribute, computedQuote + value + computedQuote);
624
625        return new TagNode(
626            generateElementString(
627                // Rather than using '.tok' here, preserve the case of the original HTML Element
628                this.str.substring(1, 1 + tok.length()), p,
629                prunedOriginalKeyOnlyAttributes, null /* SD */, this.str.endsWith("/>")
630            ));
631    }
632
633    /**
634     * This allows for inserting or updating multiple {@code TagNode} inner-tag
635     * <B STYLE="color: red;">key-value</B> pairs with a single method invocation.
636     * 
637     * @param attributes These are the new attribute <B STYLE="color: red;">key-value</B> pairs to
638     * be inserted.
639     * 
640     * @param defaultQuote This is the default quotation mark to use, if the {@code 'attribute'}
641     * themselves do not already have quotations.
642     *
643     * <BR /><BR /><B><SPAN STYLE='color: red;'>IMPORTANT:</B></SPAN> If this value is used, then
644     * none of the provided {@code Property}-<B STYLE="color: red;">values</B> of the input
645     * {@code java.lang.Properties} instance should have quotes already.  Each of these 
646     * new-<B STYLE="color: red;">values</B> will be wrapped in the quote that is provided as the
647     * value to this parameter.
648     *
649     * <BR /><BR /><B><SPAN STYLE='color: red;'>HOWEVER:</B></SPAN> If this parameter is passed a
650     * value of 'null', then no quotes will be added to the new <B STYLE="color: red;">keys</B> -
651     * <I>unless the attribute being inserted is replacing a previous attribute that was already
652     * present in the element.</I>  In this case, the original quotation shall be used.  If this
653     * parameter receives 'null' and any of the new {@code Properties} were not already present in
654     * the original ({@code 'this'}) element, then no quotation marks will be used, which may
655     * throw a {@code QuotesException} if the attribute <B STYLE="color: red;">value</B> contains
656     * any white-space.
657     *
658     * @throws InnerTagKeyException <EMBED CLASS="external-html" DATA-FILE-ID="ITKEYEXPROP">
659     * 
660     * @throws QuotesException if there are "quotes within quotes" problems, due to the
661     * <B STYLE="color: red;">values</B> of the <B STYLE="color: red;">key-value</B> pairs.
662     * 
663     * @throws HTMLTokException if an invalid HTML 4 or 5 token is not present 
664     * <B>({@code CASE_INSENSITIVE})</B>
665     * 
666     * @throws ClosingTagNodeException <EMBED CLASS="external-html" DATA-FILE-ID=CTNEX>
667     *
668     * @return An HTML {@code TagNode} instance with updated {@code TagNode} information.
669     *
670     * <BR /><BR /><B><SPAN STYLE="color: red;">IMPORTANT:</SPAN></B> Because 
671     * <I>{@code TagNode's} are immutable</I> (since they are just wrapped-java-{@code String's},
672     * which are also immutable), it is important to remember that this method <I><B>does not
673     * change the contents</B></I> of a {@code TagNode}, but rather <I><B>returns an entirely
674     * new {@code TagNode}</I></B> as a result instead.
675     *
676     * @see ClosingTagNodeException#check(TagNode)
677     * @see #setAV(String, String, SD)
678     * @see #allKeyOnlyAttributes(boolean)
679     * @see #tok
680     * @see #str
681     * @see #isClosing
682     */
683    public TagNode setAV(Properties attributes, SD defaultQuote)
684    {
685        ClosingTagNodeException.check(this);
686
687        // Check that this attributes has elements.
688        if (attributes.size() == 0) throw new IllegalArgumentException(
689            "You have passed an empty java.util.Properties instance to the " +
690            "setAV(Properties, SD) method"
691        );
692
693        // Retrieve all Inner-Tag Key-Value Pairs.
694        //      Preserve: the Case of the Attributes.
695        //      Preserve: the Quotation-Marks.
696
697        Properties originalAttributes = allAV(true, true);
698
699        // Retrieve all "Key-Only" (Boolean) attributes from the new / update attribute-list
700        Set<String> newAttributeKeys = attributes.stringPropertyNames();
701
702        // Retrieve all "Key-Only" (Boolean) Attributes from 'this' (the original) TagNode
703        // Use Java Streams to filter out all the ones that need to be clobbered by-virtue-of
704        // the fact that they are present in the new / parameter-updated attribute key-value list.
705        // SAVE: Save the updated / shortened list to a List<String>
706
707        List<String> prunedOriginalKeyOnlyAttributes = allKeyOnlyAttributes(true)
708            .filter((String originalKeyOnlyAttribute) ->
709            {
710                // Returns false when the original key-only attribute matches one of the
711                // new attributes being inserted.  Notice that a case-insensitive comparison
712                // must be performed - to preserve case.
713
714                for (String newKey : newAttributeKeys) 
715                    if (newKey.equalsIgnoreCase(originalKeyOnlyAttribute)) 
716                        return false;
717
718                return true;
719            })
720            .collect(Collectors.toList());
721
722        // NOTE: There is no need to check the validity of the new attributes.  The TagNode
723        //       constructor that is invoked on the last line of this method will do a 
724        //       validity-check on the attribute key-names provided to the 'attributes' 
725        //       java.util.Properties instance passed to to this method.
726
727        for (String newKey : newAttributeKeys)
728        {
729            String      originalValueWithQuotes = null;
730            String      computedQuote           = null;
731
732            // NOTE, there should only be ONE instance of an attribute in an HTML element, but
733            // malformed HTML happens all the time, so to keep this method safe, it checks (and
734            // removes) the entire attribute-list for matches - not just the first found instance.
735
736            for (String originalKey : originalAttributes.stringPropertyNames())
737
738                if (originalKey.equalsIgnoreCase(newKey))
739                {
740                    // Remove the original key-value inner-tag pair.
741                    Object temp = originalAttributes.remove(originalKey);
742                    if (temp instanceof String) originalValueWithQuotes = (String) temp;
743                }
744
745            // If the user does not wish to "change" the original quote choice, then find out what
746            // the original-quote choice was...
747
748            if (
749                    (defaultQuote == null) 
750                &&  (originalValueWithQuotes != null)
751                &&  (originalValueWithQuotes.length() >= 2)
752            )
753            {
754                char s = originalValueWithQuotes.charAt(0);
755                char e = originalValueWithQuotes.charAt(originalValueWithQuotes.length() - 1);
756
757                if ((s == e) && (s == '\''))        computedQuote = "" + SD.SingleQuotes.quote;
758
759                else if ((s == e) && (s == '"'))    computedQuote = "" + SD.DoubleQuotes.quote;
760
761                else                                computedQuote = "";
762            }
763
764            else if (defaultQuote == null)          computedQuote = "";
765
766            else                                    computedQuote = "" + defaultQuote.quote;
767
768            // Insert the newly, updated key-value inner-tag pair.  This 'Properties' will be
769            // used to construct a new TagNode.
770
771            originalAttributes.put(newKey, computedQuote + attributes.get(newKey) + computedQuote);
772        }
773
774        return new TagNode(
775            generateElementString(
776                // Rather than using '.tok' here, preserve the case of the original HTML Element
777                this.str.substring(1, 1 + tok.length()),
778                originalAttributes, prunedOriginalKeyOnlyAttributes, null /* SD */,
779                this.str.endsWith("/>")
780            ));
781    }
782
783
784    /**
785     * This will append a substring to the attribute <B STYLE="color: red;">value</B> of an HTML
786     * {@code TagNode}.
787     *
788     * This method can be very useful, for instance when dealing with CSS tags that are inserted
789     * inside the HTML node itself.  For example, in order to add a {@code 'color: red;
790     * background: white;'} portion to the CSS {@code 'style'} tag of an HTML
791     * {@code <TABLE STYLE="...">} element, without clobbering the {@code style}-information that
792     * is already inside the element, using this method will achieve that.
793     *
794     * @param attribute The <B STYLE="color: red;">name</B> of the attribute to which the 
795     * <B STYLE="color: red;">value</B> must be appended.  This parameter may not be null, or a
796     * {@code NullPointerException} will throw.
797     *
798     * @param appendStr The {@code String} to be appended to the
799     * attribute-<B STYLE="color: red;">value</B>.
800     * 
801     * @param startOrEnd If this parameter is <B>TRUE</B> then the append-{@code String} will be
802     * inserted at the beginning (before) whatever the current attribute-<B STYLE="color: red;">
803     * value</B> is. If this parameter is <B>FALSE</B> then the append-{@code String} will be
804     * inserted at the end (after) the current attribute-<B STYLE="color: red;">value</B>
805     * {@code String}.
806     *
807     * <BR /><BR /><B>NOTE:</B> If tag element currently does not posses this attribute, then the
808     * <B STYLE="color: red;">attribute/value</B> pair will be created and inserted with its
809     * <B STYLE="color: red;">value</B> set to the value of {@code 'appendStr'.}
810     *
811     * @param quote <EMBED CLASS='external-html' DATA-FILE-ID=TGND_QUOTE_EXPL>
812     *
813     * <BR /><BR />It is important to note that "appending" a {@code String} to an attribute's
814     * <B STYLE='color: red;'>value</B> will often (but not always) mean that the new
815     * attribute-<B STYLE='color: red;'>value</B> will have a space character.  <B><I>If</I></B>
816     * this parameter were passed null, <B><I>and if</I></B> the original tag had a value, but did
817     * not use any quotes, <B><I>then</I></B> the attribute's ultimate inclusion into the tag would
818     * generate invalid HTML, and the invocation of {@link #setAV(String, String, SD)} would
819     * throw a {@link QuotesException}.
820     *
821     * @return Since all instances of {@code TagNode} are immutable, this method will not actually 
822     * alter the {@code TagNode} element, but rather create a new object reference that contains
823     * the updated attribute.
824     *
825     * @see #AV(String)
826     * @see #setAV(String, String, SD)
827     * @see ClosingTagNodeException#check(TagNode)
828     * 
829     * @throws ClosingTagNodeException <EMBED CLASS="external-html" DATA-FILE-ID=CTNEX>
830     * 
831     * @throws QuotesException The <B><A HREF=#QUOTEEX>rules</A></B> for quotation usage apply
832     * here too, and see that explanation for how how this exception could be thrown.
833     */
834    public TagNode appendToAV(String attribute, String appendStr, boolean startOrEnd, SD quote)
835    {
836        ClosingTagNodeException.check(this);
837
838        if (attribute == null) throw new NullPointerException(
839            "You have passed 'null' to the 'attribute' (attribute-name) String-parameter, " +
840            "but this is not allowed here."
841        );
842
843        if (appendStr == null) throw new NullPointerException(
844            "You have passed 'null' to the 'appendStr' (attribute-value-append-string) " +
845            "String-parameter, but this is not allowed here."
846        );
847
848        String curVal = AV(attribute);
849
850        if (curVal == null) curVal = "";
851
852        // This decides whether to insert the "appendStr" before the current value-string,
853        // or afterwards.  This is based on the passed boolean-parameter 'startOrEnd'
854
855        curVal = startOrEnd ? (appendStr + curVal) : (curVal + appendStr);
856
857        // Reuse the 'setAV(String, String, SD)' method already defined in this class.
858        return setAV(attribute, curVal, quote);
859    }   
860
861
862    // ********************************************************************************************
863    // ********************************************************************************************
864    // Attribute Removal Operations
865    // ********************************************************************************************
866    // ********************************************************************************************
867
868
869    /**
870     * Convenience Method.
871     * <BR />Invokes: {@link #removeAttributes(String[])}
872     */
873    public TagNode remove(String attributeName) { return removeAttributes(attributeName); }
874
875    /**
876     * <EMBED CLASS="external-html" DATA-FILE-ID=TN_REM_ATTR_DESC>
877     * @param attributes <EMBED CLASS="external-html" DATA-FILE-ID=TN_REM_ATTR_ATTR>
878     * @return <EMBED CLASS="external-html" DATA-FILE-ID=TN_REM_ATTR_RET>
879     * @throws ClosingTagNodeException <EMBED CLASS="external-html" DATA-FILE-ID=CTNEX>
880     * @see ClosingTagNodeException#check(TagNode)
881     * @see #tok
882     * @see #isClosing
883     * @see #str
884     * @see #TagNode(String)
885     * @see #generateElementString(String, Properties, Iterable, SD, boolean)
886     */
887    public TagNode removeAttributes(String... attributes)
888    {
889        ClosingTagNodeException.check(this);
890
891        // Retrieve all Inner-Tag Key-Value Pairs.  Preserve the Case of the Attributes.  Preserve
892        // the Quotation-Marks.
893
894        Properties originalAttributes = allAV(true, true);
895
896        // Remove any attributes from the "Attributes Key-Value Properties Instance" which MATCH
897        // the attribute names that have been EXPLICITLY REQUESTED FOR REMOVAL
898
899        for (String key : originalAttributes.stringPropertyNames())
900            for (String attribute : attributes)
901                if (key.equalsIgnoreCase(attribute))
902                    originalAttributes.remove(key);
903
904        // Retrieve all "Boolean Attributes" (key-no-value).  Preserve the Case of these Attributes
905        // Retain only the attributes in the 'filteredKeyOnlyAttributes' String-Array which have
906        // PASSED THE FILTER OPERATION.  The filter operation only returns TRUE if the 
907        // requested-attribute-list does not contain a copy of the Key-Only-Attribute
908        //
909        // NOTE: 'true' is passed as input to the 'allKeyOnlyAttributes(boolean)' method to request
910        //       that CASE be PRESERVED.
911
912        Iterable<String> prunedKeyOnlyAttributes = allKeyOnlyAttributes(true)
913
914            .filter((String attribute) ->
915            {
916                // Returns false when the original key-only attribute matches one of the attributes
917                // that was requested to to be removed.  Notice that a case-insensitive comparison 
918                // must be performed.
919
920                for (String removeAttributes : attributes)
921                    if (removeAttributes.equalsIgnoreCase(attribute))
922                        return false;
923
924                return true;
925            })
926            .collect(Collectors.toList());
927
928        return new TagNode(
929            generateElementString(
930                // Rather than using '.tok' here, preserve the case of the original HTML Element
931                this.str.substring(1, 1 + tok.length()),
932                originalAttributes, prunedKeyOnlyAttributes, /* SD */ null, 
933                this.str.endsWith("/>")
934            ));
935    }
936
937    /**
938     * {@code TagNode's} are immutable.  And because of this, calling {@code removeAllAV()} is
939     * actually the same as retrieving the standard, zero-attribute, pre-instantiated instance of
940     * an HTML Element.  Pre-instantiated <B><I>factory-instances</I></B> of {@code class TagNode}
941     * for every HTML-Element are stored by {@code class HTMLTags} inside a {@code Hashtable.}
942     * They can be retrieved in multiple ways, two of which are found in methods in this class.
943     *
944     * <BR /><BR /><B>Point of Interest:</B> Calling these three different methods will all return
945     * <I>identical</I> {@code Object} references:
946     * 
947     * <BR /><BR />
948     * 
949     * <UL CLASS="JDUL">
950     * <LI>{@code TagNode v1 = myTagNode.removeAllAV(); } </LI>
951     * <LI>{@code TagNode v2 = TagNode.getInstance(myTagToken, openOrClosed); } </LI>
952     * <LI>{@code TagNode v3 = HTMLTag.hasTag(myTagToken, openOrClosed); } </LI>
953     * <LI><SPAN STYLE="color: red;">{@code assert((v1 == v2) && (v2 == v3)); }</SPAN></LI>
954     * </UL>
955     * 
956     * <BR /><BR /><IMG SRC='doc-files/img/removeAllAV.png' CLASS=JDIMG ALT='example'>
957     * 
958     * @return An HTML {@code TagNode} instance with all inner attributes removed.
959     *
960     * <BR /><BR /><B>NOTE:</B> If this tag contains an "ending forward slash" that ending slash
961     * will not be included in the output {@code TagNode.}
962     *
963     * <BR /><BR /><B><SPAN STYLE="color: red;">IMPORTANT:</SPAN></B> Because <I>{@code TagNode's} 
964     * are immutable</I> (since they are just wrapped-java-{@code String's}, which are also
965     * immutable), it is important to remember that this method <I><B>does not change the
966     * contents</B></I> of a {@code TagNode}, but rather <I><B>returns an entirely new
967     * {@code TagNode}</I></B> as a result instead.
968     * 
969     * @throws ClosingTagNodeException <EMBED CLASS="external-html" DATA-FILE-ID=CTNEX>
970     * 
971     * @see ClosingTagNodeException#check(TagNode)
972     * @see #getInstance(String, TC)
973     * @see #str
974     * @see #tok
975     * @see TC#OpeningTags
976     */
977    public TagNode removeAllAV()
978    {
979        ClosingTagNodeException.check(this);
980
981        // NOTE: We *CANNOT* use the 'tok' field to instantiate the TagNode here, because the 'tok'
982        // String-field is *ALWAYS* guaranteed to be in a lower-case format.  The 'str'
983        // String-field, however uses the original case that was found on the HTML Document by the
984        // parser (or in the Constructor-Parameters that were passed to construct 'this' instance
985        // of TagNode.
986
987        return getInstance(this.str.substring(1, 1 + tok.length()), TC.OpeningTags);
988    }
989
990    // ********************************************************************************************
991    // Retrieve all attributes
992    // ********************************************************************************************
993
994    /**
995     * Convenience Method.
996     * <BR />Invokes: {@link #allAV(boolean, boolean)}
997     * <BR />Attribute-<B STYLE="color: red;">names</B> will be in lower-case.
998     */
999    public Properties allAV() { return allAV(false, false); }
1000
1001    /**
1002     * This will copy every attribute <B STYLE="color: red;">key-value</B> pair inside
1003     * {@code 'this'} HTML {@code TagNode} element into a {@code java.util.Properties} Hash-Table.
1004     *
1005     * <BR /><BR /><B>RETURN-VALUE NOTE:</B> This method shall not return any "Key-Only Attributes"
1006     * (a.k.a. "Boolean Attributes").  The most commonly used "Boolean Attribute" example is the
1007     * {@code 'HIDDEN'} key-word that is used to prevent the browser from displaying an HTML
1008     * Element. Inner-tags that represent attribute <B STYLE="color: red;">key-value</B> pairs are
1009     * the only attributes that may be included in the returned {@code 'Properties'} instance.
1010     *
1011     * <BR /><BR /><IMG SRC='doc-files/img/allAV.png' CLASS=JDIMG ALT="example">
1012     * 
1013     * @param keepQuotes If this parameter is passed <B>TRUE</B>, then any surrounding quotation
1014     * marks will be included for each the <B STYLE="color: red;">values</B> of each attribute
1015     * key-value pair.
1016     *
1017     * @param preserveKeysCase If this parameter is passed <B>TRUE</B>, then the method
1018     * {@code String.toLowerCase()} will not be invoked on any of the
1019     * <B STYLE="color: red;">keys</B> (attribute-names) of each inner-tag key-value pair.
1020     *
1021     * <EMBED CLASS="external-html" DATA-FILE-ID=TAGNODE_PRESERVE_C>
1022     *
1023     * @return This returns a list of each and every attribute-<B STYLE="color: red;">name</B> -
1024     * <I>and the associate <B STYLE="color: red;">value</B> of the attribute</I> - found in
1025     * {@code 'this' TagNode}.  An instance of {@code class java.util.Properties} is used to store
1026     * the attribute <B STYLE="color: red;">key-value</B> pairs. 
1027     *
1028     * <BR /><BR /><B>NOTE:</B> This method will <B>NOT</B> return any boolean,
1029     * <B STYLE="color: red;">key-only</B> attributes present in {@code 'this' TagNode}.
1030     * 
1031     * <BR /><BR /><B>ALSO:</B> This method shall not return {@code 'null'}.  If there do not
1032     * exist any Attribute-Value Pairs, or if {@code 'this'} node is a closing-element, then
1033     * an empty {@code 'Properties'} instance shall be returned.
1034     * 
1035     * @see StringParse#ifQuotesStripQuotes(String)
1036     * @see AttrRegEx#KEY_VALUE_REGEX
1037     * @see #tok
1038     * @see #str
1039     */
1040    public Properties allAV(boolean keepQuotes, boolean preserveKeysCase)
1041    {
1042        Properties ret = new Properties();
1043
1044        // NOTE:    OPTIMIZED, "closing-versions" of the TagNode, and TagNode's whose 'str' field is
1045        //          is only longer than the token, itself, by 3 or less characters cannot have
1046        //          attributes.
1047        // CHARS:   '<', TOKEN, SPACE, '>'
1048        // RET:     In that case, just return an empty 'Properties' instance.
1049
1050        if (isClosing || (str.length() <= (tok.length() + 3))) return ret;
1051
1052        // This RegEx Matcher 'matches' against Attribute/InnerTag Key-Value Pairs.
1053        // m.group(1): UN-USED!  (Includes Key, Equals-Sign, and Value).  Not w/leading white-space
1054        // m.group(2): returns the 'key' portion of the key-value pair, before an '=' (equals-sign)
1055        // m.group(3): returns the 'value' portion of the key-value pair, after an '='
1056
1057        Matcher m = AttrRegEx.KEY_VALUE_REGEX.matcher(this.str);
1058
1059        // MORE-CODE, but MORE-EFFICIENT (slightly)
1060
1061        if      (keepQuotes     && preserveKeysCase)
1062            while (m.find()) ret.put(m.group(2), m.group(3));
1063
1064        else if (!keepQuotes    && preserveKeysCase)
1065            while (m.find()) ret.put(m.group(2), StringParse.ifQuotesStripQuotes(m.group(3)));
1066
1067        else if (keepQuotes     && !preserveKeysCase)
1068            while (m.find()) ret.put(m.group(2).toLowerCase(), m.group(3));
1069
1070        else if (!keepQuotes    && !preserveKeysCase)
1071            while (m.find()) 
1072                ret.put(m.group(2).toLowerCase(), StringParse.ifQuotesStripQuotes(m.group(3)));
1073
1074        return ret;
1075    }
1076
1077    /**
1078     * Convenience Method.
1079     * <BR />Invokes: {@link #allAN(boolean, boolean)}
1080     * <BR />Attribute-<B STYLE="color: red;">names</B> will be in lower-case
1081     */
1082    public Stream<String> allAN()
1083    { return allAN(false, false); }
1084
1085    /**
1086     * This method will only return a list of attribute-<B STYLE="color: red;">names</B>.  The
1087     * attribute-<B STYLE="color: red">values</B> shall <B>NOT</B> be included in the result.  The
1088     * {@code String's} returned can have their "case-preserved" by passing <B>TRUE</B> to the
1089     * input boolean parameter {@code 'preserveKeysCase'}.
1090     *
1091     * @param preserveKeysCase If this is parameter receives <B>TRUE</B> then the case of the
1092     * attribute-<B STYLE="color: red;">names</B> shall be preserved.
1093     *
1094     * <EMBED CLASS="external-html" DATA-FILE-ID=TAGNODE_PRESERVE_C>
1095     *
1096     * @param includeKeyOnlyAttributes When this parameter receives <B>TRUE</B>, then any
1097     * "Boolean Attributes" or "Key-Only, No-Value-Assignment" Inner-Tags will <B>ALSO</B> be
1098     * included in the {@code Stream<String>} returned by this method.
1099     *
1100     * @return an instance of {@code Stream<String>} containing all
1101     * attribute-<B STYLE="color: red;">names</B> identified in {@code 'this'} instance of
1102     * {@code TagNode}.  A {@code java.util.stream.Stream} is used because it's contents can easily
1103     * be converted to just about any data-type.  
1104     *
1105     * <EMBED CLASS="external-html" DATA-FILE-ID=STRMCNVT>
1106     *
1107     * <BR /><B>NOTE:</B> This method shall never return {@code 'null'} - even if there are no 
1108     * attribute <B STYLE="color: red;">key-value</B> pairs contained by {@code 'this' TagNode}.
1109     * If there are strictly zero attributes, an empty {@code Stream} shall be returned, instead.
1110     * 
1111     * @see #allKeyOnlyAttributes(boolean)
1112     * @see #allAN()
1113     */
1114    public Stream<String> allAN(boolean preserveKeysCase, boolean includeKeyOnlyAttributes)
1115    {
1116        // If there is NO ROOM in the "str" field for attributes, then there is now way attributes
1117        // could exist in this element.  Return "empty" immediately.
1118        // 
1119        // NOTE:    OPTIMIZED, "closing-versions" of the TagNode, and TagNode's whose 'str' field
1120        //          is only longer than the token, itself, by 3 or less characters cannot have
1121        //          attributes.
1122        //
1123        // CHARS:   '<', TOKEN, SPACE, '>'
1124        // RET:     In that case, just return an empty Stream.
1125
1126        if (isClosing || (str.length() <= (tok.length() + 3))) return Stream.empty();
1127
1128        // Use Java Streams.  A String-Stream is easily converted to just about any data-type
1129        Stream.Builder<String> b = Stream.builder();
1130
1131        // This RegEx Matcher 'matches' against Attribute/InnerTag Key-Value Pairs.
1132        // m.group(2): returns the 'key' portion of the key-value pair, before an '=' (equals-sign)
1133
1134        Matcher m = AttrRegEx.KEY_VALUE_REGEX.matcher(this.str);
1135
1136        // Retrieve all of the keys of the attribute key-value pairs.
1137        while (m.find()) b.add(m.group(2));
1138
1139        // This Stream contains only keys that were once key-value pairs, if there are "key-only" 
1140        // attributes, they have not been added yet.
1141
1142        Stream<String> ret = b.build();
1143
1144        // Convert these to lower-case, (if requested)
1145        if (! preserveKeysCase) ret = ret.map((String attribute) -> attribute.toLowerCase());
1146
1147        // Now, add in all the "Key-Only" attributes (if there are any).  Note, "preserve-case"
1148        // and "to lower case" are handled, already, in method "allKeyOnlyAttributes(boolean)"
1149
1150        if (includeKeyOnlyAttributes)
1151            return Stream.concat(ret, allKeyOnlyAttributes(preserveKeysCase));
1152
1153        return ret;
1154    }
1155
1156
1157    // ********************************************************************************************
1158    // ********************************************************************************************
1159    // Key only attributes
1160    // ********************************************************************************************
1161    // ********************************************************************************************
1162
1163
1164    /**
1165     * <EMBED CLASS='external-html' DATA-FILE-ID=TN_ALL_KOA_DESC>
1166     * @param preserveKeysCase <EMBED CLASS='external-html' DATA-FILE-ID=TN_ALL_KOA_PKC>
1167     * <EMBED CLASS="external-html" DATA-FILE-ID=TAGNODE_PRESERVE_C>
1168     * @return <EMBED CLASS="external-html" DATA-FILE-ID=TN_ALL_KOA_RET>
1169     * <EMBED CLASS="external-html" DATA-FILE-ID=STRMCNVT>
1170     * @see #tok
1171     * @see #str
1172     */
1173    public Stream<String> allKeyOnlyAttributes(boolean preserveKeysCase)
1174    {
1175        // NOTE: OPTIMIZED, "closing-versions" of the TagNode, and TagNode's whose 'str'
1176        //       field is only longer than the token, itself, by 3 or less characters cannot have
1177        //       attributes.  In that case, just return an empty 'Stream' instance.
1178
1179        int len = str.length();
1180        if (isClosing || (len <= (tok.length() + 3))) return Stream.empty();
1181
1182        // Leaves off the opening 'token' and less-than '<' symbol  (leaves off "<DIV " for example)
1183        // Also leave off the "ending-forward-slash" (if there is one) and ending '>'
1184
1185        String  s = str.substring(tok.length() + 2, len - ((str.charAt(len - 2) == '/') ? 2 : 1));
1186
1187        // if all lower-case is requested, do that here.
1188        if (! preserveKeysCase) s = s.toLowerCase();
1189
1190        // java.util.regex.Pattern.split(CharSequence) is sort of an "inverse reg-ex" in that it 
1191        // returns all of the text that was present BETWEEN the matches 
1192        // NOTE: This is the "opposite of the matches, themselves)" - a.k.a. all the stuff that was
1193        //       left-out.
1194
1195        Stream.Builder<String> b = Stream.builder();
1196
1197        // 'split' => inverse-matches  (text between KEY-VALUE pairs)
1198        for (String unMatchedStr : AttrRegEx.KEY_VALUE_REGEX.split(s))
1199
1200            // Of that stuff, now do a white-space split for connected characters
1201            for (String keyWord : unMatchedStr.split("\\s+"))
1202
1203                // Call String.trim() and String.length()
1204                if ((keyWord = keyWord.trim()).length() > 0)
1205
1206                    // Check for valid Attribute-Name's only
1207                    if (AttrRegEx.ATTRIBUTE_KEY_REGEX_PRED.test(keyWord))
1208
1209                        // ... put it in the return stream.
1210                        // NOTE: This has the potential to slightly change the original HTML
1211                        //       It will "leave out any guck" that was in the Element
1212                        b.add(keyWord);
1213
1214        // Build the Stream<String>, and return;
1215        return b.build();
1216    }
1217
1218    /**
1219     * <EMBED CLASS='external-html' DATA-FILE-ID=TN_HAS_KOA_DESC>
1220     * <!-- @ExternalHTMLDocFiles({"@returns", "@desc", "keyOnlyAttribute",
1221     *      "@IllegalArgumentException"}) -->
1222     * @param keyOnlyAttribute <EMBED CLASS='external-html' DATA-FILE-ID=TN_HAS_KOA_KOA>
1223     * @return <EMBED CLASS='external-html' DATA-FILE-ID=TN_HAS_KOA_RET>
1224     * @throws IllegalArgumentException <EMBED CLASS='external-html' DATA-FILE-ID=TN_HAS_KOA_IAEX>
1225     * @see AttrRegEx#KEY_VALUE_REGEX
1226     */
1227    public boolean hasKeyOnlyAttribute(String keyOnlyAttribute)
1228    {
1229        // Closing TagNode's do not have attributes, return false immediately.
1230        if (this.isClosing) return false;
1231
1232        // ONLY CHECKS FOR WHITE-SPACE, *NOT* VALIDITY...
1233        if (StringParse.hasWhiteSpace(keyOnlyAttribute)) throw new IllegalArgumentException(
1234            "The attribute you have passed [" + keyOnlyAttribute + "] has white-space, " +
1235            "This is not allowed here, because the search routine splits on whitespace, and " +
1236            "therefore a match would never be found."
1237        );
1238
1239        // NOTE: TagNode's whose 'str' field is only longer than the token, itself, by 3 or less
1240        //       characters cannot have attributes.  In that case, just return false.
1241
1242        int len = str.length();
1243        if (len <= (tok.length() + 3)) return false;
1244
1245        // Leaves off the opening 'token' and less-than '<' symbol  (leaves off "<DIV " for example)
1246        // Also leave off the "ending-forward-slash" (if there is one), and edning '>'
1247
1248        String s = str.substring(tok.length() + 2, len - ((str.charAt(len - 2) == '/') ? 2 : 1));
1249
1250        // java.util.regex.Pattern.split(CharSequence) is sort of an "inverse reg-ex" in that it 
1251        // returns all of the text that was present BETWEEN the matches 
1252
1253        // 'split' => inverse-matches (text between KEY-VALUE pairs)
1254        for (String unMatchedStr : AttrRegEx.KEY_VALUE_REGEX.split(s))
1255
1256            // Of that stuff, now do a white-space split for connected characters
1257            for (String keyWord : unMatchedStr.split("\\s+"))
1258
1259                // trim, check-length...
1260                if ((keyWord = keyWord.trim()).length() > 0)
1261
1262                    if (keyOnlyAttribute.equalsIgnoreCase(keyWord)) return true;
1263
1264        // Was not found, return false;
1265        return false;
1266    }
1267
1268
1269    // ********************************************************************************************
1270    // ********************************************************************************************
1271    // testAV
1272    // ********************************************************************************************
1273    // ********************************************************************************************
1274
1275
1276    /**
1277     * Convenience Method.
1278     * <BR />Invokes: {@link #testAV(String, Predicate)}
1279     * <BR />Passes: {@code String.equals(attributeValue)} as the test-{@code Predicate}
1280     */
1281    public boolean testAV(String attributeName, String attributeValue)
1282    { return testAV(attributeName, (String s) -> s.equals(attributeValue)); }
1283
1284    /**
1285     * Convenience Method.
1286     * <BR />Invokes: {@link #testAV(String, Predicate)}
1287     * <BR />Passes: {@code attributeValueTest.asPredicate()}
1288     */
1289    public boolean testAV(String attributeName, Pattern attributeValueTest)
1290    { return testAV(attributeName, attributeValueTest.asPredicate()); }
1291
1292    /**
1293     * Convenience Method.
1294     * <BR />Invokes: {@link #testAV(String, Predicate)}
1295     * <BR />Passes: {@link TextComparitor#test(String, String[])} as the test-{@code Predicate}
1296     */
1297    public boolean testAV
1298        (String attributeName, TextComparitor attributeValueTester, String... compareStrs)
1299    { return testAV(attributeName, (String s) -> attributeValueTester.test(s, compareStrs)); }
1300
1301    /**
1302     * Test the <B STYLE="color: red;">value</B> of the inner-tag named {@code 'attributeName'}
1303     * (if that attribute exists, and has a non-empty value) using a provided
1304     * {@code Predicate<String>}.
1305     * 
1306     * <BR /><BR /><IMG SRC='doc-files/img/testAV1.png' CLASS=JDIMG ALT='example'>
1307     * 
1308     * @param attributeName Any String will suffice - but only valid attribute
1309     * <B STYLE="color: red;">names</B> will match the internal regular-expression.
1310     * 
1311     * <BR /><BR /><B>NOTE:</B> The validity of this parameter <I><B>is not</I></B> checked with
1312     * the HTML attribute-<B STYLE="color: red;">name</B> Regular-Expression exception checker.
1313     * 
1314     * @param attributeValueTest Any {@code java.util.function.Predicate<String>}
1315     * 
1316     * @return Method will return <B>TRUE</B> if and only if:
1317     * 
1318     * <BR /><BR /><UL CLASS="JDUL">
1319     * <LI> {@code 'this'} instance of {@code TagNode} has an inner-tag named
1320     *      {@code 'attributeName'}.
1321     *      <BR /><BR />
1322     * </LI>
1323     * <LI> The results of the provided {@code String-Predicate}, when applied against the
1324     *      <B STYLE="color: red">value</B> of the requested attribute, returns <B>TRUE</B>.
1325     * </LI>
1326     * </UL>
1327     * 
1328     * @see AttrRegEx#KEY_VALUE_REGEX
1329     * @see #str 
1330     * @see #isClosing
1331     * @see StringParse#ifQuotesStripQuotes(String)
1332     */
1333    public boolean testAV(String attributeName, Predicate<String> attributeValueTest)
1334    {
1335        // Closing TagNode's (</DIV>, </A>) cannot attributes, or attribute-values
1336        if (isClosing) return false;
1337
1338        // OPTIMIZATION: TagNode's whose String-length is less than this computed length 
1339        // are simply too short to have the attribute named by the input parameter
1340
1341        if (this.str.length() < (this.tok.length() + attributeName.length() + 4)) return false;
1342
1343        // This Reg-Ex will allow us to iterate through each attribute key-value pair
1344        // contained / 'inside' this instance of TagNode.
1345
1346        Matcher m = AttrRegEx.KEY_VALUE_REGEX.matcher(this.str);
1347
1348        // Test each attribute key-value pair, and return the test results if an attribute
1349        // whose name matches 'attributeName' is found.
1350
1351        while (m.find())
1352            if (m.group(2).equalsIgnoreCase(attributeName))
1353                return attributeValueTest.test
1354                    (StringParse.ifQuotesStripQuotes(m.group(3)));
1355
1356        // No attribute key-value pair was found whose 'key' matched input-parameter
1357        // 'attributeName'
1358
1359        return false;
1360    }
1361
1362
1363    // ********************************************************************************************
1364    // ********************************************************************************************
1365    // has-attribute boolean-logic methods
1366    // ********************************************************************************************
1367    // ********************************************************************************************
1368
1369
1370    /**
1371     * Convenience Method.
1372     * <BR />Invokes: {@link #hasLogicOp(boolean, IntFunction, IntPredicate, String[])}
1373     * <BR />Passes: AND Boolean Logic
1374     */
1375    public boolean hasAND(boolean checkAttributeStringsForErrors, String... attributes)
1376    {
1377        // First-Function:  Tells the logic to *IGNORE* intermediate matches (returns NULL)
1378        //                  (This is *AND*, so wait until all attributes have been found, or at
1379        //                  the very least all tags in the element tested, and failed.
1380        //
1381        // Second-Function: At the End of the Loops, all Attributes have either been found, or
1382        //                  at least all attributes in 'this' tag have been tested.  Note that the
1383        //                  first-function is only called on a MATCH, and tht 'AND' requires to
1384        //                  defer a response until all attributes have been tested..  Here, simply
1385        //                  RETURN WHETHER OR NOT the MATCH-COUNT equals the number of matches in
1386        //                  the user-provided String-array.
1387
1388        return hasLogicOp(
1389            checkAttributeStringsForErrors,
1390            (int matchCount) -> null,
1391            (int matchCount) -> (matchCount == attributes.length),
1392            attributes
1393        );
1394    }
1395
1396    /**
1397     * Convenience Method.
1398     * <BR />Invokes: {@link #hasLogicOp(boolean, IntFunction, IntPredicate, String[])}
1399     * <BR />Passes: OR Boolean Logic
1400     * <!-- NOT USED NOW:
1401     *  <BR /><BR /><IMG SRC='doc-files/img/hasOR.png' CLASS=JDIMG ALT="Example"> -->
1402     */
1403    public boolean hasOR(boolean checkAttributeStringsForErrors, String... attributes)
1404    {
1405        // First-Function:  Tells the logic to return TRUE on any match IMMEDIATELY
1406        //
1407        // Second-Function: At the End of the Loops, all Attributes have been tested.  SINCE the
1408        //                  previous function returns on match immediately, AND SINCE this is an
1409        //                  OR, therefore FALSE must be returned (since there were no matches!)
1410
1411        return hasLogicOp(
1412            checkAttributeStringsForErrors,
1413            (int matchCount) -> true,
1414            (int matchCount) -> false,
1415            attributes
1416        );
1417    }
1418
1419    /**
1420     * Convenience Method.
1421     * <BR />Invokes: {@link #hasLogicOp(boolean, IntFunction, IntPredicate, String[])}
1422     * <BR />Passes: NAND Boolean Logic
1423     */
1424    public boolean hasNAND(boolean checkAttributeStringsForErrors, String... attributes)
1425    {
1426        // First-Function: Tells the logic to return FALSE on any match IMMEDIATELY
1427        //
1428        // Second-Function: At the End of the Loops, all Attributes have been tested.  SINCE
1429        //                  the previous function returns on match immediately, AND SINCE this is
1430        //                  a NAND, therefore TRUE must be returned (since there were no matches!)
1431
1432        return hasLogicOp(
1433            checkAttributeStringsForErrors,
1434            (int matchCount) -> false,
1435            (int matchCount) -> true,
1436            attributes
1437        );
1438    }
1439
1440    /**
1441     * Convenience Method.
1442     * <BR />Invokes: {@link #hasLogicOp(boolean, IntFunction, IntPredicate, String[])}
1443     * <BR />Passes: XOR Boolean Logic
1444     */
1445    public boolean hasXOR(boolean checkAttributeStringsForErrors, String... attributes)
1446    {
1447        // First-Function: Tells the logic to IGNORE the FIRST MATCH, and any matches afterwards
1448        //                 should produce a FALSE result immediately
1449        //                 (XOR means ==> one-and-only-one)
1450        //
1451        // Second-Function: At the End of the Loops, all Attributes have been tested.  Just
1452        //                  return whether or not the match-count is PRECISELY ONE.
1453
1454        return hasLogicOp(
1455            checkAttributeStringsForErrors,
1456            (int matchCount) -> (matchCount == 1) ? null : false,
1457            (int matchCount) -> (matchCount == 1),
1458            attributes
1459        );
1460    }
1461
1462    /**
1463     * Provides the Logic for methods:
1464     * 
1465     * <BR ><TABLE CLASS=BRIEFTABLE>
1466     * <TR><TH>Boolean Evaluation</TH><TH>Method</TH></TR>
1467     * <TR>
1468     *  <TD>Checks that <B STYLE='color: red;'><I>all</I></B> Attributes are found</TD>
1469     *  <TD>{@link #hasAND(boolean, String[])}</TD>
1470     * </TR>
1471     * <TR>
1472     *  <TD>Checks that <B STYLE='color: red;'><I>at least one</I></B> Attribute matches</TD>
1473     *  <TD>{@link #hasOR(boolean, String[])}</TD>
1474     * </TR>
1475     * <TR>
1476     *  <TD>Checks that <B STYLE='color: red;'><I>precisely-one</I></B> Attribute is found</TD>
1477     *  <TD>{@link #hasXOR(boolean, String[])}</TD>
1478     * </TR>
1479     * <TR>
1480     *  <TD>Checks that <B STYLE='color: red;'><I>none</I></B> of the Attributes match</TD>
1481     *  <TD>{@link #hasNAND(boolean, String[])}</TD>
1482     * </TR>
1483     * </TABLE>
1484     * 
1485     * <BR /><IMG SRC='doc-files/img/hasAND.png' CLASS=JDIMG ALT="Example">
1486     * 
1487     * @param attributes This is a list of HTML Element Attribute-<B STYLE="color: red;">Names</B> 
1488     * or "Inner Tags" as they are called in this Search and Scrape Package.
1489     * 
1490     * @param checkAttributeStringsForErrors <EMBED CLASS="external-html"
1491     *      DATA-FILE-ID=TAGNODE_HAS_BOOL>
1492     * 
1493     * @return <B>TRUE</B> if at least one of these attribute-<B STYLE="color: red;">names</B> are
1494     * present in {@code 'this'} instance, and <B>FALSE</B> otherwise.
1495     * 
1496     * <BR /><BR /><B>NOTE:</B> If this method is passed a zero-length {@code String}-array to the
1497     * {@code 'attributes'} parameter, this method shall exit immediately and return <B>FALSE</B>.
1498     * 
1499     * @throws InnerTagKeyException If any of the {@code 'attributes'} are not valid HTML
1500     * attributes, <I><B>and</B></I> the user has passed <B>TRUE</B> to parameter
1501     * {@code checkAttributeStringsForErrors}.
1502     * 
1503     * @throws NullPointerException If any of the {@code 'attributes'} are null.
1504     * 
1505     * @throws ClosingTagNodeException <EMBED CLASS="external-html" DATA-FILE-ID="CTNEX">
1506     * 
1507     * @throws IllegalArgumentException If the {@code 'attributes'} parameter has length zero.
1508     * 
1509     * @see InnerTagKeyException#check(String[])
1510     * @see #AV(String)
1511     * @see AttrRegEx#KEY_VALUE_REGEX
1512     */
1513    protected boolean hasLogicOp(
1514            boolean checkAttributeStringsForErrors, IntFunction<Boolean> onMatchFunction,
1515            IntPredicate reachedEndFunction, String... attributes
1516        )
1517    {
1518        ClosingTagNodeException.check(this);
1519
1520        // Keep a tally of how many of the input attributes are found
1521        int matchCount = 0;
1522
1523        // Don't clobber the user's input
1524        attributes = attributes.clone();
1525
1526        // If no attributes are passed to 'attributes' parameter, throw exception.
1527        if (attributes.length == 0) throw new IllegalArgumentException
1528            ("Input variable-length String[] array parameter, 'attributes', has length zero.");
1529
1530        // OPTIMIZATION: TagNode's whose String-length is less than this computed length 
1531        // are simply too short to have any attribute-value pairs.
1532        // 4 (characters) are: '<', '>', ' ' and 'X'
1533        // SHORTEST POSSIBLE SUCH-ELEMENT: <DIV X>
1534        //
1535        // This TagNode doesn't have any attributes in it.
1536        // There is no need to check anything, so return FALSE immediately ("OR" fails)
1537
1538        if (this.str.length() < (this.tok.length() + 4)) return false;
1539
1540        if (checkAttributeStringsForErrors) InnerTagKeyException.check(attributes);
1541
1542
1543        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
1544        // Check the main key=value attributes
1545        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
1546
1547        // Get all inner-tag key-value pairs.  If even one of these is inside the 'attributes'
1548        // input-parameter string-array,  Then we must return true, since this is OR
1549
1550        Matcher keyValueInnerTags = AttrRegEx.KEY_VALUE_REGEX.matcher(this.str);
1551
1552        while (keyValueInnerTags.find())
1553        {
1554            // Retrieve the key of the key-value pair
1555            String innerTagKey = keyValueInnerTags.group(2);                   
1556
1557            // Iterate every element of the String[] 'attributes' parameter
1558            SECOND_FROM_TOP:
1559            for (int i=0; i < attributes.length; i++)
1560
1561                // No need to check attributes that have already been matched.
1562                // When an attribute matches, it's place in the array is set to null
1563                if (attributes[i] != null)
1564
1565                    // Does the latest keyOnlyInnerTag match one of the user-requested
1566                    // attribute names?
1567                    if (innerTagKey.equalsIgnoreCase(attributes[i]))
1568                    {
1569                        // NAND & OR will exit immediately on a match.  XOR and AND
1570                        // will return 'null' meaning they are not sure yet.
1571
1572                        Boolean whatToDo = onMatchFunction.apply(++matchCount);
1573
1574                        if (whatToDo != null) return whatToDo;
1575
1576                        // Increment the matchCounter, if this ever reaches the length
1577                        // of the input array, there is no need to continue with the loop
1578
1579                        if (matchCount == attributes.length)
1580                            return reachedEndFunction.test(matchCount); 
1581
1582                        // There are still more matches to be found (not every element in
1583                        // this array is null yet)... Keep Searching, but eliminated the
1584                        // recently identified attribute from the list, because it has
1585                        // already been found.
1586
1587                        attributes[i] = null;
1588                        continue SECOND_FROM_TOP;
1589                    }
1590        }
1591
1592
1593        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
1594        // Check the main key-only (Boolean) Attributes
1595        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
1596        //
1597        // Also check the "Boolean Attributes" also known as the "Key-Word Only Attributes"
1598        // Use the "Inverse Reg-Ex Matcher" (which matches all the strings that are "between" the
1599        // real matches)
1600
1601        // substring eliminates the leading "<TOK ..." and the trailing '>' character
1602        // 2: '<' character *PLUS* the space (' ') character
1603        String strToSplit = this.str.substring(
1604            2 + tok.length(),
1605            this.str.length() - ((str.charAt(this.str.length() - 2) == '/') ? 2 : 1)
1606        ).trim();
1607
1608        // 'split' => inverse-matches  (text between KEY-VALUE pairs)
1609        for (String unMatchedStr : AttrRegEx.KEY_VALUE_REGEX.split(strToSplit))  
1610
1611            // Of that stuff, now do a white-space split for connected characters
1612            SECOND_FROM_TOP:
1613            for (String keyOnlyInnerTag : unMatchedStr.split("\\s+"))          
1614
1615                // Just-In-Case, usually not necessary
1616                if ((keyOnlyInnerTag = keyOnlyInnerTag.trim()).length() > 0)
1617
1618                    // Iterate all the input-parameter String-array attributes
1619                    for (int i=0; i < attributes.length; i++)
1620
1621                        // No need to check attributes that have already been matched.
1622                        // When an attribute matches, it's place in the array is set to null
1623                        if (attributes[i] != null)
1624
1625                            // Does the latest keyOnlyInnerTag match one of the user-requested
1626                            // attribute names?
1627                            if (keyOnlyInnerTag.equalsIgnoreCase(attributes[i]))
1628                            {
1629                                // NAND & OR will exit immediately on a match.  XOR and AND
1630                                // will return 'null' meaning they are not sure yet.
1631
1632                                Boolean whatToDo = onMatchFunction.apply(++matchCount);
1633
1634                                if (whatToDo != null) return whatToDo;
1635        
1636                                // Increment the matchCounter, if this ever reaches the length
1637                                // of the input array, there is no need to continue with the loop
1638        
1639                                if (matchCount == attributes.length)
1640                                    return reachedEndFunction.test(matchCount); 
1641
1642                                // There are still more matches to be found (not every element in
1643                                // this array is null yet)... Keep Searching, but eliminated the
1644                                // recently identified attribute from the list, because it has
1645                                // already been found.
1646
1647                                attributes[i] = null;
1648                                continue SECOND_FROM_TOP;
1649                            }
1650
1651        // Let them know how many matches there were
1652        return reachedEndFunction.test(matchCount);
1653    }
1654
1655
1656    // ********************************************************************************************
1657    // ********************************************************************************************
1658    // has methods - extended, variable attribute-names
1659    // ********************************************************************************************
1660    // ********************************************************************************************
1661
1662
1663    /**
1664     * Convenience Method.
1665     * <BR />Invokes: {@link #has(Predicate)}
1666     * <BR />Passes: {@code String.equalsIgnoreCase(attributeName)} as the test-{@code Predicate}
1667     */
1668    public boolean has(String attributeName)
1669    { return has((String s) -> s.equalsIgnoreCase(attributeName)); }
1670
1671    /**
1672     * Convenience Method.
1673     * <BR />Invokes: {@link #has(Predicate)}
1674     * <BR />Passes: {@code Pattern.asPredicate()}
1675     */
1676    public boolean has(Pattern attributeNameRegExTest)
1677    { return has(attributeNameRegExTest.asPredicate()); }
1678
1679    /**
1680     * Convenience Method.
1681     * <BR />Invokes: {@link #has(Predicate)}
1682     * <BR />Passes: {@link TextComparitor#test(String, String[])} as the test-{@code Predicate}
1683     */
1684    public boolean has(TextComparitor tc, String... compareStrs)
1685    { return has((String s) -> tc.test(s, compareStrs)); }
1686
1687    /**
1688     * <EMBED CLASS='external-html' DATA-FILE-ID=TN_HAS_DESC2>
1689     * <EMBED CLASS='external-html' DATA-FILE-ID=TAGNODE_HAS_NOTE>
1690     * @param attributeNameTest <EMBED CLASS='external-html' DATA-FILE-ID=TN_HAS_ANT>
1691     * @return <EMBED CLASS='external-html' DATA-FILE-ID=TN_HAS_RET2>
1692     * @see AttrRegEx#KEY_VALUE_REGEX
1693     * @see StrFilter
1694     */
1695    public boolean has(Predicate<String> attributeNameTest)
1696    {
1697        // Closing HTML Elements may not have attribute-names or values.
1698        // Exit gracefully, and immediately.
1699
1700        if (this.isClosing) return false;
1701
1702        // OPTIMIZATION: TagNode's whose String-length is less than this computed length 
1703        // are simply too short to have such an attribute-value pair.
1704        // 4 (characters) are: '<', '>', ' ' and 'X'
1705        // SHORTEST POSSIBLE SUCH-ELEMENT: <DIV X>
1706
1707        if (this.str.length() < (this.tok.length() + 4)) return false;
1708
1709        // Get all inner-tag key-value pairs.  If any of them match with the 'attributeNameTest'
1710        // Predicate, return TRUE immediately.
1711        Matcher keyValueInnerTags = AttrRegEx.KEY_VALUE_REGEX.matcher(this.str);
1712
1713        // the matcher.group(2) has the key (not the value)
1714        while (keyValueInnerTags.find())
1715            if (attributeNameTest.test(keyValueInnerTags.group(2)))
1716                return true;
1717
1718        // Also check the "Boolean Attributes" also known as the "Key-Word Only Attributes"
1719        // Use the "Inverse Reg-Ex Matcher" (which matches all the strings that are "between" the
1720        // real matches)
1721
1722        // 'split' => inverse-matches  (text between KEY-VALUE pairs)
1723        for (String unMatchedStr : AttrRegEx.KEY_VALUE_REGEX.split(this.str))  
1724
1725            // Of that stuff, now do a white-space split for connected characters
1726            for (String keyOnlyInnerTag : unMatchedStr.split("\\s+"))          
1727
1728                // Just-In-Case, usually not necessary
1729                if ((keyOnlyInnerTag = keyOnlyInnerTag.trim()).length() > 0)   
1730
1731                    if (attributeNameTest.test(keyOnlyInnerTag))
1732                        return true;
1733
1734        // A match was not found in either the "key-value pairs", or the boolean "key-only list."
1735        return false;
1736    }
1737
1738
1739    // ********************************************************************************************
1740    // ********************************************************************************************
1741    // hasValue(...) methods
1742    // ********************************************************************************************
1743    // ********************************************************************************************
1744
1745
1746    /**
1747     * Convenience Method.
1748     * <BR />Invokes: {@link #hasValue(Predicate, boolean, boolean)}
1749     * <BR />Passes: {@code String.equals(attributeValue)} as the test-{@code Predicate}
1750     */
1751    public Map.Entry<String, String> hasValue
1752        (String attributeValue, boolean retainQuotes, boolean preserveKeysCase)
1753    { return hasValue((String s) -> attributeValue.equals(s), retainQuotes, preserveKeysCase); }
1754
1755    /**
1756     * Convenience Method.
1757     * <BR />Invokes: {@link #hasValue(Predicate, boolean, boolean)}
1758     * <BR />Passes: {@code attributeValueRegExTest.asPredicate()}
1759     */
1760    public Map.Entry<String, String> hasValue
1761        (Pattern attributeValueRegExTest, boolean retainQuotes, boolean preserveKeysCase)
1762    { return hasValue(attributeValueRegExTest.asPredicate(), retainQuotes, preserveKeysCase); }
1763
1764    /**
1765     * Convenience Method.
1766     * <BR />Invokes: {@link #hasValue(Predicate, boolean, boolean)}
1767     * <BR />Passes: {@link TextComparitor#test(String, String[])} as the test-{@code Predicate}
1768     */
1769    public Map.Entry<String, String> hasValue(
1770            boolean retainQuotes, boolean preserveKeysCase, TextComparitor attributeValueTester,
1771            String... compareStrs
1772        )
1773    {
1774        return hasValue(
1775            (String s) -> attributeValueTester.test(s, compareStrs), retainQuotes,
1776            preserveKeysCase
1777        );
1778    }
1779
1780    /**
1781     * <EMBED CLASS="external-html" DATA-FILE-ID=TN_HASVAL_DESC2>
1782     * <EMBED CLASS="external-html" DATA-FILE-ID=TN_HASVAL_DNOTE>
1783     * @param attributeValueTest <EMBED CLASS='external-html' DATA-FILE-ID=TN_HASVAL_AVT>
1784     * @param retainQuotes <EMBED CLASS='external-html' DATA-FILE-ID=TN_HASVAL_RQ>
1785     * @param preserveKeysCase 
1786     * <EMBED CLASS="external-html" DATA-FILE-ID=TN_HASVAL_PKC>
1787     * <EMBED CLASS="external-html" DATA-FILE-ID=TAGNODE_PRESERVE_C>
1788     * @return <EMBED CLASS="external-html" DATA-FILE-ID=TN_HASVAL_RET2>
1789     * @see AttrRegEx#KEY_VALUE_REGEX
1790     * @see StrFilter
1791     */
1792    public Map.Entry<String, String> hasValue
1793        (Predicate<String> attributeValueTest, boolean retainQuotes, boolean preserveKeysCase)
1794    {
1795        // Closing HTML Elements may not have attribute-names or values.
1796        // Exit gracefully, and immediately.
1797
1798        if (this.isClosing) return null;
1799
1800        // OPTIMIZATION: TagNode's whose String-length is less than this computed length 
1801        // are simply too short to have such an attribute-value pair.
1802        // 5 (characters) are: '<', '>', ' ', 'X' and '=' 
1803        // SHORTEST POSSIBLE SUCH-ELEMENT: <DIV X=>
1804
1805        if (this.str.length() < (this.tok.length() + 5)) return null;
1806
1807        // Get all inner-tag key-value pairs.  If any are 'equal' to parameter attributeName,
1808        // return TRUE immediately.
1809
1810        Matcher keyValueInnerTags = AttrRegEx.KEY_VALUE_REGEX.matcher(this.str);
1811
1812        while (keyValueInnerTags.find())
1813        {
1814            // Matcher.group(3) has the key's value, of the inner-tag key-value pair
1815            // (matcher.group(2) has the key's name)
1816
1817            String foundAttributeValue = keyValueInnerTags.group(3);
1818
1819            // The comparison must be performed on the version of the value that DOES NOT HAVE the
1820            // surrounding quotation-marks
1821
1822            String foundAttributeValueNoQuotes =
1823                StringParse.ifQuotesStripQuotes(foundAttributeValue);
1824
1825            // Matcher.group(3) has the key-value, make sure to remove quotation marks (if present)
1826            // before comparing.
1827
1828            if (attributeValueTest.test(foundAttributeValueNoQuotes))
1829
1830                // matcher.group(2) has the key's name, not the value.  This is returned via the
1831                // Map.Entry key
1832
1833                return retainQuotes
1834
1835                    ? new AbstractMap.SimpleImmutableEntry<>(
1836                        preserveKeysCase
1837                            ? keyValueInnerTags.group(2)
1838                            : keyValueInnerTags.group(2).toLowerCase(),
1839                        foundAttributeValue
1840                    )
1841
1842                    : new AbstractMap.SimpleImmutableEntry<>(
1843                        preserveKeysCase
1844                            ? keyValueInnerTags.group(2)
1845                            : keyValueInnerTags.group(2).toLowerCase(),
1846                        foundAttributeValueNoQuotes
1847                    );
1848        }
1849
1850        // No match was identified, return null.
1851        return null;
1852    }
1853
1854
1855    // ********************************************************************************************
1856    // ********************************************************************************************
1857    // getInstance()
1858    // ********************************************************************************************
1859    // ********************************************************************************************
1860
1861
1862    /**
1863     * <EMBED CLASS='external-html' DATA-FILE-ID=TN_GETINST_DESC>
1864     * @param tok Any valid HTML tag.
1865     * @param openOrClosed <EMBED CLASS='external-html' DATA-FILE-ID=TN_GETINST_OOC>
1866     * @return An instance of this class
1867     * 
1868     * @throws IllegalArgumentException If parameter {@code TC openOrClose} is {@code null} or
1869     * {@code TC.Both}
1870     * 
1871     * @throws HTMLTokException If the parameter {@code String tok} is not a valid HTML-tag
1872     * 
1873     * @throws SingletonException If the token requested is a {@code singleton} (self-closing) tag,
1874     * but the Tag-Criteria {@code 'TC'} parameter is requesting a closing-version of the tag.
1875     * 
1876     * @see HTMLTags#hasTag(String, TC)
1877     * @see HTMLTags#isSingleton(String)
1878     */
1879    public static TagNode getInstance(String tok, TC openOrClosed)
1880    {
1881        if (openOrClosed == null)
1882            throw new NullPointerException("The value of openOrClosed cannot be null.");
1883
1884        if (openOrClosed == TC.Both)
1885            throw new IllegalArgumentException("The value of openOrClosed cannot be TC.Both.");
1886
1887        if (HTMLTags.isSingleton(tok) && (openOrClosed == TC.ClosingTags))
1888
1889            throw new SingletonException(
1890                "The value of openOrClosed is TC.ClosingTags, but unfortunately you have asked " +
1891                "for a [" + tok + "] HTML element, which is a singleton element, and therefore " +
1892                "cannot have a closing-tag instance."
1893            );
1894
1895        TagNode ret = HTMLTags.hasTag(tok, openOrClosed);
1896
1897        if (ret == null)
1898            throw new HTMLTokException
1899                ("The HTML-Tag provided isn't valid!\ntok: " + tok + "\nTC: " + openOrClosed);
1900
1901        return ret;
1902    }
1903
1904
1905    // ********************************************************************************************
1906    // ********************************************************************************************
1907    // Methods for "CSS Classes"
1908    // ********************************************************************************************
1909    // ********************************************************************************************
1910
1911
1912    /**
1913     * Convenience Method.
1914     * <BR />Invokes: {@link #cssClasses()}
1915     * <BR />Catches-Exception
1916     */
1917    public Stream<String> cssClassesNOCSE()
1918    { try { return cssClasses(); } catch (CSSStrException e) { return Stream.empty(); } }
1919
1920    /**
1921     * <EMBED CLASS="external-html" DATA-FILE-ID=TN_CSS_CL_DESC>
1922     * @return 
1923     * <EMBED CLASS="external-html" DATA-FILE-ID=TN_CSS_CL_RET>
1924     * <EMBED CLASS="external-html" DATA-FILE-ID=STRMCNVT>
1925     * @throws CSSStrException <EMBED CLASS="external-html" DATA-FILE-ID=TN_CSS_CL_CSSSE>
1926     * @see #cssClasses()
1927     * @see #AV(String)
1928     * @see StringParse#WHITE_SPACE_REGEX
1929     * @see CSSStrException#check(Stream)
1930     */
1931    public Stream<String> cssClasses()
1932    {
1933        // The CSS Class is just an attribute/inner-tag within an HTML Element.
1934        String classes = AV("class"); 
1935
1936        // IF the "class" attribute was not present, OR (after trimming) was empty, return
1937        // "empty stream"
1938
1939        if ((classes == null) || ((classes = classes.trim()).length() == 0))
1940            return Stream.empty();
1941
1942        // STEP 1: Split the string on white-space
1943        // STEP 2: Check each element of the output Stream using the "CSSStrException Checker"
1944
1945        return CSSStrException.check(StringParse.WHITE_SPACE_REGEX.splitAsStream(classes));
1946    }
1947
1948    /**
1949     * <EMBED CLASS='external-html' DATA-FILE-ID=TN_SET_CSS_CL_DESC>
1950     * @param quote <EMBED CLASS='external-html' DATA-FILE-ID=TGND_QUOTE_EXPL>
1951     * @param appendOrClobber <EMBED CLASS='external-html' DATA-FILE-ID=TN_SET_CSS_CL_AOC>
1952     * @param cssClasses <EMBED CLASS='external-html' DATA-FILE-ID=TN_SET_CSS_CL_CCL>
1953     * @return <EMBED CLASS='external-html' DATA-FILE-ID=TN_SET_CSS_CL_RET>
1954     * @throws CSSStrException This exception shall throw if any of the {@code 'cssClasses'} in the
1955     * var-args {@code String...} parameter do not meet the HTML 5 CSS {@code Class} naming rules.
1956     * @throws ClosingTagNodeException <EMBED CLASS="external-html" DATA-FILE-ID=CTNEX>
1957     * @throws QuotesException <EMBED CLASS='external-html' DATA-FILE-ID=TN_SET_CSS_CL_QEX>
1958     * @see CSSStrException#check(String[])
1959     * @see CSSStrException#VALID_CSS_CLASS_OR_NAME_TOKEN
1960     * @see #appendToAV(String, String, boolean, SD)
1961     * @see #setAV(String, String, SD)
1962     */
1963    public TagNode setCSSClasses(SD quote, boolean appendOrClobber, String... cssClasses)
1964    {
1965        // Throw CSSStrException if any of the input strings are invalid CSS Class-Names.
1966        CSSStrException.check(cssClasses);
1967
1968        // Build the CSS 'class' Attribute String.  This will be inserted into the TagNode Element
1969        StringBuilder   sb      = new StringBuilder();
1970        boolean         first   = true;
1971
1972        for (String c : cssClasses) 
1973            { sb.append((first ? "" : " ") + c.trim()); first=false; }
1974
1975        return appendOrClobber
1976            ? appendToAV("class", " " + sb.toString(), false, quote)
1977            : setAV("class", sb.toString(), quote);
1978    }
1979
1980    /**
1981     * <EMBED CLASS='external-html' DATA-FILE-ID=TN_APD_CSS_CL_DESC>
1982     * @param cssClass This is the CSS-{@code Class} name that is being inserted into
1983     * {@code 'this'} instance of {@code TagNode}
1984     * @param quote <EMBED CLASS='external-html' DATA-FILE-ID=TGND_QUOTE_EXPL>
1985     * @return A newly instantiated {@code TagNode} with updated CSS {@code Class} Name(s).
1986     * @throws CSSStrException <EMBED CLASS='external-html' DATA-FILE-ID=TN_APD_CSS_CL_CSSSE>
1987     * @throws ClosingTagNodeException <EMBED CLASS="external-html" DATA-FILE-ID=CTNEX>
1988     * @throws QuotesException <EMBED CLASS='external-html' DATA-FILE-ID=TN_APD_CSS_CL_QEX>
1989     * @see CSSStrException#check(String[])
1990     * @see #setAV(String, String, SD)
1991     * @see #appendToAV(String, String, boolean, SD)
1992     */
1993    public TagNode appendCSSClass(String cssClass, SD quote)
1994    {
1995        // Do a validity check on the class.  If this is "problematic" due to use of specialized / 
1996        // pre-processed CSS Class directives, use the general purpose "setAV(...)" method
1997
1998        CSSStrException.check(cssClass);
1999
2000        String curCSSClassSetting = AV("class");
2001
2002        // If there wasn't a CSS Class already defined, use "setAV(...)", 
2003        // otherwise use "appendToAV(...)"
2004
2005        if ((curCSSClassSetting == null) || (curCSSClassSetting.length() == 0))
2006            return setAV("class", cssClass, quote);
2007
2008        else
2009            return appendToAV("class", cssClass + ' ', true, quote);
2010    }
2011
2012
2013    // ********************************************************************************************
2014    // ********************************************************************************************
2015    // Methods for "CSS Style"
2016    // ********************************************************************************************
2017    // ********************************************************************************************
2018
2019
2020    /**
2021     * <EMBED CLASS="external-html" DATA-FILE-ID=TN_CSS_STYLE_DESC>
2022     * <EMBED CLASS="external-html" DATA-FILE-ID=TN_CSS_STYLE_DESCEX>
2023     * @return <EMBED CLASS="external-html" DATA-FILE-ID=TN_CSS_STYLE_RET>
2024     * @see AttrRegEx#CSS_INLINE_STYLE_REGEX
2025     * @see #AV(String)
2026     */
2027    public Properties cssStyle()
2028    {
2029        Properties  p           = new Properties();
2030        String      styleStr    = AV("style");
2031            // Returns the complete attribute-value of "style" in the TagNode
2032
2033        // There was no STYLE='...' attribute found, return empty Properties.
2034        if (styleStr == null) return p;
2035
2036        // Standard String-trim routine
2037        styleStr = styleStr.trim();
2038
2039        if (styleStr.length() == 0) return p;
2040
2041        // This reg-ex "iterates" over matches of strings that follow the (very basic) form:
2042        // declaration-name: declaration-string;
2043        //
2044        // Where the ":" (color), and ";" (semi-colon) are the only watched/monitored tokens.
2045        // See the reg-ex definition in "See Also" tag.
2046
2047        Matcher m = AttrRegEx.CSS_INLINE_STYLE_REGEX.matcher(styleStr);
2048
2049        // m.group(1): declaration-name     (stuff before the ":")
2050        // m.group(2): declaration-string   (stuff before the next ";", or end-of-string)
2051        // 
2052        // For Example, if the style attribute-value was specified as:
2053        // STYLE="font-weight: bold;   margin: 1em 1em 1em 2em;   color: #0000FF"
2054        //
2055        // The returned Properties object would contain the string key-value pair elements:
2056        // "font-weight"    -> "bold"
2057        // "margin"         -> "1em 1em 1em 2em"
2058        // "color"          -> "#0000FF"
2059
2060        while (m.find()) p.put(m.group(1).toLowerCase(), m.group(2));
2061
2062        return p;
2063    }
2064
2065    /**
2066     * <EMBED CLASS='external-html' DATA-FILE-ID=TN_SET_CSS_STY_DESC>
2067     * @param p <EMBED CLASS='external-html' DATA-FILE-ID=TN_SET_CSS_STY_P>
2068     * @param quote <EMBED CLASS='external-html' DATA-FILE-ID=TGND_QUOTE_EXPL>
2069     * @param appendOrClobber <EMBED CLASS='external-html' DATA-FILE-ID=TN_SET_CSS_STY_AOC>
2070     * @return <EMBED CLASS='external-html' DATA-FILE-ID=TN_SET_CSS_STY_RET>
2071     * @throws ClosingTagNodeException <EMBED CLASS="external-html" DATA-FILE-ID=CTNEX>
2072     * @throws CSSStrException If there is an invalid CSS Style Property Name.
2073     * @throws QuotesException If the style-element's quotation marks are incompatible with any
2074     * and all quotation marks employed by the style-element definitions.
2075     * @see CSSStrException#VALID_CSS_CLASS_OR_NAME_TOKEN
2076     * @see #appendToAV(String, String, boolean, SD)
2077     * @see #setAV(String, String, SD)
2078     */
2079    public TagNode setCSSStyle(Properties p, SD quote, boolean appendOrClobber)
2080    {
2081        // this is used in the "exception constructor" below (which means, it might not be used).
2082        int counter = 0;
2083
2084        // Follows the "FAIL-FAST" philosophy, and does not allow invalid CSS declaration-name
2085        // tokens.  Use TagNode.setAV("style", ...), or TagNode.appendToAV("style", ...), to
2086        // bypass exception-check.
2087
2088        for (String key : p.stringPropertyNames())
2089
2090            if (! CSSStrException.VALID_CSS_CLASS_OR_NAME_TOKEN_PRED.test(key))
2091            {
2092                String[] keyArr = new String[p.size()];
2093
2094                throw new CSSStrException(
2095
2096                    "CSS Style Definition Property: [" + key + "] does not conform to the " +
2097                    "valid, HTML 5, regular-expression for CSS Style Definitions Properties:\n[" +
2098                    CSSStrException.VALID_CSS_CLASS_OR_NAME_TOKEN.pattern() + "].",
2099
2100                    // One minor "PRESUMPTION" that is the Iterator will return the elements of 
2101                    // Properties in the EXACT SAME ORDER on both creations / instantiations of the
2102                    // iterator.  Specifically: two invocations of method .iterator(), will return
2103                    // the same-list of property-keys, in the same order, BOTH TIMES.  Once for the
2104                    // for-loop, and once for the exception message.  This only matters if there is
2105                    // an exception.
2106
2107                    p.stringPropertyNames().toArray(keyArr),
2108                    ++counter
2109                );
2110            }
2111            else ++counter; 
2112
2113        // Follows the "FAIL-FAST" philosophy, and does not allow "quotes-within-quote" problems
2114        // to occur.  An attribute-value surrounded by single-quotes, cannot contain a
2115        // single-quote inside, and double-within-double.
2116
2117        counter = 0;
2118
2119        for (String key : p.stringPropertyNames())
2120
2121            if (StrCmpr.containsOR(p.get(key).toString(), ("" + quote.quote), ";"))
2122            {
2123                String[] keyArr = new String[p.size()];
2124
2125                throw new CSSStrException(
2126                    "CSS Style Definition Property: [" + key + "], which maps to style-" +
2127                    "definition property-value:\n[" + p.get(key) + "], contains either a " +
2128                    "semi-colon ';' character, or the same quotation-mark specified: [" + 
2129                    quote.quote + "], and is therefore not a valid assignment for a CSS " +
2130                    "Definition Property.",
2131
2132                    p   .stringPropertyNames()
2133                        .stream()
2134                        .map((String propertyName) -> p.get(propertyName))
2135                        .toArray((int i) -> new String[i]),
2136
2137                    ++counter
2138                );
2139            }
2140            else ++counter;
2141
2142        // ERROR-CHECKING: FINISHED, NOW JUST BUILD THE ATTRIBUTE-VALUE STRING
2143        // (using StringBuilder), AND INSERT IT.
2144
2145        StringBuilder sb = new StringBuilder();
2146
2147        for (String key : p.stringPropertyNames()) sb.append(key + ": " + p.get(key) + ";");
2148
2149        return appendOrClobber
2150            ? appendToAV("style", " " + sb.toString(), false, quote)
2151            : setAV("style", sb.toString(), quote);
2152    }
2153
2154
2155    // ********************************************************************************************
2156    // ********************************************************************************************
2157    // Methods for "CSS ID"
2158    // ********************************************************************************************
2159    // ********************************************************************************************
2160
2161
2162    /**
2163     * Convenience Method.
2164     * <BR />Invokes: {@link #AV(String)}
2165     * <BR />Passes: {@code String "id"}, the CSS-ID attribute-<B STYLE="color: red;">name</B>
2166     * <!-- RIDICULOUS: BR />BR />IMG SRC='doc-files/img/getID.png' CLASS=JDIMG ALT='example'> -->
2167     */
2168    public String getID()
2169    {
2170        String id = AV("ID");
2171        return (id == null) ? null : id.trim();
2172    }
2173
2174    /**
2175     * This merely sets the current CSS {@code 'ID'} Attribute <B STYLE="color: red;">Value</B>.
2176     *
2177     * @param id This is the new CSS {@code 'ID'} attribute-<B STYLE="color: red;">value</B> that
2178     * the user would like applied to {@code 'this'} instance of {@code TagNode}.
2179     * 
2180     * @param quote <EMBED CLASS='external-html' DATA-FILE-ID=TGND_QUOTE_EXPL>
2181     *
2182     * @return Returns a new instance of {@code TagNode} that has an updated {@code 'ID'} 
2183     * attribute-<B STYLE="color: red;">value</B>.
2184     * 
2185     * @throws IllegalArgumentException This exception shall throw if an invalid
2186     * {@code String}-token has been passed to parameter {@code 'id'}.
2187     *
2188     * <BR /><BR /><B>BYPASS NOTE:</B> If the user would like to bypass this exception-check, for
2189     * instance because he / she is using a CSS Pre-Processor, then applying the general-purpose
2190     * method {@code TagNode.setAV("id", "some-new-id")} ought to suffice.  This other method will
2191     * not apply validity checking, beyond scanning for the usual "quotes-within-quotes" problems,
2192     * which is always disallowed.
2193     *
2194     * @throws ClosingTagNodeException <EMBED CLASS="external-html" DATA-FILE-ID=CTNEX>
2195     * 
2196     * @see CSSStrException#VALID_CSS_CLASS_OR_NAME_TOKEN
2197     * @see #setAV(String, String, SD)
2198     */
2199    public TagNode setID(String id, SD quote)
2200    {
2201        if (! CSSStrException.VALID_CSS_CLASS_OR_NAME_TOKEN_PRED.test(id))
2202
2203            throw new IllegalArgumentException(
2204                "The id parameter provide: [" + id + "], does not conform to the standard CSS " +
2205                "Names.\nEither try using the generic TagNode.setAV(\"id\", yourNewId, quote); " +
2206                "method to bypass this check, or change the value passed to the 'id' parameter " +
2207                "here."
2208            );
2209
2210        return setAV("id", id.trim(), quote);
2211    }
2212
2213
2214    // ********************************************************************************************
2215    // ********************************************************************************************
2216    // Attributes that begin with "data-..."
2217    // ********************************************************************************************
2218    // ********************************************************************************************
2219
2220
2221    /**
2222     * Convenience Method.
2223     * <BR />Invokes: {@link #AV(String)}
2224     * <BR />Passes: {@code "data-"} prepended to parameter {@code 'dataName'} for the
2225     * attribute-<B STYLE='color:red'>name</B>
2226     */
2227    public String dataAV(String dataName) { return AV("data-" + dataName); }
2228
2229    /**
2230     * This method will remove any HTML <B STYLE="color: red;">'data'</B> Attributes - if there are
2231     * any present.   "Data Inner-Tags" are simply the attributes inside of HTML Elements whose 
2232     * <B STYLE="color: red;">names</B> begin with <B STYLE="color: red;">{@code "data-"}</B>.
2233     * 
2234     * <BR /><BR />Since {@code class TagNode} is immutable, a new {@code TagNode} must be
2235     * instantiated, if any data-inner-tags are removed.  If no data-attributes are removed,
2236     * {@code 'this'} instance {@code TagNode} shall be returned instead.
2237     *
2238     * @return This will return a newly constructed {@code 'TagNode'} instance, if there were any
2239     * "<B STYLE="color: red;">Data</B> Attributes" that were removed by request.  If the original
2240     * {@code TagNode} has remained unchanged, a reference to {@code 'this'} shall be returned.
2241     * 
2242     * @throws ClosingTagNodeException This exception throws if {@code 'this'} instance of 
2243     * {@code TagNode} is a closing-version of the HTML Element.  Closing HTML Elements may not
2244     * have data attributes, because they simply are not intended to contain <I>any</I> attributes.
2245     */
2246    public TagNode removeDataAttributes() 
2247    {
2248        // Because this method expects to modify the TagNode, this exception-check is necessary.
2249        ClosingTagNodeException.check(this);
2250
2251        // Make sure to keep the quotes that were already used, we are removing attributes, and the
2252        // original attributes that aren't removed need to preserve their quotation marks.
2253
2254        Properties          p       = this.allAV(true, true);
2255        Enumeration<Object> keys    = p.keys();
2256        int                 oldSize = p.size();
2257
2258        // Visit each Property Element, and remove any properties that have key-names which
2259        // begin with the word "data-"
2260
2261        while (keys.hasMoreElements())
2262        {
2263            String key = keys.nextElement().toString();
2264            if (key.startsWith("data-")) p.remove(key);
2265        }
2266
2267        // If any of properties were removed, we have to rebuild the TagNode, and replace it.
2268        // REMEMBER: 'null' is passed to the 'SD' parameter, because we preserved the original
2269        //           quotes above.
2270
2271        return (p.size() == oldSize)
2272            ? this
2273            : new TagNode(this.tok, p, null, this.str.endsWith("/>"));
2274    }
2275
2276    /**
2277     * Convenience Method.
2278     * <BR />Invokes: {@link #getDataAV(boolean)}
2279     * <BR />Attribute-<B STYLE="color: red;">names</B> will be in lower-case
2280     */
2281    public Properties getDataAV() { return getDataAV(false); }
2282
2283    /**
2284     * This will retrieve and return any/all <B STYLE="color: red;">'data'</B> HTML Attributes.
2285     * "Data Inner-Tags" are simply the attributes inside of HTML Elements whose 
2286     * <B STYLE="color: red;">names</B> begin with <B STYLE="color: red;">{@code "data-"}</B>.
2287     *
2288     * @param preserveKeysCase When this parameter is passed <B>TRUE</B>, the case of the attribute
2289     * <B STYLE="color: red;">names</B> in the returned {@code Properties} table will have been
2290     * preserved.  When <B>FALSE</B> is passed, all {@code Properties}
2291     * <B STYLE="color: red">keys</B> shall have been converted to lower-case first.
2292     *
2293     * <EMBED CLASS="external-html" DATA-FILE-ID=TAGNODE_PRESERVE_C>
2294     *
2295     * @return This will return a {@code java.util.Properties} of all 
2296     * <B STYLE="color: red;">data</B>-attributes which are found in {@code 'this'} HTML Element.
2297     * If no such attributes were found, 'null' shall not be returned by this method, but rather an
2298     * empty {@code Properties} instance will be provided, instead.
2299     *
2300     * @see TagNode#isClosing
2301     * @see TagNode#str
2302     * @see TagNode#tok
2303     * @see AttrRegEx#DATA_ATTRIBUTE_REGEX
2304     */
2305    public Properties getDataAV(boolean preserveKeysCase) 
2306    {
2307        Properties ret = new Properties();
2308
2309        // NOTE: OPTIMIZED, "closing-versions" of the TagNode, and TagNode's whose 'str'
2310        //       field is only longer than the token, itself, by 3 or less characters cannot have
2311        //       attributes.v In that case, just return an empty 'Properties' instance.
2312
2313        if (isClosing || (str.length() <= (tok.length() + 3))) return ret;
2314
2315        // This RegEx Matcher 'matches' against Attribute/InnerTag Key-Value Pairs
2316        //      ONLY PAIRS WHOSE KEY BEGINS WITH "data-" WILL MATCH
2317        //
2318        // m.group(2): returns the 'key' portion of the key-value pair, before an '=' (equals-sign)
2319        // m.group(3): returns the 'value' portion of the key-value pair, after an '='
2320
2321        Matcher m = AttrRegEx.DATA_ATTRIBUTE_REGEX.matcher(this.str);
2322
2323        // NOTE: HTML mandates attributes must be 'case-insensitive' to the attribute 'key-part'
2324        //      (but not necessarily the 'value-part')
2325        //
2326        // HOWEVER: Java does not require anything for the 'Properties' class.
2327        // ALSO:    Case is PRESERVED for the 'value-part' of the key-value pair.
2328
2329        if (preserveKeysCase)
2330            while (m.find())
2331                ret.put(m.group(2), StringParse.ifQuotesStripQuotes(m.group(3)));
2332
2333        else
2334            while (m.find())
2335                ret.put(m.group(2).toLowerCase(), StringParse.ifQuotesStripQuotes(m.group(3)));
2336 
2337        return ret;
2338    }
2339
2340    /**
2341     * Convenience Method.
2342     * <BR />Invokes: {@link #getDataAN(boolean)}
2343     * <BR />Attribute-<B STYLE="color: red;">names</B> will be in lower-case
2344     */
2345    public Stream<String> getDataAN() { return getDataAN(false); }
2346
2347    /**
2348     * This method will only return a list of all data-attribute <B STYLE="color: red;">names</B>.
2349     * The data-attribute <B STYLE="color: red;">values</B> shall not be included in the result.
2350     * An HTML Element "data-attribute" is any attribute inside of an HTML {@code TagNode} whose 
2351     * <B STYLE="color: red;">key-value</B> pair uses a <B STYLE="color: red;">key</B> that begins
2352     * with <B STYLE="color: red;">{@code "data-"}</B>...  <I>It is that simple!</I>
2353     *
2354     * @param preserveKeysCase When this parameter is passed <B>TRUE</B>, the case of the attribute
2355     * <B STYLE="color: red;">names</B> that are returned by this method will have been
2356     * preserved.  When <B>FALSE</B> is passed, they shall be converted to lower-case first.
2357     *
2358     * <EMBED CLASS="external-html" DATA-FILE-ID=TAGNODE_PRESERVE_C>
2359     *
2360     * @return Returns an instance of {@code Stream<String>}.  The attribute 
2361     * <B STYLE="color: red;">names</B> that are returned, are all converted to lower-case.
2362     * 
2363     * <BR /><BR />A return type of {@code Stream<String>} is used.  Please see the list below for
2364     * how to convert a {@code Stream} to another data-type.
2365     *
2366     * <EMBED CLASS="external-html" DATA-FILE-ID=STRMCNVT>
2367     *
2368     * <BR /><B>NOTE:</B> This method shall never return 'null' - even if there are no
2369     * <B STYLE="color: red;">data-</B>attribute <B STYLE="color: red;">key-value</B> pairs
2370     * contained by the {@code TagNode}.  If there are strictly zero such attributes, 
2371     * ({@code Stream.empty()}) shall be returned, instead.
2372     * 
2373     * @see #str
2374     * @see #tok
2375     * @see AttrRegEx#DATA_ATTRIBUTE_REGEX
2376     */
2377    public Stream<String> getDataAN(boolean preserveKeysCase) 
2378    {
2379        // Java Stream's can be quickly and easily converted to any data-structure the user needs.
2380        Stream.Builder<String> b = Stream.builder();
2381
2382        // NOTE: OPTIMIZED, "closing-versions" of the TagNode, and TagNode's whose 'str'
2383        //       field is only longer than the token, itself, by 3 or less characters cannot have
2384        //       attributes.  In that case, just return an empty 'Properties' instance.
2385
2386        if (isClosing || (str.length() <= (tok.length() + 3))) return Stream.empty();
2387
2388        // This RegEx Matcher 'matches' against Attribute/InnerTag Key-Value Pairs
2389        //      ONLY PAIRS WHOSE KEY BEGINS WITH "data-" WILL MATCH
2390        // m.group(2): returns the 'key' portion of the key-value pair, before an '=' (equals-sign).
2391        // m.group(3): returns the 'value' portion of the key-value pair, after an '='
2392
2393        Matcher m = AttrRegEx.DATA_ATTRIBUTE_REGEX.matcher(this.str);
2394
2395        // NOTE: HTML mandates attributes must be 'case-insensitive' to the attribute 'key-part'
2396        //      (but not necessarily the 'value-part')
2397        // HOWEVER: Java does not require anything for the 'Properties' class.
2398        // ALSO: Case is PRESERVED for the 'value-part' of the key-value pair.
2399
2400        if (preserveKeysCase)   while (m.find()) b.accept(m.group(2));
2401        else                    while (m.find()) b.accept(m.group(2).toLowerCase());
2402 
2403        return b.build();
2404    }
2405
2406
2407    // ********************************************************************************************
2408    // ********************************************************************************************
2409    // Java Methods
2410    // ********************************************************************************************
2411    // ********************************************************************************************
2412
2413
2414    /**
2415     * This does a "longer version" of the parent {@code toString()} method.  This is because it
2416     * also parses and prints inner-tag <B STYLE="color: red;">key-value</B> pairs.  The ordinary
2417     * {@code public String toString()} method that is inherited from parent {@code class HTMLNode}
2418     * will just return the value of {@code class HTMLNode} field: {@code public final String str}.
2419     * 
2420     * @return A String with the inner-tag <B STYLE="color: red;">key-value</B> pairs specified.
2421     * 
2422     * <DIV CLASS="EXAMPLE">{@code 
2423     * // The following code, would output the text below
2424     * TagNode tn = new TagNode("<BUTTON CLASS='MyButtons' ONCLICK='MyListener();'>");
2425     * System.out.println(tn.toStringAV());
2426     *
2427     * // Outputs the following Text:
2428     * 
2429     * // TagNode.str: [<BUTTON class='MyButtons' onclick='MyListener();'>], TagNode.tok: [button],
2430     * //      TagNode.isClosing: [false]
2431     * // CONTAINS a total of (2) attributes / inner-tag key-value pairs:
2432     * // (KEY, VALUE):   [onclick], [MyListener();]
2433     * // (KEY, VALUE):   [class], [MyButtons]
2434     * }</DIV>
2435     * 
2436     * @see #allAV()
2437     * @see #tok
2438     * @see #isClosing
2439     * @see HTMLNode#toString()
2440     */
2441    public String toStringAV()
2442    {
2443        StringBuilder sb = new StringBuilder();
2444
2445        // Basic information.  This info is common to ALL instances of TagNode
2446        sb.append(
2447            "TagNode.str: [" + this.str + "], TagNode.tok: [" + this.tok + "], " +
2448            "TagNode.isClosing: [" + this.isClosing + "]\n"
2449        );
2450
2451        // Not all instances of TagNode will have attributes.
2452        Properties attributes = this.allAV(false, true);
2453
2454        sb.append(
2455            "CONTAINS a total of (" + attributes.size() + ") attributes / inner-tag " +
2456            "key-value pairs" + (attributes.size() == 0 ? "." : ":") + "\n"
2457        );
2458
2459        // If there are inner-tags / attributes, then add them to the output-string, each on a
2460        // separate line.
2461
2462        for (String key : attributes.stringPropertyNames())
2463            sb.append("(KEY, VALUE):\t[" + key + "], [" + attributes.get(key) + "]\n");
2464
2465        // Build the string from the StringBuilder, and return it.
2466        return sb.toString();    
2467    }
2468
2469    /**
2470     * Java's {@code interface Cloneable} requirements.  This instantiates a new {@code TagNode}
2471     * with identical <SPAN STYLE="color: red;">{@code String str}</SPAN> fields, and also
2472     * identical <SPAN STYLE="color: red;">{@code boolean isClosing}</SPAN> and
2473     * <SPAN STYLE="color: red;">{@code String tok}</SPAN> fields.
2474     * 
2475     * @return A new {@code TagNode} whose internal fields are identical to this one.
2476     */
2477    public TagNode clone() { return new TagNode(str); }
2478
2479    /**
2480     * This sorts by:
2481     * 
2482     * <BR /><BR /><OL CLASS="JDOL">
2483     * <LI> by {@code String 'tok'} fields character-order (ASCII-alphabetical).
2484     *      <BR />The following {@code final String 'tok'} fields are ASCII ordered:
2485     *      {@code 'a', 'button', 'canvas', 'div', 'em', 'figure' ...}
2486     * </LI>
2487     * <LI>then (if {@code 'tok'} fields are equal) by the {@code public final boolean 'isClosing'}
2488     *      field. <BR />{@code TagNode's} that have a {@code 'isClosing'} set to <B>FALSE</B> come
2489     *      before {@code TagNode's} whose {@code 'isClosing'} field is set to <B>TRUE</B>
2490     * </LI>
2491     * <LI> finally, if the {@code 'tok'} and {@code 'isClosing'} fields are equal, they are
2492     *      sorted by <I>the integer-length of</I> {@code final String 'str'} field.
2493     * </LI>
2494     * </OL>
2495     * 
2496     * @param n Any other {@code TagNode} to be compared to {@code 'this' TagNode}
2497     * 
2498     * @return An integer that fulfils Java's
2499     * {@code interface Comparable<T> public boolean compareTo(T t)} method requirements.
2500     * 
2501     * @see #tok
2502     * @see #isClosing
2503     * @see #str
2504     */
2505    public int compareTo(TagNode n)
2506    {
2507        // Utilize the standard "String.compare(String)" method with the '.tok' string field.
2508        // All 'tok' fields are stored as lower-case strings.
2509        int compare1 = this.tok.compareTo(n.tok);
2510
2511        // Comparison #1 will be non-zero if the two TagNode's being compared had different
2512        // .tok fields
2513        if (compare1 != 0) return compare1;
2514
2515        // If the '.tok' fields were the same, use the 'isClosing' field for comparison instead.
2516        // This comparison will only be used if they are different.
2517        if (this.isClosing != n.isClosing) return (this.isClosing == false) ? -1 : 1;
2518    
2519        // Finally try using the entire element '.str' String field, instead.  
2520        return this.str.length() - n.str.length();
2521    }
2522
2523
2524    // ********************************************************************************************
2525    // ********************************************************************************************
2526    // UpperCase, LowerCase 
2527    // ********************************************************************************************
2528    // ********************************************************************************************
2529
2530    // public TagNode toUpperCase(boolean b) { return null; }
2531    // public TagNode toLowerCase(boolean b) { return null; }
2532
2533    /**
2534     * Return a capitalized (upper-case) instance of the {@code String}-Contents of this
2535     * {@code TagNode}.
2536     * 
2537     * <BR /><BR />The user has the option of capitalizing the Tag-Name only, or the Tag-Name
2538     * and the Attribute-<B STYLE='color: red;'>Name's</B>.
2539     * 
2540     * <BR /><BR />White-space shall remain unchanged by this method.
2541     * 
2542     * @param justTag_Or_TagAndAttributeNames When this parameter is passed {@code TRUE}, only the
2543     * Element-Name will be converted to Upper-Case.  This is the {@link #tok} field of this
2544     * {@code TagNode}.
2545     * 
2546     * <BR /><BR />If this parameter receives {@code FALSE}, then
2547     * <B STYLE='color: red;'><I>BOTH</I></B> the Tag-Name <B STYLE='color: red;'><I>AND</I></B>
2548     * the Attribute-Names are capitalized.
2549     * 
2550     * <!--
2551     * NOTE: THIS CODE BREAKS COMMAND 'javadoc', AND I DON'T KNOW WHY...
2552     * 
2553     * <BR /><BR />The follwing example will (hopefully) elucidate the output of this method.
2554     * 
2555     * <DIV CLASS=EXAMPLE>{@code
2556     * TagNode tn = new TagNode("<a href='http://some.url.com/Some/Case/Sensitive/DIR'>");
2557     * 
2558     * System.out.println(tn.toUpperCase(true));
2559     * // Prints: "<A href='http://some.url.com/Some/Case/Sensitive/DIR'>"
2560     * // NOTE: Only the 'a' is upper-case, the 'href' attribute is still lower-case
2561     * 
2562     * System.out.println(tn.toUpperCase(false));
2563     * // Prints: "<A HREF='http://some.url.com/Some/Case/Sensitive/DIR'>"
2564     * // NOTE: The Tag-Name and all Attribute-Names are now upper-case.  
2565     * // ALSO: The HREF-URL (an Attribute-Value) has remained unchanged.
2566     * }<DIV>
2567     * -->
2568     * 
2569     * @return A capitalized version of {@code 'this'} instance.  Only the Tag-Name, or the
2570     * Tag-Name and the Attribute-<B STYLE='color: red;'>Name's</B> will be capitalized.  This 
2571     * method will leave Attributte-<B STYLE='color: red;'>Values</B> unmodified.
2572     * 
2573     * <BR /><BR />All spacing characters and (again) Attribute-<B STYLE='color: red;'>Values</B>
2574     * will remain unchanged.
2575     * 
2576     * <BR /><BR /><B CLASS=JDRedLabel>'DATA-' ATTRIBUTES:</B>
2577     * 
2578     * <BR />HTML {@code 'data-*'} Attributes are Inner-Tags that allow a programmer to pass
2579     * 'values', of one kind or another, that have a name to a Web-Browser or client.  Since there
2580     * is the possibility that the 'name' provided is case-sensitive, this method will not alter the
2581     * name text that apears after the {@code 'data-'} portion of the
2582     * Attribute-<B STYLE='color: red;'>Name</B> for HTML Data-Attributes.
2583     */
2584    public TagNode toUpperCase(boolean justTag_Or_TagAndAttributeNames)
2585    {
2586        if (justTag_Or_TagAndAttributeNames) return new TagNode(
2587            this.isClosing
2588                ? ("</" + this.tok.toUpperCase() + this.str.substring(2 + this.tok.length()))
2589                : ('<' + this.tok.toUpperCase() + this.str.substring(1 + this.tok.length()))
2590        );
2591
2592        StringBuilder   sb = new StringBuilder();
2593        Matcher         m = AttrRegEx.KEY_VALUE_REGEX.matcher(this.str);
2594
2595        sb.append(this.isClosing ? "</" : "<");
2596        sb.append(this.tok.toUpperCase());
2597
2598        // Skip over the opening '<' and the Tag-Name
2599        int pos = this.tok.length() + 1;
2600
2601        // If this was a "Closing Tag", remember to skip the opeing '/'
2602        if (this.isClosing) pos++;
2603
2604        // Here, the Key-Value (Attribue-Name & Attribute-Value) pairs are iterated.  Care is 
2605        // taken to ensure that only the names (not the values) are modified.
2606
2607        while (m.find())
2608        {
2609            // Apppend white-space that occurs **BEFORE** the Name-Value Pair
2610            //
2611            // NOTE: The 'toUpperCase' here will catch any Attribute-Name-Only Attributes
2612            //       (also known as "Boolean-Attributes")
2613
2614            sb.append(this.str.substring(pos, m.start(2)).toUpperCase());
2615    
2616            // Append the Attribute-Name, and make sure to Capitalize it.  First this needs to be
2617            // retrieved, and (more importantly), do not capitalize the actual name-part of 
2618            // "data-" Attributes, their case **COULD POSSIBLY** be important... (They are for the
2619            // EmbedTag Parameters Data-Attributes)
2620
2621            String attrName = m.group(2).toUpperCase();
2622
2623            sb.append(
2624                StrCmpr.startsWithIgnoreCase(attrName, "data-")
2625                    ? ("DATA-" + attrName.substring(5).toUpperCase())
2626                    : attrName.toUpperCase()
2627            );
2628
2629            // Append the Attribute-Value, and update the 'pos' variable to reflect where
2630            // in the String the current Match-Location ends...
2631            //
2632            // NOTE: m.end(2) and m.end() are the exact same values, since this regex's
2633            //       group #2 ends at the very-end of the regex pattern.
2634
2635            sb.append(this.str.substring(pos = m.end(2), pos));
2636        }
2637
2638        // ALWAYS: After the last match of a RegEx, remember to append any text that occurs
2639        //         after the last match.  This is also quite important in the HTML-Parser
2640        //         not to forget this line.
2641
2642        sb.append(this.str.substring(pos));
2643
2644        // Return the new TagNode
2645        return new TagNode(sb.toString());
2646    }
2647
2648    /**
2649     * Return a de-capitalized (Lower-case) instance of the {@code String}-Contents of this
2650     * {@code TagNode}.
2651     * 
2652     * <BR /><BR />The user has the option of de-capitalizing the Tag-Name only, or the Tag-Name
2653     * and the Attribute-<B STYLE='color: red;'>Name's</B>.
2654     * 
2655     * <BR /><BR />White-space shall remain unchanged by this method.
2656     * 
2657     * @param justTag_Or_TagAndAttributeNames When this parameter is passed {@code TRUE}, only the
2658     * Element-Name will be converted to Lower-Case.  (The '{@link #tok}'' field of this
2659     * {@code TagNode} is changed)
2660     * 
2661     * <BR /><BR />If this parameter receives {@code FALSE}, then
2662     * <B STYLE='color: red;'><I>BOTH</I></B> the Tag-Name <B STYLE='color: red;'><I>AND</I></B>
2663     * the Attribute-Names are de-capitalized.
2664     * 
2665     * <!--
2666     * THIS BREAKS JAVADOC, FOR SOME ODD REASON...
2667     * 
2668     * <BR /><BR />The follwing example will (hopefully) elucidate the output of this method.
2669     * 
2670     * <DIV CLASS=EXAMPLE>{@code
2671     * TagNode tn = new TagNode("<DIV CLASS=MyMainClass1>");
2672     * 
2673     * System.out.println(tn.toLowerCase(true));
2674     * // Prints: "<div CLASS=MyMainClass1>"
2675     * // NOTE: Only the 'div' is lower-case, the 'class' attribute is still upper-case
2676     * 
2677     * System.out.println(tn.toLowerCase(false));
2678     * // Prints: "<div class=MyMainClass1>"
2679     * // NOTE: The Tag-Name and all Attribute-Names are now lower-case.  
2680     * // ALSO: The CSS-Class Name (an Attribute-Value) has remained unchanged.
2681     * }<DIV>
2682     * -->
2683     * 
2684     * @return A lower-case version of {@code 'this'} instance.  Only the Tag-Name, or the
2685     * Tag-Name and the Attribute-<B STYLE='color: red;'>Name's</B> will be de-capitalized.  This
2686     * method will leave Attributte-<B STYLE='color: red;'>Values</B> unmodified.
2687     * 
2688     * <BR /><BR />All spacing characters and (again) Attribute-<B STYLE='color: red;'>Values</B>
2689     * will remain unchanged.
2690     * 
2691     * <BR /><BR /><B CLASS=JDRedLabel>'DATA-' ATTRIBUTES:</B>
2692     * 
2693     * <BR />HTML {@code 'data-*'} Attributes are Inner-Tags that allow a programmer to pass
2694     * 'values', of one kind or another, that have a name to a Web-Browser or client.  Since there
2695     * is the possibility that the 'name' provided is case-sensitive, this method will not alter the
2696     * name text that apears after the {@code 'data-'} portion of the
2697     * Attribute-<B STYLE='color: red;'>Name</B> for HTML Data-Attributes.
2698     */
2699    public TagNode toLowerCase(boolean justTag_Or_TagAndAttributeNames)
2700    {
2701        if (justTag_Or_TagAndAttributeNames) return new TagNode(
2702            this.isClosing
2703                ? ("</" + this.tok.toLowerCase() + this.str.substring(2 + this.tok.length()))
2704                : ('<' + this.tok.toLowerCase() + this.str.substring(1 + this.tok.length()))
2705        );
2706
2707        StringBuilder   sb = new StringBuilder();
2708        Matcher         m = AttrRegEx.KEY_VALUE_REGEX.matcher(this.str);
2709
2710        sb.append(this.isClosing ? "</" : "<");
2711        sb.append(this.tok.toLowerCase());
2712
2713        // Skip over the opening '<' and the Tag-Name
2714        int pos = this.tok.length() + 1;
2715
2716        // If this was a "Closing Tag", remember to skip the opeing '/'
2717        if (this.isClosing) pos++;
2718
2719        // Here, the Key-Value (Attribue-Name & Attribute-Value) pairs are iterated.  Care is 
2720        // taken to ensure that only the names (not the values) are modified.
2721
2722        while (m.find())
2723        {
2724            // Apppend white-space that occurs **BEFORE** the Name-Value Pair
2725            //
2726            // NOTE: The 'toLowerCase' here will catch any Attribute-Name-Only Attributes
2727            //       (also known as "Boolean-Attributes")
2728
2729            sb.append(this.str.substring(pos, m.start(2)).toLowerCase());
2730    
2731            // Append the Attribute-Name, and make sure to De-Capitalize it.  First this needs to
2732            // be retrieved, and (more importantly), do not modify the actual name-part of "data-"
2733            // Attributes, their case **COULD POSSIBLY** be important... (They are for the EmbedTag
2734            // Parameters Data-Attributes)
2735
2736            String attrName = m.group(2).toLowerCase();
2737
2738            sb.append(
2739                StrCmpr.startsWithIgnoreCase(attrName, "data-")
2740                    ? ("DATA-" + attrName.substring(5).toLowerCase())
2741                    : attrName.toLowerCase()
2742            );
2743
2744            // Append the Attribute-Value, and update the 'pos' variable to reflect where
2745            // in the String the current Match-Location ends...
2746            //
2747            // NOTE: m.end(2) and m.end() are the exact same values, since this regex's
2748            //       group #2 ends at the very-end of the regex pattern.
2749
2750            sb.append(this.str.substring(pos = m.end(2), pos));
2751        }
2752
2753        // ALWAYS: After the last match of a RegEx, remember to append any text that occurs
2754        //         after the last match.  This is also quite important in the HTML-Parser
2755        //         not to forget this line.
2756
2757        sb.append(this.str.substring(pos));
2758
2759        // Return the new TagNode
2760        return new TagNode(sb.toString());
2761    }
2762
2763
2764    // ********************************************************************************************
2765    // ********************************************************************************************
2766    // Internally used Regular Expressions, (STATIC FIELDS INSIDE STATIC CLASS)
2767    // ********************************************************************************************
2768    // ********************************************************************************************
2769
2770
2771    /**
2772     * Regular-Expressions that are used by both the parsing class {@link HTMLPage}, and class 
2773     * {@link TagNode} for searching HTML tags for attributes and even data.
2774     * 
2775     * <BR /><BR /><EMBED CLASS="external-html" DATA-FILE-ID=TAGNODE_ATTR_REGEX>
2776     */
2777    public static final class AttrRegEx
2778    {
2779        private AttrRegEx() { }
2780
2781        /**
2782         * <EMBED CLASS="external-html" DATA-FILE-ID=TAGNODE_REGEX_KV>
2783         * @see TagNode#allAV(boolean, boolean)
2784         */
2785        public static final Pattern KEY_VALUE_REGEX = Pattern.compile(
2786            "(?:\\s+?" +                    // mandatory leading white-space
2787                "(([\\w-]+?)=(" +           // inner-tag name (a.k.a. 'key' or 'attribute-name')
2788                    "'[^']*?'"     + "|" +  // inner-tag value using single-quotes ... 'OR'
2789                    "\"[^\"]*?\""   + "|" + // inner-tag value using double-quotes ... 'OR'
2790                    "[^\"'>\\s]*"   +       // inner-tag value without quotes
2791            ")))",
2792            Pattern.CASE_INSENSITIVE | Pattern.DOTALL
2793        );
2794
2795        /**
2796         * A {@code Predicate<String>} Regular-Expression.
2797         * @see #KEY_VALUE_REGEX
2798         */
2799        public static final Predicate<String> KEY_VALUE_REGEX_PRED =
2800            KEY_VALUE_REGEX.asPredicate();
2801
2802        /**
2803         * <SPAN STYLE="color: red;"><B>LEGACY-REGEX:</B></SPAN> Was used by the method
2804         * {@code TagNode.AV(String)}, among others.  Note that this regular-expression will be
2805         * deprecated, since it is redundant.
2806         *
2807         * <BR /><BR /><B>CAPTURE GROUPS:</B> <I>Nearly</I> the entire Reg-Ex is surrounding by
2808         * parenthesis.  {@code m.group(1)} shall return a {@code String} that differs with
2809         * {@code m.group()}, less a leading {@code '='} (equals-sign).
2810         *
2811         * @see TagNode#AV(String)
2812         */
2813        public static final Pattern QUOTES_AND_VALUE_REGEX = Pattern.compile(
2814            // Matches, for example:  ='MyClass'   or    ="MyClass"   or   =MyClass
2815            "=(" + 
2816                "\"[^\"]*?\""   + "|" + // inner-tag value using single-quotes ... 'OR'
2817                "'[^']*?'"      + "|" + // inner-tag value using double-quotes ... 'OR'
2818                "[\\w-]+"       +       // inner-tag value without quotes
2819            ")",
2820            Pattern.DOTALL
2821        );
2822
2823        /**
2824         * A {@code Predicate<String>} Regular-Expression.
2825         * @see #QUOTES_AND_VALUE_REGEX
2826         */
2827        public static final Predicate<String> QUOTES_AND_VALUE_REGEX_PRED =
2828            QUOTES_AND_VALUE_REGEX.asPredicate();
2829
2830        /**
2831         * This matches all valid attribute-<B STYLE="color: red;">keys</B> <I>(not values)</I> of
2832         * HTML Element <B STYLE="color: red;">key-value pairs</B>.
2833         * 
2834         * <BR /><BR /><UL CLASS="JDUL">
2835         * <LI> <B>PART-1:</B> {@code [A-Za-z_]} The first character must be a letter or the
2836         *      underscore.
2837         * </LI>
2838         * <LI> <B>PART-2:</B> {@code [A-Za-z0-9_-]} All other characters must be alpha-numeric,
2839         *      the dash {@code '-'}, or the underscore {@code '_'}.
2840         * </LI>
2841         * </UL>
2842         * 
2843         * @see InnerTagKeyException#check(String[])
2844         * @see #allKeyOnlyAttributes(boolean)
2845         */
2846        public static final Pattern ATTRIBUTE_KEY_REGEX = 
2847            Pattern.compile("^[A-Za-z_][A-Za-z0-9_-]*$");
2848
2849        /**
2850         * A {@code Predicate<String>} Regular-Expression.
2851         * @see #ATTRIBUTE_KEY_REGEX
2852         */
2853        public static final Predicate<String> ATTRIBUTE_KEY_REGEX_PRED =
2854            ATTRIBUTE_KEY_REGEX.asPredicate();
2855
2856        /**
2857         * <EMBED CLASS="external-html" DATA-FILE-ID=TAGNODE_REGEX_DATA>
2858         * @see TagNode#getDataAN()
2859         * @see TagNode#getDataAV()
2860         */
2861        public static final Pattern DATA_ATTRIBUTE_REGEX = Pattern.compile(
2862            // regex will match, for example:   data-src="https://cdn.imgur.com/MyImage.jpg"
2863            "(?:\\s+?" +                            // mandatory leading white-space
2864                "(data-([\\w-]+?)=" +               // data inner-tag name 
2865                    "(" +   "'[^']*?'"      + "|" + // inner-tag value using single-quotes ... 'OR'
2866                            "\"[^\"]*?\""   + "|" + // inner-tag value using double-quotes ... 'OR
2867                            "[^\"'>\\s]*"   +       // inner-tag value without quotes
2868                ")))",
2869            Pattern.CASE_INSENSITIVE | Pattern.DOTALL  
2870        );
2871
2872        /**
2873         * A {@code Predicate<String>} Regular-Expression.
2874         * @see #DATA_ATTRIBUTE_REGEX
2875         */
2876        public static final Predicate<String> DATA_ATTRIBUTE_REGEX_PRED =
2877            DATA_ATTRIBUTE_REGEX.asPredicate();
2878
2879        /**
2880         * <EMBED CLASS="external-html" DATA-FILE-ID=TAGNODE_REGEX_CSS>
2881         * @see TagNode#cssStyle()
2882         */
2883        public static final Pattern CSS_INLINE_STYLE_REGEX = Pattern.compile(
2884                // regex will match, for example:  font-weight: bold;
2885
2886                // CSS Style Property Name - Must begin with letter or underscore
2887                "([_\\-a-zA-Z]+" + "[_\\-a-zA-Z0-9]*)" +
2888
2889                // The ":" symbol between property-name and property-value
2890                "\\s*?" + ":" + "\\s*?" +
2891
2892                // CSS Style Property Value
2893                "([^;]+?\\s*)" +
2894
2895                // text after the "Name : Value" definition    
2896                "(;|$|[\\w]+$)"
2897        );
2898
2899        /**
2900         * A {@code Predicate<String>} Regular-Expression.
2901         * @see #CSS_INLINE_STYLE_REGEX
2902         */
2903        public static final Predicate<String> CSS_INLINE_STYLE_REGEX_PRED =
2904            CSS_INLINE_STYLE_REGEX.asPredicate();
2905    }
2906
2907}