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}