1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.devacfr.maven.skins.reflow;
20
21 import javax.annotation.Nonnull;
22 import javax.annotation.Nullable;
23
24 import java.text.Normalizer;
25 import java.text.Normalizer.Form;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.HashSet;
31 import java.util.List;
32 import java.util.Locale;
33 import java.util.Map;
34 import java.util.Map.Entry;
35 import java.util.Set;
36 import java.util.Stack;
37 import java.util.regex.Pattern;
38
39 import com.google.common.base.Strings;
40 import com.google.common.collect.Lists;
41 import org.apache.velocity.tools.ToolContext;
42 import org.apache.velocity.tools.config.DefaultKey;
43 import org.apache.velocity.tools.generic.SafeConfig;
44 import org.apache.velocity.tools.generic.ValueParser;
45 import org.jsoup.Jsoup;
46 import org.jsoup.internal.StringUtil;
47 import org.jsoup.nodes.Document;
48 import org.jsoup.nodes.Element;
49 import org.jsoup.nodes.Node;
50 import org.jsoup.parser.Tag;
51
52 import static java.util.Collections.emptyList;
53 import static java.util.Objects.requireNonNull;
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69 @DefaultKey("htmlTool")
70 public class HtmlTool extends SafeConfig {
71
72 private static final int SLUG_SIZE = 50;
73
74
75 public static final String DEFAULT_SLUG_SEPARATOR = "-";
76
77
78 private static final String SEPARATOR_TOC = "_toc_";
79
80
81 private static final List<String> HEADINGS = Collections
82 .unmodifiableList(Arrays.asList("h1", "h2", "h3", "h4", "h5", "h6"));
83
84
85 public enum JoinSeparator {
86
87
88
89 AFTER,
90
91
92
93 BEFORE,
94
95 NO
96 }
97
98
99 private String outputEncoding = "UTF-8";
100
101 private boolean prettyPrint = true;
102
103
104
105
106
107
108 @Override
109 protected void configure(final ValueParser values) {
110
111
112 final Object velocityContext = values.get("velocityContext");
113
114 if (!(velocityContext instanceof ToolContext)) {
115 return;
116 }
117
118 final ToolContext ctxt = (ToolContext) velocityContext;
119
120
121 final Object outputEncodingObj = ctxt.get("outputEncoding");
122 if (outputEncodingObj instanceof String) {
123 this.outputEncoding = (String) outputEncodingObj;
124 }
125
126 final Object prettyPrint = ctxt.get("prettyPrint");
127 if (prettyPrint instanceof Boolean) {
128 this.prettyPrint = (Boolean) prettyPrint;
129 }
130 }
131
132
133
134
135
136
137
138
139
140 @Nullable public String normaliseWhitespace(@Nullable final String html) {
141 if (Strings.isNullOrEmpty(html)) {
142 return null;
143 }
144 return StringUtil.normaliseWhitespace(html);
145 }
146
147
148
149
150
151
152
153
154
155
156
157
158
159 public List<String> split(@Nonnull final String content, @Nonnull final String separatorCssSelector) {
160 return split(content, separatorCssSelector, JoinSeparator.NO);
161 }
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180 public List<String> splitOnStarts(final @Nonnull String content, final @Nonnull String separatorCssSelector) {
181
182 final List<String> result = split(content, separatorCssSelector, JoinSeparator.AFTER);
183
184 if (result == null || result.size() <= 1) {
185
186 return result;
187 }
188
189
190
191
192 return result.subList(1, result.size());
193 }
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209 public List<String> split(final @Nonnull String content,
210 final @Nonnull String separatorCssSelector,
211 final String separatorStrategy) {
212
213 JoinSeparator sepStrategy;
214 if ("before".equals(separatorStrategy)) {
215 sepStrategy = JoinSeparator.BEFORE;
216 } else if ("after".equals(separatorStrategy)) {
217 sepStrategy = JoinSeparator.AFTER;
218 } else {
219 sepStrategy = JoinSeparator.NO;
220 }
221
222 return split(content, separatorCssSelector, sepStrategy);
223 }
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243 public List<String> split(@Nonnull final String content,
244 @Nonnull final String separatorCssSelector,
245 @Nonnull final JoinSeparator separatorStrategy) {
246
247 requireNonNull(separatorStrategy);
248 final Element body = parse(content).body();
249
250 final List<Element> separators = body.select(separatorCssSelector);
251 if (separators.size() > 0) {
252 final List<List<Element>> partitions = split(separators, separatorStrategy, body);
253
254 final List<String> sectionHtml = new ArrayList<>();
255
256 for (final List<Element> partition : partitions) {
257 final String html = outerHtml(partition);
258 if (!Strings.isNullOrEmpty(html)) {
259 sectionHtml.add(outerHtml(partition));
260 }
261 }
262
263 return sectionHtml;
264 } else {
265
266 return Collections.singletonList(content);
267 }
268 }
269
270
271
272
273
274
275
276
277
278
279
280
281 private static List<List<Element>> split(final Collection<Element> separators,
282 final JoinSeparator separatorStrategy,
283 final Element parent) {
284
285 final List<List<Element>> partitions = Lists.newLinkedList();
286
287 for (final Element child : parent.children()) {
288
289 if (separators.contains(child)) {
290
291
292
293
294 getLastPartition(partitions);
295
296 if (separatorStrategy == JoinSeparator.BEFORE) {
297
298 getLastPartition(partitions).add(child);
299 }
300
301
302 final List<Element> newPartition = Lists.newLinkedList();
303 partitions.add(newPartition);
304
305 if (separatorStrategy == JoinSeparator.AFTER) {
306
307 newPartition.add(child);
308 }
309
310 } else {
311
312 final List<List<Element>> childPartitions = split(separators, separatorStrategy, child);
313
314
315 getLastPartition(partitions).add(child);
316
317 if (childPartitions.size() > 1) {
318
319
320
321
322 final List<Element> allChildren = child.children();
323 final List<Element> firstPartition = childPartitions.get(0);
324
325 allChildren.removeAll(firstPartition);
326 for (final Element removeChild : allChildren) {
327 removeChild.remove();
328 }
329
330
331 for (final List<Element> nextPartition : childPartitions.subList(1, childPartitions.size())) {
332 partitions.add(nextPartition);
333 }
334 }
335 }
336 }
337
338 return partitions;
339 }
340
341
342
343
344
345
346
347 private static List<Element> getLastPartition(final List<List<Element>> partitions) {
348 if (partitions.isEmpty()) {
349 final List<Element> newPartition = Lists.newLinkedList();
350 partitions.add(newPartition);
351 return newPartition;
352 } else {
353 return partitions.get(partitions.size() - 1);
354 }
355 }
356
357
358
359
360
361
362
363 private static String outerHtml(final List<Element> elements) {
364
365 switch (elements.size()) {
366 case 0:
367 return "";
368
369 case 1:
370 return elements.get(0).outerHtml();
371
372 default:
373
374
375 final Element root = new Element(Tag.valueOf("div"), "");
376 for (final Element elem : elements) {
377 root.appendChild(elem);
378 }
379
380 return root.html();
381 }
382 }
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397 public String reorderToTop(final String content, final String selector, final int amount) {
398 return reorderToTop(content, selector, amount, null);
399 }
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416 public String reorderToTop(final String content,
417 final String selector,
418 final int amount,
419 final String wrapRemaining) {
420
421
422 final List<Element> extracted = extractElements(content, selector, amount);
423
424 if (extracted.size() > 1) {
425
426 final Element body = extracted.get(0);
427
428 if (wrapRemaining != null) {
429 wrapInner(body, wrapRemaining);
430 }
431
432 final List<Element> elements = extracted.subList(1, extracted.size());
433
434
435
436 for (int index = elements.size() - 1; index >= 0; index--) {
437 body.prependChild(elements.get(index));
438 }
439
440 return body.html();
441 } else {
442
443 return content;
444 }
445 }
446
447 private static Element wrapInner(final Element element, final String html) {
448
449
450
451 final Element topDiv = new Element(Tag.valueOf("div"), "");
452 for (final Element topElem : element.children()) {
453
454 topElem.remove();
455 topDiv.appendChild(topElem);
456 }
457
458
459 element.appendChild(topDiv);
460
461
462 topDiv.wrap(html);
463
464 topDiv.unwrap();
465
466 return element;
467 }
468
469
470
471
472
473
474
475
476
477
478 private List<Element> extractElements(final String content, final String selector, final int amount) {
479
480 final Element body = parse(content).body();
481
482 List<Element> elements = body.select(selector);
483 if (elements.size() > 0) {
484
485 elements = filterParents(elements);
486
487 if (amount >= 0) {
488
489 elements = elements.subList(0, Math.min(amount, elements.size()));
490 }
491
492
493 for (final Element element : elements) {
494 element.remove();
495 }
496 }
497
498 final List<Element> results = new ArrayList<>();
499
500 results.add(body);
501 results.addAll(elements);
502 return results;
503 }
504
505
506
507
508
509
510
511
512 private static List<Element> filterParents(final List<Element> elements) {
513 final List<Element> filtered = new ArrayList<>();
514 for (final Element element : elements) {
515
516 final List<Element> parentsInter = element.parents();
517 parentsInter.retainAll(elements);
518 if (parentsInter.isEmpty()) {
519
520 filtered.add(element);
521 }
522 }
523
524 return filtered;
525 }
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542 @Nonnull
543 public ExtractResult extract(final String content, final String selector, final int amount) {
544
545 final List<Element> extracted = extractElements(content, selector, amount);
546
547 if (extracted.size() > 1) {
548
549
550 final Element body = extracted.get(0);
551 final List<Element> elements = extracted.subList(1, extracted.size());
552
553
554 final List<String> elementStr = new ArrayList<>();
555 for (final Element el : elements) {
556 elementStr.add(el.outerHtml());
557 }
558
559 return new DefaultExtractResult(elementStr, body.html());
560 } else {
561
562 return new DefaultExtractResult(Collections.<String> emptyList(), content);
563 }
564 }
565
566
567
568
569
570
571
572
573 public interface ExtractResult {
574
575
576
577
578
579
580 List<String> getExtracted();
581
582
583
584
585
586
587 String getRemainder();
588 }
589
590
591
592
593 private static final class DefaultExtractResult implements ExtractResult {
594
595
596 private final List<String> extracted;
597
598
599 private final String remainder;
600
601 private DefaultExtractResult(final List<String> extracted, final String remainder) {
602 this.extracted = extracted;
603 this.remainder = remainder;
604 }
605
606 @Override
607 public List<String> getExtracted() {
608 return Collections.unmodifiableList(extracted);
609 }
610
611 @Override
612 public String getRemainder() {
613 return remainder;
614 }
615 }
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631 public String setAttr(final String content, final String selector, final String attributeKey, final String value) {
632
633 final Element body = parse(content).body();
634
635 final List<Element> elements = body.select(selector);
636 if (elements.size() > 0) {
637
638 for (final Element element : elements) {
639 element.attr(attributeKey, value);
640 }
641
642 return body.html();
643 } else {
644
645 return content;
646 }
647 }
648
649
650
651
652
653
654
655
656 public Document parse(@Nonnull final String content) {
657 final Document doc = Jsoup.parseBodyFragment(content);
658 doc.outputSettings().charset(outputEncoding).prettyPrint(prettyPrint);
659 return doc;
660 }
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675 public List<String> getAttr(final String content, final String selector, final String attributeKey) {
676
677 final Element body = parse(content).body();
678
679 final List<Element> elements = body.select(selector);
680 final List<String> attrs = new ArrayList<>();
681
682 for (final Element element : elements) {
683 final String attrValue = element.attr(attributeKey);
684 attrs.add(attrValue);
685 }
686
687 return attrs;
688 }
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704 public String addClass(final String content,
705 final String selector,
706 final List<String> classNames,
707 final int amount) {
708
709 final Element body = parse(content).body();
710
711 List<Element> elements = body.select(selector);
712 if (amount >= 0) {
713
714 elements = elements.subList(0, Math.min(amount, elements.size()));
715 }
716
717 if (elements.size() > 0) {
718
719 for (final Element element : elements) {
720 for (final String className : classNames) {
721 element.addClass(className);
722 }
723 }
724
725 return body.html();
726 } else {
727
728 return content;
729 }
730 }
731
732
733
734
735
736
737
738
739
740
741
742
743
744 public String addClass(final String content, final String selector, final List<String> classNames) {
745 return addClass(content, selector, classNames, -1);
746 }
747
748
749
750
751
752
753
754
755
756
757
758
759
760 public String addClass(final String content, final String selector, final String className) {
761 return addClass(content, selector, Collections.singletonList(className));
762 }
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778 public String wrap(final String content, final String selector, final String wrapHtml, final int amount) {
779
780 final Element body = parse(content).body();
781
782 List<Element> elements = body.select(selector);
783 if (amount >= 0) {
784
785 elements = elements.subList(0, Math.min(amount, elements.size()));
786 }
787
788 if (elements.size() > 0) {
789
790 for (final Element element : elements) {
791 element.wrap(wrapHtml);
792 }
793
794 return body.html();
795 } else {
796
797 return content;
798 }
799 }
800
801
802
803
804
805
806
807
808
809
810
811 public String remove(final String content, final String selector) {
812
813 final Element body = parse(content).body();
814
815 final List<Element> elements = body.select(selector);
816 if (elements.size() > 0) {
817 for (final Element element : elements) {
818 element.remove();
819 }
820
821 return body.html();
822 } else {
823
824 return content;
825 }
826 }
827
828
829
830
831
832
833
834
835
836
837
838
839
840 public String replace(final String content, final String selector, final String replacement) {
841 return replaceAll(content, Collections.singletonMap(selector, replacement));
842 }
843
844
845
846
847
848
849
850
851
852
853
854
855 public String replaceAll(final String content, final Map<String, String> replacements) {
856
857 final Element body = parse(content).body();
858
859 boolean modified = false;
860 for (final Entry<String, String> replacementEntry : replacements.entrySet()) {
861 final String selector = replacementEntry.getKey();
862 final String replacement = replacementEntry.getValue();
863
864 final List<Element> elements = body.select(selector);
865 if (elements.size() > 0) {
866
867
868 final Element replacementElem = parse(replacement).body().child(0);
869
870 if (replacementElem != null) {
871 for (final Element element : elements) {
872 element.replaceWith(replacementElem.clone());
873 }
874
875 modified = true;
876 }
877 }
878 }
879
880 if (modified) {
881 return body.html();
882 } else {
883
884 return content;
885 }
886 }
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901 public String replaceWith(final String content, final String selector, final String newElement) {
902
903 final Element body = parse(content).body();
904
905 boolean modified = false;
906 final List<Element> elements = body.select(selector);
907 if (elements.size() > 0) {
908
909
910 final Element replacementElem = parse(newElement).body().child(0);
911
912 if (replacementElem != null) {
913 for (final Element element : elements) {
914 final List<Node> children = element.childNodes();
915 final Element el = replacementElem.clone();
916 for (final Node child : children) {
917 el.appendChild(child.clone());
918 }
919 element.replaceWith(el);
920 }
921
922 modified = true;
923 }
924 }
925
926 if (modified) {
927 return body.html();
928 } else {
929
930 return content;
931 }
932 }
933
934
935
936
937
938
939
940
941
942
943
944
945 public List<String> text(@Nullable final String content, @Nonnull final String selector) {
946 if (Strings.isNullOrEmpty(content)) {
947 return emptyList();
948 }
949 final Element body = parse(content).body();
950
951 final List<Element> elements = body.select(selector);
952 final List<String> texts = new ArrayList<>();
953
954 for (final Element element : elements) {
955 texts.add(element.text());
956 }
957
958 return texts;
959 }
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983 public String headingAnchorToId(final String content) {
984
985 final Element body = parse(content).body();
986
987
988 final List<String> headNoIds = concat(HEADINGS, ":not([id])", true);
989
990
991 final String nameA = "a[name]:not([href])";
992
993
994 final List<Element> headingsInnerA = body
995 .select(String.join(", ", concat(headNoIds, ":has(" + nameA + ")", true)));
996
997 boolean modified = false;
998 for (final Element heading : headingsInnerA) {
999 final List<Element> anchors = heading.select(nameA);
1000
1001 if (!anchors.isEmpty()) {
1002 anchorToId(heading, anchors.get(0));
1003 modified = true;
1004 }
1005 }
1006
1007
1008 final List<Element> headingsPreA = body.select(String.join(", ", concat(headNoIds, nameA + " + ", false)));
1009
1010 for (final Element heading : headingsPreA) {
1011 final Element anchor = heading.previousElementSibling();
1012 if (anchor != null) {
1013 anchorToId(heading, anchor);
1014 modified = true;
1015 }
1016 }
1017
1018
1019
1020
1021 final List<Element> anchorsPreH = body.select(String.join(", ", concat(headNoIds, " + " + nameA, true)));
1022
1023 for (final Element anchor : anchorsPreH) {
1024 final Element heading = anchor.previousElementSibling();
1025 if (heading != null) {
1026 anchorToId(heading, anchor);
1027 modified = true;
1028 }
1029 }
1030
1031 if (modified) {
1032 return body.html();
1033 } else {
1034
1035 return content;
1036 }
1037 }
1038
1039
1040
1041
1042
1043
1044
1045 private static void anchorToId(final Element heading, final Element anchor) {
1046
1047 if ("a".equals(anchor.tagName()) && heading.id().isEmpty()) {
1048 final String aName = anchor.attr("name");
1049 if (!aName.isEmpty()) {
1050
1051 heading.attr("id", aName);
1052
1053
1054 anchor.remove();
1055 }
1056 }
1057 }
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071 public static List<String> concat(final List<String> elements, final String text, final boolean append) {
1072 final List<String> concats = new ArrayList<>();
1073
1074 for (final String element : elements) {
1075 concats.add(append ? element + text : text + element);
1076 }
1077
1078 return concats;
1079 }
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104 public String ensureHeadingIds(final String pageType,
1105 final String currentPage,
1106 final String content,
1107 final String idSeparator) {
1108 final List<String> excludedPages = Arrays.asList("checkstyle-aggregate", "checkstyle");
1109
1110 final Element body = parse(content).body();
1111
1112
1113 if (excludedPages.contains(currentPage)) {
1114 return content;
1115 }
1116
1117
1118 final List<Element> idElems = body.select("*[id]");
1119
1120 final Set<String> ids = new HashSet<>();
1121 boolean modified = false;
1122 for (final Element idElem : idElems) {
1123
1124
1125 final String id = idElem.id();
1126 idElem.attr("id", slug(id, idSeparator));
1127 modified = true;
1128
1129 ids.add(idElem.id());
1130 }
1131
1132
1133 final List<String> headIds = concat(HEADINGS, "[id]", true);
1134
1135 final List<Element> headingIds = body.select(String.join(", ", headIds));
1136
1137 for (final Element heading : headingIds) {
1138 final String headingText = heading.text();
1139 String headingSlug = slug(headingText, idSeparator);
1140
1141 if (headingSlug.length() > SLUG_SIZE) {
1142 headingSlug = headingSlug.substring(0, SLUG_SIZE);
1143 }
1144 final String headingId = generateUniqueId(pageType, currentPage, ids, headingSlug);
1145
1146 heading.attr("id", headingId);
1147 }
1148
1149 final List<String> headNoIds = concat(HEADINGS, ":not([id])", true);
1150
1151
1152 final List<Element> headingsNoId = body.select(String.join(", ", headNoIds));
1153
1154 if (!headingsNoId.isEmpty() || modified) {
1155 for (final Element heading : headingsNoId) {
1156
1157 final String headingText = heading.text();
1158 String headingSlug = slug(headingText, idSeparator);
1159
1160 if (headingSlug.length() > SLUG_SIZE) {
1161 headingSlug = headingSlug.substring(0, SLUG_SIZE);
1162 }
1163 final String headingId = generateUniqueId(pageType, currentPage, ids, headingSlug);
1164
1165 heading.attr("id", headingId);
1166 }
1167 }
1168
1169 return body.html();
1170 }
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185 private static String generateUniqueId(final String pageType,
1186 final String currentPage,
1187 final Set<String> ids,
1188 final String idBase) {
1189 String id = idBase;
1190 int counter = 1;
1191 while (ids.contains(id)) {
1192 id = idBase + String.valueOf(counter++);
1193 }
1194
1195
1196 ids.add(id);
1197 if ("frame".equals(pageType)) {
1198 id = currentPage + SEPARATOR_TOC + id;
1199 }
1200 return id;
1201 }
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214 public String fixTableHeads(final String content) {
1215
1216 final Element body = parse(content).body();
1217
1218 final List<Element> tables = body.select("table");
1219
1220 for (final Element table : tables) {
1221
1222 final List<Element> tableHeadRows = table.select("tbody > tr:has(th)");
1223
1224 if (tableHeadRows.size() == 1) {
1225
1226 for (final Element row : tableHeadRows) {
1227
1228
1229 row.remove();
1230
1231
1232 final Element thead = new Element(Tag.valueOf("thead"), "");
1233 thead.appendChild(row);
1234
1235 table.prependChild(thead);
1236 }
1237 }
1238 }
1239 return body.html();
1240 }
1241
1242
1243 private static final Pattern NONLATIN = Pattern.compile("[^\\w-]");
1244
1245
1246 private static final Pattern WHITESPACE = Pattern.compile("[\\s]");
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257 public static String slug(final String input) {
1258 return slug(input, DEFAULT_SLUG_SEPARATOR);
1259 }
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273 private static String slug(final String input, final String separator) {
1274 final String nowhitespace = WHITESPACE.matcher(input).replaceAll(separator);
1275 final String normalized = Normalizer.normalize(nowhitespace, Form.NFD);
1276 return NONLATIN.matcher(normalized).replaceAll("").toLowerCase(Locale.ENGLISH);
1277 }
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295 public List<? extends IdElement> headingTree(final String content, final List<String> sections) {
1296
1297 final List<String> sectionContents = this.split(content, "hr");
1298 final List<String> headIds = concat(HEADINGS, "[id]:not(.no-anchor)", true);
1299 final List<HeadingItem> headingItems = new ArrayList<>();
1300
1301 int index = 0;
1302 for (final String sectionContent : sectionContents) {
1303 final String sectionType = index < sections.size() ? sections.get(index++) : "";
1304
1305
1306 if ("carousel".equals(sectionType)) {
1307 continue;
1308 }
1309 final Element body = parse(sectionContent).body();
1310
1311 final List<Element> headings = body.select(String.join(", ", headIds));
1312 for (final Element heading : headings) {
1313 headingItems
1314 .add(new HeadingItem(heading.id(), heading.nodeName(), heading.text(), headingIndex(heading)));
1315 }
1316 }
1317
1318 final List<HeadingItem> topHeadings = new ArrayList<>();
1319 final Stack<HeadingItem> parentHeadings = new Stack<>();
1320
1321 for (final HeadingItem heading : headingItems) {
1322
1323 while (!parentHeadings.isEmpty() && parentHeadings.peek().headingLevel >= heading.headingLevel) {
1324 parentHeadings.pop();
1325 }
1326
1327 if (parentHeadings.isEmpty()) {
1328
1329 topHeadings.add(heading);
1330 } else {
1331
1332 parentHeadings.peek().children.add(heading);
1333 }
1334
1335
1336 parentHeadings.push(heading);
1337 }
1338
1339 return topHeadings;
1340 }
1341
1342
1343
1344
1345
1346
1347
1348 private static int headingIndex(final Element element) {
1349 final String tagName = element.tagName();
1350 if (tagName.startsWith("h")) {
1351 try {
1352 return Integer.parseInt(tagName.substring(1));
1353 } catch (final Exception ex) {
1354 throw new IllegalArgumentException("Must be a header tag: " + tagName, ex);
1355 }
1356 } else {
1357 throw new IllegalArgumentException("Must be a header tag: " + tagName);
1358 }
1359 }
1360
1361
1362
1363
1364 private static final class HeadingItem implements IdElement {
1365
1366
1367 private final String id;
1368
1369
1370 private final String tagName;
1371
1372
1373 private final String text;
1374
1375
1376 private final int headingLevel;
1377
1378
1379 private final List<HeadingItem> children = new ArrayList<>();
1380
1381 private HeadingItem(final String id, final String tagName, final String text, final int headingLevel) {
1382 this.id = id;
1383 this.tagName = tagName;
1384 this.text = text;
1385 this.headingLevel = headingLevel;
1386 }
1387
1388 @Override
1389 public String getId() {
1390 return id;
1391 }
1392
1393 @Override
1394 public String getTagName() {
1395 return tagName;
1396 }
1397
1398 @Override
1399 public String getText() {
1400 return text;
1401 }
1402
1403 @Override
1404 public List<HeadingItem> getItems() {
1405 return Collections.unmodifiableList(children);
1406 }
1407
1408 @Override
1409 public int getHeadingLevel() {
1410 return headingLevel;
1411 }
1412 }
1413
1414
1415
1416
1417
1418
1419
1420 public interface IdElement {
1421
1422
1423
1424
1425
1426
1427 String getId();
1428
1429
1430
1431
1432 String getTagName();
1433
1434
1435
1436
1437
1438
1439 String getText();
1440
1441
1442
1443
1444 int getHeadingLevel();
1445
1446
1447
1448
1449
1450
1451 List<? extends IdElement> getItems();
1452 }
1453 }