001package Torello.HTML;
002
003import Torello.HTML.NodeSearch.*;
004import Torello.Java.FileRW; // used in @see comments
005import Torello.Java.StringParse;
006import Torello.Java.Additional.Ret2;
007
008import java.util.*;
009import java.util.function.BiConsumer;
010import java.util.stream.IntStream;
011
012/**
013 * Utilities for checking that opening and closing {@link TagNode} elements match up (that the HTML
014 * is balanced).
015 * 
016 * <EMBED CLASS='external-html' DATA-FILE-ID=BALANCE>
017 */
018@Torello.JavaDoc.Annotations.StaticFunctional
019public class Balance
020{
021    private Balance() { }
022
023    /**
024     * <EMBED CLASS='external-html' DATA-FILE-ID=B_CB_DESC>
025     * @param html <EMBED CLASS='external-html' DATA-FILE-ID=HTMLVECSUP>
026     * 
027     * @return Will return null if the snippet or page has 'balanced' HTML, otherwise returns the
028     * trimmed balance-report as a {@code String}.
029     */
030    public static String CB(final Vector<HTMLNode> html)
031    {
032        final String ret = toStringBalance(checkNonZero(check(html)));
033        return (ret.length() == 0) ? null : ret;
034    }
035
036    /**
037     * <EMBED CLASS='external-html' DATA-FILE-ID=B_CHECK_V1_DESC_P1>
038     * <EMBED CLASS='external-html' DATA-FILE-ID=BALANCE_VALID_NOTE1>
039     * <EMBED CLASS='external-html' DATA-FILE-ID=B_CHECK_V1_DESC_P2>
040     * @param html  <EMBED CLASS='external-html' DATA-FILE-ID=HTMLVECSUP>
041     * @return      <EMBED CLASS='external-html' DATA-FILE-ID=B_CHECK_V1_RET>
042     * @see         FileRW#loadFileToString(String)
043     * @see         HTMLPage#getPageTokens(CharSequence, boolean)
044     */
045    public static Hashtable<String, Integer> check(final Vector<? super TagNode> html)
046    {
047        final Hashtable<String, Integer> ht = new Hashtable<>();
048
049        // Iterate through the HTML List, we are only counting HTML Elements, not text or comments
050        for (final Object o : html) if (o instanceof TagNode)
051        {
052            final TagNode tn = (TagNode) o;
053
054            // Singleton tags are also known as 'self-closing' tags.  BR, HR, IMG, etc...
055            if (HTMLTags.isSingleton(tn.tok)) continue;
056
057    
058            // Current value in the table, or 'null' if this tag hasn't been seen yet.
059            // 
060            // An opening-version (TC.OpeningTags, For Instance <DIV ...>) will ADD 1 to the count
061            // A closing-tag (For Instance: </DIV>) will SUBTRACT 1 from the count
062
063            final Integer I = ht.get(tn.tok);
064
065            final int updated =
066                ((I == null) ? 0 : I) +     // Convert 'null' to Zero; otherwise no-change
067                (tn.isClosing ? -1 : 1);    // ClosingTags => -1, OpeningTags => +1
068
069            // Update the return result Hashtable for this particular HTML-Element (tn.tok)
070            ht.put(tn.tok, updated);
071        }
072
073        return ht;
074    }
075
076    /**
077     * Creates an array that includes an open-and-close {@code 'count'} for each HTML-Tag / 
078     * that was requested via the passed input {@code String[]}-Array parameter {@code 'htmlTags'}.
079     * 
080     * <EMBED CLASS='external-html' DATA-FILE-ID=BALANCE_VALID_NOTE1> <!-- Validity Note -->
081     * 
082     * @param html              <EMBED CLASS='external-html' DATA-FILE-ID=HTMLVECSUP>
083     * @param htmlTags          <EMBED CLASS='external-html' DATA-FILE-ID=B_CHECK_V2_HTMLTAGS>
084     * @return                  <EMBED CLASS='external-html' DATA-FILE-ID=B_CHECK_V2_RET>
085     * @throws HTMLTokException If any of the tags passed are not valid HTML tags.
086     * 
087     * @throws SingletonException If any of the {@code String}-Tags passed to parameter
088     * {@code 'htmlTags'} are {@code 'singleton'} (Self-Closing) Tags, then this exception throws
089     */
090    public static int[] check(final Vector<? super TagNode> html, String... htmlTags)
091    {
092        // Check that these are all valid HTML Tags, throw an exception if not.
093        htmlTags = ARGCHECK.htmlTags(htmlTags);
094
095        // Temporary Hash-table, used to store the count of each htmlTag
096        final Hashtable<String, Integer> ht = new Hashtable<>();
097
098
099        // Initialize the temporary hash-table.  This will be discarded at the end of the method,
100        // and converted into a parallel array.  (Parallel to the input String... htmlTags array).
101        // Also, check to make sure the user hasn't requested a count of Singleton HTML Elements.
102
103        for (final String htmlTag : htmlTags)
104        {
105            if (HTMLTags.isSingleton(htmlTag)) throw new SingletonException(
106                "One of the tags you have passed: [" + htmlTag + "] is a singleton-tag, " +
107                "and is only allowed opening versions of the tag."
108            );
109
110            ht.put(htmlTag, Integer.valueOf(0));
111        }
112
113        // Iterate through the HTML List, we are only counting HTML Elements, not text or comments
114        for (final Object o : html) if (o instanceof TagNode)
115        {
116            final TagNode tn = (TagNode) o;
117
118            // Get the current count from the hash-table
119            final Integer I = ht.get(tn.tok);
120
121            // The hash-table only holds elements we are counting, if null, then skip.
122            if (I == null) continue;
123
124
125            // Save the new, computed count, in the hash-table
126            //
127            // An opening-version (TC.OpeningTags, For Instance <DIV ...>) will ADD 1 to the count
128            // A closing-tag (For Instance: </DIV>) will SUBTRACT 1 from the count
129            //
130            // NOTE: this line of code utilizes Java's Auto-Boxing & Auto-Unboxing features.
131
132            ht.put(tn.tok, I + (tn.isClosing ? -1 : 1));
133        }
134
135
136        // Convert the hash-table to an integer-array, and return this to the user
137        // Chat-GPT has assured me, by the way, that arrays are always initialized with zeroes.
138        // 
139        // No need to set the elements of this array to zero...  Ok... I new that.  😊😊
140
141        final int[] ret = new int[htmlTags.length]; 
142
143        for (int i=0; i < htmlTags.length; i++)
144        {
145            final Integer I = ht.get(htmlTags[i]);
146
147            // The assignment part of this 'if' statement is Java's Auto Un-Boxing feature
148            if (I != null) ret[i] = I;
149        }
150
151        return ret;
152    }
153
154    /**
155     * <EMBED CLASS='external-html' DATA-FILE-ID=B_CHECK_NZ_DESC>
156     * <EMBED CLASS='external-html' DATA-FILE-ID=BALANCE_VALID_NOTE1>   <!-- Validity Note  -->
157     * <EMBED CLASS='external-html' DATA-FILE-ID=BALANCE_CLONE>         <!-- Clone Note     -->
158     *
159     * @param ht This should be a {@code Hashtable} that was produced by a call to one of the two
160     * available {@code check(...)} methods.
161     * 
162     * @return <EMBED CLASS='external-html' DATA-FILE-ID=B_CHECK_NZ_RET>
163     */
164    public static Hashtable<String, Integer> checkNonZero(final Hashtable<String, Integer> ht)
165    {
166        @SuppressWarnings("unchecked")
167        final Hashtable<String, Integer>    ret     = (Hashtable<String, Integer>) ht.clone();
168        final Enumeration<String>           keys    = ret.keys();
169
170        while (keys.hasMoreElements())
171        {
172            final String key = keys.nextElement();
173
174            // Remove any keys (HTML element-names) that have a normal ('0') count.
175            if (ret.get(key).intValue() == 0) ret.remove(key);
176        }
177
178        return ret;
179    }
180
181
182    /**
183     * This will compute a {@code count} for just one, particular, HTML Element of whether that
184     * Element has been properly opened and closed.  An open and close {@code count} (integer
185     * value) will be returned by this method.
186     * 
187     * <EMBED CLASS='external-html' DATA-FILE-ID=BALANCE_VALID_NOTE1> <!-- Validity Note -->
188     * 
189     * @param html <EMBED CLASS='external-html' DATA-FILE-ID=HTMLVECSUP>
190     * 
191     * @param htmlTag This the html element whose open-close count needs to be kept.
192     * 
193     * @return The count of each html-element present in this {@code Vector}.  For instance, if the
194     * user had requested that HTML Anchor Links be counted, and if the input {@code Vector} had 5
195     * {@code '<A ...>'} (Anchor-Link) elements, and six {@code '</A>'} then this method would
196     * return {@code -1}.
197     * 
198     * @throws HTMLTokException If any of the tags passed are not valid HTML tags.
199     * 
200     * @throws SingletonException If this {@code 'htmlTag'} is a {@code 'singleton'} (Self-Closing)
201     * Tag, this exception will throw.
202     */
203    public static int checkTag(final Vector<? super TagNode> html, String htmlTag)
204    {
205        // Check that this is a valid HTML Tag, throw an exception if invalid
206        htmlTag = ARGCHECK.htmlTag(htmlTag);
207
208        if (HTMLTags.isSingleton(htmlTag)) throw new SingletonException(
209            "The tag you have passed: [" + htmlTag + "] is a singleton-tag, and is only " +
210            "allowed opening versions of the tag."
211        );
212
213
214        // Iterate through the HTML List, we are only counting HTML Elements, not text, and
215        // not HTML Comments
216
217        TagNode tn;
218        int i = 0;
219
220        for (final Object o : html) if (o instanceof TagNode) 
221
222            // If we encounter an HTML Element whose tag is the tag whose count we are 
223            // computing, then....
224
225            if ((tn = (TagNode) o).tok.equals(htmlTag))
226            
227                // An opening-version (TC.OpeningTags, For Instance <DIV ...>) will ADD 1 to the count
228                // A closing-tag (For Instance: </DIV>) will SUBTRACT 1 from the count
229
230                i += tn.isClosing ? -1 : 1;
231
232        return i;
233    }
234
235
236    /**
237     * <EMBED CLASS='external-html' DATA-FILE-ID=B_DEPTH_V1_DESC_P1>
238     * <EMBED CLASS='external-html' DATA-FILE-ID=BALANCE_VALID_NOTE2>
239     * <EMBED CLASS='external-html' DATA-FILE-ID=B_DEPTH_V1_DESC_P2>
240     *
241     * @param html  <EMBED CLASS='external-html' DATA-FILE-ID=HTMLVECSUP>
242     * @return      <EMBED CLASS='external-html' DATA-FILE-ID=B_DEPTH_V1_RET>
243     * 
244     * @throws HTMLTokException     If any of the tags passed are not valid HTML tags.
245     * @throws SingletonException   throws if {@code 'htmlTag'} is a 'singleton' (Self-Closing) tag
246     */
247    public static Hashtable<String, int[]> depth(final Vector<? super TagNode> html)
248    {
249        final Hashtable<String, int[]> ht = new Hashtable<>();
250
251        // Iterate through the HTML List, we are only counting HTML Elements, not text, and not HTML Comments
252        for (Object o : html) if (o instanceof TagNode) 
253        {
254            final TagNode tn = (TagNode) o;
255
256            // Don't keep a count on singleton tags.
257            if (HTMLTags.isSingleton(tn.tok)) continue;
258
259
260            // If this is the first encounter of a particular HTML Element, create a MAX/MIN
261            // integer array, and initialize it's values to zero.
262
263            int[] curMaxAndMinArr = ht.get(tn.tok);
264
265            if (curMaxAndMinArr == null)
266
267                // Current Min Depth Count for Element "tn.tok" is zero
268                // Current Max Depth Count for Element "tn.tok" is zero
269                // Current Computed Depth Count for "tn.tok" is zero
270
271                ht.put(tn.tok, curMaxAndMinArr = new int[3]);
272
273
274            // curCount += tn.isClosing ? -1 : 1;
275            //
276            // An opening-version (TC.OpeningTags, For Instance <DIV ...>) will ADD 1 to the count
277            // A closing-tag (For Instance: </DIV>) will SUBTRACT 1 from the count
278
279            curMaxAndMinArr[2] += tn.isClosing ? -1 : 1;
280
281
282            // If the current depth-count is a "New Minimum" (a new low! :), then save it in the
283            // minimum pos of the output-array.
284
285            if (curMaxAndMinArr[2] < curMaxAndMinArr[0])
286                curMaxAndMinArr[0] = curMaxAndMinArr[2];
287
288
289            // If the current depth-count (for this tag) is a "New Maximum" (a new high), save it
290            // to the max-pos of the output-array.
291
292            if (curMaxAndMinArr[2] > curMaxAndMinArr[1])
293                curMaxAndMinArr[1] = curMaxAndMinArr[2];
294        }
295
296        return ht;
297    }
298
299
300    /**
301     * <EMBED CLASS='external-html' DATA-FILE-ID=B_DEPTH_V2_DESC_P1>
302     * <EMBED CLASS='external-html' DATA-FILE-ID=BALANCE_VALID_NOTE2>
303     * <EMBED CLASS='external-html' DATA-FILE-ID=B_DEPTH_V2_DESC_P2>
304     * 
305     * @param html              <EMBED CLASS='external-html' DATA-FILE-ID=HTMLVECSUP>
306     * @return                  <EMBED CLASS='external-html' DATA-FILE-ID=B_DEPTH_V2_RET>
307     * @throws HTMLTokException If any of the tags passed are not valid HTML tags.
308     * 
309     * @throws SingletonException If this {@code 'htmlTag'} is a {@code 'singleton'}
310     * (Self-Closing) Tag, this exception will throw.
311     */
312    public static Hashtable<String, int[]> depth(
313            final Vector<? super TagNode>   html,
314            String...                       htmlTags
315        )
316    {
317        // Check that these are all valid HTML Tags, throw an exception if not.
318        htmlTags = ARGCHECK.htmlTags(htmlTags);
319
320        final Hashtable<String, int[]> ht = new Hashtable<>();
321
322
323        // Initialize the temporary hash-table.  This will be discarded at the end of the method,
324        // and converted into a parallel array.  (Parallel to the input String... htmlTags array).
325        // Also, check to make sure the user hasn't requested a count of Singleton HTML Elements.
326
327        for (final String htmlTag : htmlTags)
328
329            if (HTMLTags.isSingleton(htmlTag)) throw new SingletonException(
330                "One of the tags you have passed: [" + htmlTag + "] is a singleton-tag, " +
331                "and is only allowed opening versions of the tag."
332            );
333
334            // Insert a new array for this HTML-Tag,  Java Auto Initializes array cells to zero
335            else ht.put(htmlTag, new int[3]);
336
337
338        // Iterate through the HTML List, we are only counting HTML Elements, not text nor comments
339        for (final Object o: html) if (o instanceof TagNode) 
340        {
341            final TagNode tn = (TagNode) o;
342
343            final int[] curMaxAndMinArr = ht.get(tn.tok);
344
345
346            // If this is null, we are attempting to perform the count on an HTML Element that
347            // wasn't requested by the user with the var-args 'String... htmlTags' parameter.
348            // The Hashtable was initialized to only have those tags. (see about 5 lines above 
349            // where the Hashtable is initialized)
350
351            if (curMaxAndMinArr == null) continue;
352
353
354            // An opening-version (TC.OpeningTags, For Instance <DIV ...>) will ADD 1 to the count
355            // A closing-tag (For Instance: </DIV>) will SUBTRACT 1 from the count
356
357            curMaxAndMinArr[2] += tn.isClosing ? -1 : 1;
358
359    
360            // If the current depth-count is a "New Minimum" (a new low! :), then save it in the
361            // minimum pos of the output-array.
362
363            if (curMaxAndMinArr[2] < curMaxAndMinArr[0]) curMaxAndMinArr[0] = curMaxAndMinArr[2];
364
365
366            // If the current depth-count (for this tag) is a "New Maximum" (a new high), save it
367            // to the max-pos of the output-array.
368
369            if (curMaxAndMinArr[2] > curMaxAndMinArr[1]) curMaxAndMinArr[1] = curMaxAndMinArr[2];
370
371
372            // No need to update the hash-table, since this is an array - changing its
373            // values is already "reflected" into the Hashtable.
374        }
375
376        return ht;
377    }
378
379
380    /**
381     * <EMBED CLASS='external-html' DATA-FILE-ID=B_DEPTH_INVAL_DESC>
382     * 
383     * @param ht This should be a {@code Hashtable} that was produced by a call to one of the two
384     * available {@code depth(...)} methods.
385     * 
386     * @return This shall a return a list of HTML Tags that are <I>potentially (but not guaranteed
387     * to be)</I> invalid.
388     */
389    public static Hashtable<String, int[]> depthInvalid(final Hashtable<String, int[]> ht)
390    {
391        @SuppressWarnings("unchecked")
392        final Hashtable<String, int[]>  ret     = (Hashtable<String, int[]>) ht.clone();
393        final Enumeration<String>       keys    = ret.keys();
394
395
396        // Using the "Enumeration" class allows the situation where elements can be removed from
397        // the underlying data-structure - while iterating through that data-structure.  This is
398        // not possible using a keySet Iterator.
399
400        while (keys.hasMoreElements())
401        {
402            final String    key = keys.nextElement();
403            final int[]     arr = ret.get(key);
404
405            if ((arr[1] >= 0) && (arr[2] == 0)) ret.remove(key);
406        }
407
408        return ret;
409    }
410
411    /**
412     * <EMBED CLASS='external-html' DATA-FILE-ID=B_DEPTH_GT1_DESC>
413     *
414     * @param ht This should be a {@code Hashtable} that was produced by a call to one of the two
415     * available {@code depth(...)} methods.
416     * 
417     * @return This shall a return a list of HTML Tags that are <I>potentially (but not guaranteed
418     * to be)</I> invalid.
419     */
420    public static Hashtable<String, int[]> depthGreaterThanOne(final Hashtable<String, int[]> ht)
421    {
422        @SuppressWarnings("unchecked")
423        final Hashtable<String, int[]>  ret     = (Hashtable<String, int[]>) ht.clone();
424        final Enumeration<String>       keys    = ret.keys();
425
426
427        // Using the "Enumeration" class allows the situation where elements can be removed from
428        // the underlying data-structure - while iterating through that data-structure.  This is not
429        // possible using a keySet Iterator.
430
431        while (keys.hasMoreElements())
432        {
433            final String    key = keys.nextElement();
434            final int[]     arr = ret.get(key);
435
436            if (arr[1] == 1) ret.remove(key);
437        }
438
439        return ret;
440    }
441
442
443    /**
444     * <EMBED CLASS='external-html' DATA-FILE-ID=B_DEPTH_TAG_DESC_P1>
445     * <EMBED CLASS='external-html' DATA-FILE-ID=BALANCE_VALID_NOTE2>
446     * <EMBED CLASS='external-html' DATA-FILE-ID=B_DEPTH_TAG_DESC_P2>
447     *
448     * @param html      <EMBED CLASS='external-html' DATA-FILE-ID=HTMLVECSUP>
449     * @param htmlTag   The html element whose maximum and minimum depth-count needs to be computed
450     * @return          <EMBED CLASS='external-html' DATA-FILE-ID=B_DEPTH_TAG_RET>
451     * 
452     * @throws HTMLTokException     throws if any of the tags passed are not valid HTML tags
453     * @throws SingletonException   throws if {@code 'htmlTag'} is a 'singleton' (Self-Closing) tag
454     */
455    public static int[] depthTag(final Vector<? super TagNode> html, String htmlTag)
456    {
457        // Check that this is a valid HTML Tag, throw an exception if invalid
458        htmlTag = ARGCHECK.htmlTag(htmlTag);
459
460        if (HTMLTags.isSingleton(htmlTag)) throw new SingletonException(
461            "The tag you have passed: [" + htmlTag + "] is a singleton-tag, and is only allowed " +
462            "opening versions of the tag."
463        );
464
465        int i = 0, max = 0, min = 0;
466
467        // Iterate through the HTML List, we are only counting HTML Elements, not text or Comments
468        for (final Object o : html) if (o instanceof TagNode)
469        {
470            final TagNode tn = (TagNode) o;
471            if (! tn.tok.equals(htmlTag)) continue;
472
473            // An opening "<TABLE ...>" ADDS 1 to the count. A closing-"</TABLE>" SUBTRACTS 1
474            i += tn.isClosing ? -1 : 1;
475
476            if (i > max) max = i;
477            if (i < min) min = i;
478        }
479
480        return new int[] { min, max, i };
481    }
482
483    /**
484     * <EMBED CLASS='external-html' DATA-FILE-ID=B_NON_NESTED_C_DESC>
485     * @param html      <EMBED CLASS='external-html' DATA-FILE-ID=HTMLVECSUP>
486     * @param htmlTag   <EMBED CLASS='external-html' DATA-FILE-ID=B_NON_NESTED_C_HT>
487     * @return          <EMBED CLASS='external-html' DATA-FILE-ID=B_NON_NESTED_C_RET>
488     * @throws HTMLTokException If any of the tags passed are not valid HTML tags.
489     * 
490     * @throws SingletonException If this {@code 'htmlTag'} is a {@code 'singleton'} (Self-Closing)
491     * Tag, this exception will throw.
492     * 
493     * @see FileRW#loadFileToString(String)
494     * @see HTMLPage#getPageTokens(CharSequence, boolean)
495     * @see Debug#print(Vector, int[], int, String, boolean, BiConsumer)
496     */
497    public static int[] nonNestedCheck(final Vector<? super TagNode> html, String htmlTag)
498    {
499        // Check that this is a valid HTML Tag, throw an exception if invalid
500        htmlTag = ARGCHECK.htmlTag(htmlTag);
501
502        if (HTMLTags.isSingleton(htmlTag)) throw new SingletonException(
503            "The tag you have passed: [" + htmlTag + "] is a singleton-tag, and is only " +
504            "allowed opening versions of the tag."
505        );
506
507
508        // Java Streams are an easier way to keep variable-length lists.  They use "builders".
509        // This one is for an "IntStream"
510
511        final IntStream.Builder b = IntStream.builder();
512
513        // Iterate through  HTML List, we are only counting HTML Elements, not text nor commeents
514        final int LEN = html.size();
515        TC last = null;
516
517        for (int i=0; i < LEN; i++)
518
519            if (html.elementAt(i) instanceof TagNode)
520            {
521                final TagNode tn = (TagNode) html.elementAt(i);
522                if (! tn.tok.equals(htmlTag)) continue;
523
524                if ((tn.isClosing)      && (last == TC.ClosingTags)) b.add(i);
525                if ((! tn.isClosing)    && (last == TC.OpeningTags)) b.add(i);
526
527                last = tn.isClosing ? TC.ClosingTags : TC.OpeningTags;
528            }
529
530        return b.build().toArray();
531    }
532
533    /**
534     * <EMBED CLASS='external-html' DATA-FILE-ID=B_LOC_DEPTH_DESC>
535     * @param html      <EMBED CLASS='external-html' DATA-FILE-ID=HTMLVECSUP>
536     * @param htmlTag   This the html element that has an imbalanced OPEN-CLOSE ratio in the tree.
537     * @return          <EMBED CLASS='external-html' DATA-FILE-ID=B_LOC_DEPTH_RET>
538     * 
539     * @throws HTMLTokException     throws if any of the tags passed are not valid HTML tags.
540     * @throws SingletonException   throws if {@code 'htmlTag'} is a 'singleton' - Self-Closing Tag
541     */
542    public static Ret2<int[], int[]> locationsAndDepth
543        (final Vector<? super TagNode> html, String htmlTag)
544    {
545        // Check that this is a valid HTML Tag, throw an exception if invalid
546        htmlTag = ARGCHECK.htmlTag(htmlTag);
547
548        if (HTMLTags.isSingleton(htmlTag)) throw new SingletonException(
549            "The tag you have passed: [" + htmlTag + "] is a singleton-tag, and is only " +
550            "allowed opening versions of the tag."
551        );
552
553
554        // Java Streams are an easier way to keep variable-length lists.  They use "builders".
555        // These builders are for an "IntStream"
556
557        final IntStream.Builder locations       = IntStream.builder();
558        final IntStream.Builder depthAtLocation = IntStream.builder();
559
560        // Iterate through the HTML List, we are only counting HTML Elements, not text or comments
561        final int LEN = html.size();
562        int depth = 0;
563
564        for (int i=0; i < LEN; i++)
565
566            if (html.elementAt(i) instanceof TagNode) 
567            {
568                final TagNode tn = (TagNode) html.elementAt(i);
569
570                if (! tn.tok.equals(htmlTag)) continue;
571
572                depth += tn.isClosing ? -1 : 1;
573                locations.add(i);
574                depthAtLocation.add(depth);
575            }
576
577        return new Ret2<int[], int[]>
578            (locations.build().toArray(), depthAtLocation.build().toArray());
579    }
580
581    /**
582     * Converts a depth report to a {@code String}, for printing.
583     * @param depthReport This should be a {@code Hashtable} returned by any of the depth-methods.
584     * @return This shall return the report as a {@code String}.
585     */
586    public static String toStringDepth(final Hashtable<String, int[]> depthReport)
587    {
588        final StringBuilder sb = new StringBuilder();
589
590        for (final String htmlTag : depthReport.keySet())
591        {
592            final int[] arr = depthReport.get(htmlTag);
593
594            sb.append(
595                "HTML Element: [" + htmlTag + "]:\t" +
596                "Min-Depth: " + arr[0] + ",\tMax-Depth: " + arr[1] + ",\tCount: " + arr[2] + "\n"
597            );
598        }
599
600        return sb.toString();
601    }
602
603
604    /**
605     * Converts a balance report to a {@code String}, for printing.
606     * 
607     * @param balanceCheckReport This should be a {@code Hashtable} returned by any of the
608     * balance-check methods.
609     * 
610     * @return This shall return the report as a {@code String}.
611     */
612    public static String toStringBalance(final Hashtable<String, Integer> balanceCheckReport)
613    {
614        final StringBuilder sb = new StringBuilder();
615
616        int maxTagLen = 0, maxValStrLen = 0, maxAbsValStrLen = 0;
617
618        // For good spacing purposes, we need the length of the longest of the tags.
619        for (final String htmlTag : balanceCheckReport.keySet())
620            if (htmlTag.length() > maxTagLen)
621                maxTagLen = htmlTag.length();
622
623        // 17 is the length of the string below, 2 is the amount of extra-space needed
624        maxTagLen += 17 + 2; 
625
626        for (final int v : balanceCheckReport.values())
627        {
628            final String vStr = "" + v;
629            if (vStr.length() > maxValStrLen) maxValStrLen = vStr.length();
630
631            final String absStr = "" + Math.abs(v);
632            if (absStr.length() > maxAbsValStrLen) maxAbsValStrLen = absStr.length();
633        }
634
635        int val = 0;
636        for (final String htmlTag : balanceCheckReport.keySet()) sb.append(
637            StringParse.rightSpacePad("HTML Element: [" + htmlTag + "]:", maxTagLen) +
638
639            StringParse.rightSpacePad(
640                ("" + (val = balanceCheckReport.get(htmlTag).intValue())),
641                maxValStrLen
642            ) +
643
644            NOTE(val, htmlTag, maxAbsValStrLen) +
645            "\n"
646        );
647
648        return sb.toString();
649    }
650
651    private static String NOTE(
652            final int       val,
653            final String    htmlTag,
654            final int       padding
655        )
656    {
657        if (val == 0) return "";
658
659        else if (val > 0) return
660            ", which implies " + StringParse.rightSpacePad("" + Math.abs(val), padding) +
661            " unclosed <" + htmlTag + "> element(s)";
662
663        else return
664            ", which implies " + StringParse.rightSpacePad("" + Math.abs(val), padding) +
665            " extra </" + htmlTag + "> element(s)";
666    }
667
668    /**
669     * Converts a balance report to a {@code String}, for printing.
670     * 
671     * @param balanceCheckReport This should be a {@code Hashtable} returned by any of the
672     * balance-check methods.
673     * 
674     * @return This shall return the report as a {@code String}.
675     * 
676     * @throws IllegalArgumentException This exception throws if the length of the two input arrays
677     * are not equal.  It is imperative that the balance report being printed was created by the
678     * html-tags that are listed in the HTML Token var-args parameter.  If the two arrays are the
679     * same length, but the tags used to create the report Hashtable are not the same ones being
680     * passed to the var-args parameter {@code 'htmlTags'} - <I>the logic will not know the
681     * difference, and no exception is thrown.</I>
682     */
683    public static String toStringBalance(
684            final int[]     balanceCheckReport,
685            final String... htmlTags
686        )
687    {
688        if (balanceCheckReport.length != htmlTags.length) throw new IllegalArgumentException(
689            "The balance report that you are checking was not generated using the html token " +
690            "list provided, they are different lengths.  balanceCheckReport.length: " +
691            "[" + balanceCheckReport.length + "]\t htmlTags.length: [" + htmlTags.length + "]"
692        );
693
694        final StringBuilder sb = new StringBuilder();
695
696        for (int i=0; i < balanceCheckReport.length; i++)
697            sb.append("HTML Element: [" + htmlTags[i] + "]:\t" + balanceCheckReport[i] + "\n");
698
699        return sb.toString();
700    }
701}