1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
package Torello.HTML;

import Torello.HTML.helper.AttrRegEx;

import Torello.Java.StringParse;

import java.util.Map;
import java.util.AbstractMap;

import java.util.function.Predicate;
import java.util.function.IntFunction;
import java.util.function.IntPredicate;

import java.util.regex.Matcher;

class HasAndTest
{
    static boolean testAV(
            final TagNode           tn,
            final String            attributeName,
            final Predicate<String> attributeValueTest
        )
    {
        // Closing TagNode's (</DIV>, </A>) cannot attributes, or attribute-values
        if (tn.isClosing) return false;


        // OPTIMIZATION: TagNode's whose String-length is less than this computed length 
        // are simply too short to have the attribute named by the input parameter

        if (tn.str.length() < (tn.tok.length() + attributeName.length() + 4)) return false;


        // This Reg-Ex will allow us to iterate through each attribute key-value pair
        // contained / 'inside' this instance of TagNode.

        Matcher m = AttrRegEx.KEY_VALUE_REGEX.matcher(tn.str);


        // Test each attribute key-value pair, and return the test results if an attribute
        // whose name matches 'attributeName' is found.

        while (m.find())
            if (m.group(2).equalsIgnoreCase(attributeName))
                return attributeValueTest.test
                    (StringParse.ifQuotesStripQuotes(m.group(3)));


        // No attribute key-value pair was found whose 'key' matched input-parameter
        // 'attributeName'

        return false;
    }

    static boolean hasLogicOp(
            final TagNode               tn,
            final boolean               checkAttributeStringsForErrors,
            final IntFunction<Boolean>  onMatchFunction,
            final IntPredicate          reachedEndFunction,
            final String...             attributesInput
        )
    {
        ClosingTagNodeException.check(tn);

        // Keep a tally of how many of the input attributes are found
        int matchCount = 0;

        // Don't clobber the user's input
        String[] attributes = attributesInput.clone();

        // If no attributes are passed to 'attributes' parameter, throw exception.
        if (attributes.length == 0) throw new IllegalArgumentException
            ("Input variable-length String[] array parameter, 'attributes', has length zero.");


        // OPTIMIZATION: TagNode's whose String-length is less than this computed length 
        // are simply too short to have any attribute-value pairs.
        // 4 (characters) are: '<', '>', ' ' and 'X'
        // SHORTEST POSSIBLE SUCH-ELEMENT: <DIV X>
        //
        // This TagNode doesn't have any attributes in it.
        // There is no need to check anything, so return FALSE immediately ("OR" fails)

        if (tn.str.length() < (tn.tok.length() + 4)) return false;

        if (checkAttributeStringsForErrors) InnerTagKeyException.check(attributes);


        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
        // Check the main key=value attributes
        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
        // 
        // Get all inner-tag key-value pairs.  If even one of these is inside the 'attributes'
        // input-parameter string-array,  Then we must return true, since this is OR

        Matcher keyValueInnerTags = AttrRegEx.KEY_VALUE_REGEX.matcher(tn.str);

        while (keyValueInnerTags.find())
        {
            // Retrieve the key of the key-value pair
            String innerTagKey = keyValueInnerTags.group(2);                   

            // Iterate every element of the String[] 'attributes' parameter
            SECOND_FROM_TOP:
            for (int i=0; i < attributes.length; i++)


                // No need to check attributes that have already been matched.
                // When an attribute matches, it's place in the array is set to null

                if (attributes[i] != null)


                    // Does the latest keyOnlyInnerTag match one of the user-requested
                    // attribute names?

                    if (innerTagKey.equalsIgnoreCase(attributes[i]))
                    {
                        // NAND & OR will exit immediately on a match.  XOR and AND
                        // will return 'null' meaning they are not sure yet.

                        Boolean whatToDo = onMatchFunction.apply(++matchCount);

                        if (whatToDo != null) return whatToDo;


                        // Increment the matchCounter, if this ever reaches the length
                        // of the input array, there is no need to continue with the loop

                        if (matchCount == attributes.length)
                            return reachedEndFunction.test(matchCount); 


                        // There are still more matches to be found (not every element in
                        // this array is null yet)... Keep Searching, but eliminated the
                        // recently identified attribute from the list, because it has
                        // already been found.

                        attributes[i] = null;
                        continue SECOND_FROM_TOP;
                    }
        }


        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
        // Check the main key-only (Boolean) Attributes
        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
        //
        // Also check the "Boolean Attributes" also known as the "Key-Word Only Attributes"
        // Use the "Inverse Reg-Ex Matcher" (which matches all the strings that are "between" the
        // real matches)
        //
        // substring eliminates the leading "<TOK ..." and the trailing '>' character
        // 2: '<' character *PLUS* the space (' ') character

        String strToSplit = tn.str.substring(
            2 + tn.tok.length(),
            tn.str.length() - ((tn.str.charAt(tn.str.length() - 2) == '/') ? 2 : 1)
        ).trim();

        // 'split' => inverse-matches  (text between KEY-VALUE pairs)
        for (String unMatchedStr : AttrRegEx.KEY_VALUE_REGEX.split(strToSplit))  

            // Of that stuff, now do a white-space split for connected characters
            SECOND_FROM_TOP:
            for (String keyOnlyInnerTag : unMatchedStr.split("\\s+"))          

                // Just-In-Case, usually not necessary
                if ((keyOnlyInnerTag = keyOnlyInnerTag.trim()).length() > 0)

                    // Iterate all the input-parameter String-array attributes
                    for (int i=0; i < attributes.length; i++)


                        // No need to check attributes that have already been matched.
                        // When an attribute matches, it's place in the array is set to null

                        if (attributes[i] != null)


                            // Does the latest keyOnlyInnerTag match one of the user-requested
                            // attribute names?

                            if (keyOnlyInnerTag.equalsIgnoreCase(attributes[i]))
                            {
                                // NAND & OR will exit immediately on a match.  XOR and AND
                                // will return 'null' meaning they are not sure yet.

                                Boolean whatToDo = onMatchFunction.apply(++matchCount);

                                if (whatToDo != null) return whatToDo;


                                // Increment the matchCounter, if this ever reaches the length
                                // of the input array, there is no need to continue with the loop
        
                                if (matchCount == attributes.length)
                                    return reachedEndFunction.test(matchCount); 


                                // There are still more matches to be found (not every element in
                                // this array is null yet)... Keep Searching, but eliminated the
                                // recently identified attribute from the list, because it has
                                // already been found.

                                attributes[i] = null;
                                continue SECOND_FROM_TOP;
                            }

        // Let them know how many matches there were
        return reachedEndFunction.test(matchCount);
    }

    static boolean has(
            final TagNode           tn,
            final Predicate<String> attributeNameTest
        )
    {
        // Closing HTML Elements may not have attribute-names or values.
        // Exit gracefully, and immediately.

        if (tn.isClosing) return false;


        // OPTIMIZATION: TagNode's whose String-length is less than this computed length 
        // are simply too short to have such an attribute-value pair.
        // 4 (characters) are: '<', '>', ' ' and 'X'
        // SHORTEST POSSIBLE SUCH-ELEMENT: <DIV X>

        if (tn.str.length() < (tn.tok.length() + 4)) return false;


        // Get all inner-tag key-value pairs.  If any of them match with the 'attributeNameTest'
        // Predicate, return TRUE immediately.

        Matcher keyValueInnerTags = AttrRegEx.KEY_VALUE_REGEX.matcher(tn.str);

        // the matcher.group(2) has the key (not the value)
        while (keyValueInnerTags.find())
            if (attributeNameTest.test(keyValueInnerTags.group(2)))
                return true;


        // Also check the "Boolean Attributes" also known as the "Key-Word Only Attributes"
        // Use the "Inverse Reg-Ex Matcher" (which matches all the strings that are "between" the
        // real matches)
        // 
        // 'split' => inverse-matches  (text between KEY-VALUE pairs)

        for (String unMatchedStr : AttrRegEx.KEY_VALUE_REGEX.split(tn.str))  

            // Of that stuff, now do a white-space split for connected characters
            for (String keyOnlyInnerTag : unMatchedStr.split("\\s+"))          

                // Just-In-Case, usually not necessary
                if ((keyOnlyInnerTag = keyOnlyInnerTag.trim()).length() > 0)   

                    if (attributeNameTest.test(keyOnlyInnerTag))
                        return true;

        // A match was not found in either the "key-value pairs", or the boolean "key-only list."
        return false;
    }

    static Map.Entry<String, String> hasValue(
            final TagNode           tn,
            final Predicate<String> attributeValueTest,
            final boolean           retainQuotes,
            final boolean           preserveKeysCase
        )
    {
        // Closing HTML Elements may not have attribute-names or values.
        // Exit gracefully, and immediately.

        if (tn.isClosing) return null;


        // OPTIMIZATION: TagNode's whose String-length is less than this computed length 
        // are simply too short to have such an attribute-value pair.
        // 5 (characters) are: '<', '>', ' ', 'X' and '=' 
        // SHORTEST POSSIBLE SUCH-ELEMENT: <DIV X=>

        if (tn.str.length() < (tn.tok.length() + 5)) return null;


        // Get all inner-tag key-value pairs.  If any are 'equal' to parameter attributeName,
        // return TRUE immediately.

        Matcher keyValueInnerTags = AttrRegEx.KEY_VALUE_REGEX.matcher(tn.str);

        while (keyValueInnerTags.find())
        {
            // Matcher.group(3) has the key's value, of the inner-tag key-value pair
            // (matcher.group(2) has the key's name)

            String foundAttributeValue = keyValueInnerTags.group(3);


            // The comparison must be performed on the version of the value that DOES NOT HAVE the
            // surrounding quotation-marks

            String foundAttributeValueNoQuotes =
                StringParse.ifQuotesStripQuotes(foundAttributeValue);


            // Matcher.group(3) has the key-value, make sure to remove quotation marks (if present)
            // before comparing.

            if (attributeValueTest.test(foundAttributeValueNoQuotes))


                // matcher.group(2) has the key's name, not the value.  This is returned via the
                // Map.Entry key

                return retainQuotes

                    ? new AbstractMap.SimpleImmutableEntry<>(
                        preserveKeysCase
                            ? keyValueInnerTags.group(2)
                            : keyValueInnerTags.group(2).toLowerCase(),
                        foundAttributeValue
                    )

                    : new AbstractMap.SimpleImmutableEntry<>(
                        preserveKeysCase
                            ? keyValueInnerTags.group(2)
                            : keyValueInnerTags.group(2).toLowerCase(),
                        foundAttributeValueNoQuotes
                    );
        }

        // No match was identified, return null.
        return null;
    }
}