001package org.jsoup.nodes;
002
003import org.jsoup.helper.Validate;
004import org.jsoup.internal.QuietAppendable;
005import org.jsoup.internal.SharedConstants;
006import org.jsoup.internal.StringUtil;
007import org.jsoup.nodes.Document.OutputSettings.Syntax;
008import org.jsoup.parser.ParseSettings;
009import org.jspecify.annotations.Nullable;
010
011import java.util.AbstractMap;
012import java.util.AbstractSet;
013import java.util.ArrayList;
014import java.util.Arrays;
015import java.util.Collections;
016import java.util.ConcurrentModificationException;
017import java.util.HashMap;
018import java.util.HashSet;
019import java.util.Iterator;
020import java.util.List;
021import java.util.Map;
022import java.util.NoSuchElementException;
023import java.util.Objects;
024import java.util.Set;
025
026import static org.jsoup.internal.Normalizer.asciiLowerCase;
027import static org.jsoup.internal.Normalizer.equalsIgnoreAsciiCase;
028import static org.jsoup.nodes.Document.OutputSettings.Syntax.xml;
029import static org.jsoup.nodes.Range.AttributeRange.UntrackedAttr;
030
031/**
032 * The attributes of an Element.
033 * <p>
034 * During parsing, attributes in with the same name in an element are deduplicated, according to the configured parser's
035 * attribute case-sensitive setting. It is possible to have duplicate attributes subsequently if
036 * {@link #add(String, String)} vs {@link #put(String, String)} is used.
037 * </p>
038 * <p>
039 * Attribute name and value comparisons are generally <b>case-sensitive</b>. By default for HTML, attribute names are
040 * normalized to lower-case on parsing. That means you should use lower-case strings when referring to attributes by
041 * name. Case-insensitive name comparisons fold only ASCII letters; non-ASCII characters are compared as written.
042 * </p>
043 *
044 * @author Jonathan Hedley, jonathan@hedley.net
045 */
046public class Attributes implements Iterable<Attribute>, Cloneable {
047    // The Attributes object is only created on the first use of an attribute; the Element will just have a null
048    // Attribute slot otherwise
049
050    static final char InternalPrefix = '/'; // Indicates an internal key. Can't be set via HTML. (It could be set via accessor, but not too worried about that. Suppressed from list, iter, size.)
051    protected static final String dataPrefix = "data-"; // data attributes
052    private static final String EmptyString = "";
053
054    // manages the key/val arrays
055    private static final int InitialCapacity = 3; // sampling found mean count when attrs present = 1.49; 1.08 overall. 2.6:1 don't have any attrs.
056    private static final int GrowthFactor = 2;
057    static final int NotFound = -1;
058
059    // the number of instance fields is kept as low as possible giving an object size of 24 bytes
060    int size = 0; // number of slots used (not total capacity, which is keys.length). Package visible for actual size (incl internal)
061    @Nullable String[] keys = new String[InitialCapacity]; // keys is not null, but contents may be. Same for vals
062    @Nullable Object[] vals = new Object[InitialCapacity]; // Genericish: all non-internal attribute values must be Strings and are cast on access.
063    // todo - make keys iterable without creating Attribute objects
064
065    // check there's room for more
066    private void checkCapacity(int minNewSize) {
067        Validate.isTrue(minNewSize >= size);
068        int curCap = keys.length;
069        if (curCap >= minNewSize)
070            return;
071        int newCap = curCap >= InitialCapacity ? size * GrowthFactor : InitialCapacity;
072        if (minNewSize > newCap)
073            newCap = minNewSize;
074
075        keys = Arrays.copyOf(keys, newCap);
076        vals = Arrays.copyOf(vals, newCap);
077    }
078
079    int indexOfKey(String key) {
080        Validate.notNull(key);
081        for (int i = 0; i < size; i++) {
082            if (key.equals(keys[i]))
083                return i;
084        }
085        return NotFound;
086    }
087
088    /**
089     Finds a visible attribute's range index, skipping internal metadata slots.
090     */
091    int visibleIndexOfKey(String key) {
092        Validate.notNull(key);
093        int visible = 0;
094        for (int i = 0; i < size; i++) {
095            String attrKey = keys[i];
096            if (isInternalKey(attrKey))
097                continue;
098            if (key.equals(attrKey))
099                return visible;
100            visible++;
101        }
102        return NotFound;
103    }
104
105    /**
106     Maps an attribute array slot to the matching visible attribute index.
107     */
108    private int visibleIndex(int index) {
109        int visible = 0;
110        for (int i = 0; i < index; i++) {
111            if (!isInternalKey(keys[i]))
112                visible++;
113        }
114        return visible;
115    }
116
117    private int indexOfKeyIgnoreCase(String key) {
118        Validate.notNull(key);
119        for (int i = 0; i < size; i++) {
120            if (equalsIgnoreAsciiCase(key, keys[i]))
121                return i;
122        }
123        return NotFound;
124    }
125
126    /**
127     Boolean attributes have null values, and internal attributes may hold arbitrary objects; return empty for either.
128     */
129    static String checkNotNull(@Nullable Object val) {
130        return val instanceof String ? (String) val : EmptyString;
131    }
132
133    /**
134     Get an attribute value by key.
135     @param key the (case-sensitive) attribute key
136     @return the attribute value if set; or empty string if not set (or a boolean attribute).
137     @see #hasKey(String)
138     */
139    public String get(String key) {
140        int i = indexOfKey(key);
141        return i == NotFound ? EmptyString : checkNotNull(vals[i]);
142    }
143
144    /**
145     Get an Attribute by key. The Attribute will remain connected to these Attributes, so changes made via
146     {@link Attribute#setKey(String)}, {@link Attribute#setValue(String)} etc will cascade back to these Attributes and
147     their owning Element.
148     @param key the (case-sensitive) attribute key
149     @return the Attribute for this key, or null if not present.
150     @since 1.17.2
151     */
152    @Nullable public Attribute attribute(String key) {
153        int i = indexOfKey(key);
154        return i == NotFound ? null : new Attribute(key, checkNotNull(vals[i]), this);
155    }
156
157    /**
158     * Get an attribute's value by case-insensitive key
159     * @param key the attribute name
160     * @return the first matching attribute value if set; or empty string if not set (ora boolean attribute).
161     */
162    public String getIgnoreCase(String key) {
163        int i = indexOfKeyIgnoreCase(key);
164        return i == NotFound ? EmptyString : checkNotNull(vals[i]);
165    }
166
167    /**
168     * Adds a new attribute. Will produce duplicates if the key already exists.
169     * @see Attributes#put(String, String)
170     */
171    public Attributes add(String key, @Nullable String value) {
172        addObject(key, value);
173        return this;
174    }
175
176    private void addObject(String key, @Nullable Object value) {
177        checkCapacity(size + 1);
178        keys[size] = key;
179        vals[size] = value;
180        size++;
181    }
182
183    /**
184     * Set a new attribute, or replace an existing one by key.
185     * @param key case sensitive attribute key (not null)
186     * @param value attribute value (which can be null, to set a true boolean attribute)
187     * @return these attributes, for chaining
188     */
189    public Attributes put(String key, @Nullable String value) {
190        Validate.notNull(key);
191        int i = indexOfKey(key);
192        if (i != NotFound)
193            vals[i] = value;
194        else
195            addObject(key, value);
196        return this;
197    }
198
199    /**
200     Get the map holding any user-data associated with these Attributes. Will be created empty on first use. Held as
201     an internal attribute, not a field member, to reduce the memory footprint of Attributes when not used. Can hold
202     arbitrary objects; use for connecting W3C nodes to Elements, etc.
203     * @return the map holding user-data
204     */
205    @SuppressWarnings("unchecked")
206    Map<String, Object> userData() {
207        final Map<String, Object> userData;
208        int i = indexOfKey(SharedConstants.UserDataKey);
209        if (i == NotFound) {
210            userData = new HashMap<>();
211            addObject(SharedConstants.UserDataKey, userData);
212        } else {
213            userData = (Map<String, Object>) vals[i];
214        }
215        assert userData != null;
216        return userData;
217    }
218
219    /**
220     Check if these attributes have any user data associated with them.
221     */
222    boolean hasUserData() {
223        return hasKey(SharedConstants.UserDataKey);
224    }
225
226    /**
227     Get an arbitrary user-data object by key.
228     * @param key case-sensitive key to the object.
229     * @return the object associated to this key, or {@code null} if not found.
230     * @see #userData(String key, Object val)
231     * @since 1.17.1
232     */
233    @Nullable
234    public Object userData(String key) {
235        Validate.notNull(key);
236        if (!hasUserData()) return null; // no user data exists
237        Map<String, Object> userData = userData();
238        return userData.get(key);
239    }
240
241    /**
242     Set an arbitrary user-data object by key. Will be treated as an internal attribute, so will not be emitted in HTML.
243     * @param key case-sensitive key
244     * @param value object value. Providing a {@code null} value has the effect of removing the key from the userData map.
245     * @return these attributes
246     * @see #userData(String key)
247     * @since 1.17.1
248     */
249    public Attributes userData(String key, @Nullable Object value) {
250        Validate.notNull(key);
251        if (value == null && !hasKey(SharedConstants.UserDataKey)) return this; // no user data exists, so short-circuit
252        Map<String, Object> userData = userData();
253        if (value == null)  userData.remove(key);
254        else                userData.put(key, value);
255        return this;
256    }
257
258    /**
259     Gets the range spans, if source tracking was used.
260     */
261    Range.@Nullable Spans spans() {
262        int i = indexOfKey(SharedConstants.RangeSpansKey);
263        return i == NotFound ? null : (Range.Spans) vals[i];
264    }
265
266    /**
267     Gets or creates the range spans for this attributes object.
268     */
269    Range.Spans ensureSpans() {
270        Range.Spans rangeSpans = spans();
271        if (rangeSpans == null) {
272            rangeSpans = new Range.Spans();
273            addObject(SharedConstants.RangeSpansKey, rangeSpans);
274        }
275        return rangeSpans;
276    }
277
278    /** Sets the leaf node's source ranges without discarding existing attribute ranges. */
279    void putSpans(Range.Spans rangeSpans) {
280        int i = indexOfKey(SharedConstants.RangeSpansKey);
281        if (i == NotFound) {
282            addObject(SharedConstants.RangeSpansKey, rangeSpans);
283        } else {
284            Range.Spans attributeSpans = (Range.Spans) vals[i];
285            rangeSpans.copyAttributeRanges(attributeSpans);
286            vals[i] = rangeSpans;
287        }
288    }
289
290    void putIgnoreCase(String key, @Nullable String value) {
291        int i = indexOfKeyIgnoreCase(key);
292        if (i != NotFound) {
293            vals[i] = value;
294            String old = keys[i];
295            assert old != null;
296            if (!old.equals(key)) // case changed, update
297                keys[i] = key;
298        }
299        else
300            addObject(key, value);
301    }
302
303    /**
304     * Set a new boolean attribute. Removes the attribute if the value is false.
305     * @param key case <b>insensitive</b> attribute key
306     * @param value attribute value
307     * @return these attributes, for chaining
308     */
309    public Attributes put(String key, boolean value) {
310        if (value)
311            putIgnoreCase(key, null);
312        else
313            remove(key);
314        return this;
315    }
316
317    /**
318     Set a new attribute, or replace an existing one by key.
319     @param attribute attribute with case-sensitive key
320     @return these attributes, for chaining
321     */
322    public Attributes put(Attribute attribute) {
323        Validate.notNull(attribute);
324        put(attribute.getKey(), attribute.getValue());
325        attribute.parent = this;
326        return this;
327    }
328
329    // removes and shifts up
330    @SuppressWarnings("AssignmentToNull")
331    private void remove(int index) {
332        Validate.isFalse(index >= size);
333        Range.Spans rangeSpans = spans();
334        // Source ranges are stored by visible attribute index; internal metadata slots have no matching range record.
335        if (rangeSpans != null && !isInternalKey(keys[index]))
336            rangeSpans.removeAttributeRange(visibleIndex(index));
337
338        int shifted = size - index - 1;
339        if (shifted > 0) {
340            System.arraycopy(keys, index + 1, keys, index, shifted);
341            System.arraycopy(vals, index + 1, vals, index, shifted);
342        }
343        size--;
344        keys[size] = null; // release hold
345        vals[size] = null;
346    }
347
348    /**
349     Remove an attribute by key. <b>Case sensitive.</b>
350     @param key attribute key to remove
351     */
352    public void remove(String key) {
353        int i = indexOfKey(key);
354        if (i != NotFound)
355            remove(i);
356    }
357
358    /**
359     Remove an attribute by key. <b>Case insensitive.</b>
360     @param key attribute key to remove
361     */
362    public void removeIgnoreCase(String key) {
363        int i = indexOfKeyIgnoreCase(key);
364        if (i != NotFound)
365            remove(i);
366    }
367
368    /**
369     Tests if these attributes contain an attribute with this key.
370     @param key case-sensitive key to check for
371     @return true if key exists, false otherwise
372     */
373    public boolean hasKey(String key) {
374        return indexOfKey(key) != NotFound;
375    }
376
377    /**
378     Tests if these attributes contain an attribute with this key.
379     @param key key to check for
380     @return true if key exists, false otherwise
381     */
382    public boolean hasKeyIgnoreCase(String key) {
383        return indexOfKeyIgnoreCase(key) != NotFound;
384    }
385
386    /**
387     * Check if these attributes contain an attribute with a value for this key.
388     * @param key key to check for
389     * @return true if key exists, and it has a value
390     */
391    public boolean hasDeclaredValueForKey(String key) {
392        int i = indexOfKey(key);
393        return i != NotFound && vals[i] != null;
394    }
395
396    /**
397     * Check if these attributes contain an attribute with a value for this key.
398     * @param key case-insensitive key to check for
399     * @return true if key exists, and it has a value
400     */
401    public boolean hasDeclaredValueForKeyIgnoreCase(String key) {
402        int i = indexOfKeyIgnoreCase(key);
403        return i != NotFound && vals[i] != null;
404    }
405
406    /**
407     Get the number of attributes in this set, excluding any internal-only attributes (e.g. user data).
408     <p>Internal attributes are excluded from the {@link #html()}, {@link #asList()}, and {@link #iterator()}
409     methods.</p>
410
411     @return size
412     */
413    public int size() {
414        if (size == 0) return 0;
415        int count = 0;
416        for (int i = 0; i < size; i++) {
417            if (!isInternalKey(keys[i]))  count++;
418        }
419        return count;
420    }
421
422    /**
423     Test if this Attributes list is empty.
424     <p>This does not include internal attributes, such as user data.</p>
425     */
426    public boolean isEmpty() {
427        return size() == 0;
428    }
429
430    /**
431     Add all the attributes from the incoming set to this set.
432     @param incoming attributes to add to these attributes.
433     */
434    public void addAll(Attributes incoming) {
435        int incomingSize = incoming.size(); // not adding internal
436        if (incomingSize == 0) return;
437        checkCapacity(size + incomingSize);
438
439        boolean needsPut = size != 0; // if this set is empty, no need to check existing set, so can add() vs put()
440        // (and save bashing on the indexOfKey()
441        for (Attribute attr : incoming) {
442            if (needsPut)
443                put(attr);
444            else
445                addObject(attr.getKey(), attr.getValue());
446        }
447    }
448
449    /**
450     Get the source ranges (start to end position) in the original input source from which this attribute's <b>name</b>
451     and <b>value</b> were parsed.
452     <p>Position tracking must be enabled before parsing the content.</p>
453     @param key the attribute name
454     @return the ranges for the attribute's name and value, or {@code untracked} if the attribute does not exist or its range
455     was not tracked.
456     @see org.jsoup.parser.Parser#setTrackPosition(boolean)
457     @see Attribute#sourceRange()
458     @see Node#sourceRange()
459     @see Element#endSourceRange()
460     @since 1.17.1
461     */
462    public Range.AttributeRange sourceRange(String key) {
463        int index = visibleIndexOfKey(key);
464        if (index == NotFound) return UntrackedAttr;
465        Range.Spans rangeSpans = spans();
466        return rangeSpans != null ? rangeSpans.attributeRange(index) : UntrackedAttr;
467    }
468
469    @Override
470    public Iterator<Attribute> iterator() {
471        //noinspection ReturnOfInnerClass
472        return new Iterator<Attribute>() {
473            int expectedSize = size;
474            int i = 0;
475
476            @Override
477            public boolean hasNext() {
478                checkModified();
479                while (i < size) {
480                    String key = keys[i];
481                    assert key != null;
482                    if (isInternalKey(key)) // skip over internal keys
483                        i++;
484                    else
485                        break;
486                }
487
488                return i < size;
489            }
490
491            @Override
492            public Attribute next() {
493                checkModified();
494                if (i >= size) throw new NoSuchElementException();
495                String key = keys[i];
496                assert key != null;
497                final Attribute attr = new Attribute(key, (String) vals[i], Attributes.this);
498                i++;
499                return attr;
500            }
501
502            private void checkModified() {
503                if (size != expectedSize) throw new ConcurrentModificationException("Use Iterator#remove() instead to remove attributes while iterating.");
504            }
505
506            @Override
507            public void remove() {
508                Attributes.this.remove(--i); // next() advanced, so rewind
509                expectedSize--;
510            }
511        };
512    }
513
514    /**
515     Get the attributes as a List, for iteration.
516     @return a view of the attributes as an unmodifiable List.
517     */
518    public List<Attribute> asList() {
519        ArrayList<Attribute> list = new ArrayList<>(size);
520        for (int i = 0; i < size; i++) {
521            String key = keys[i];
522            assert key != null;
523            if (isInternalKey(key))
524                continue; // skip internal keys
525            Attribute attr = new Attribute(key, (String) vals[i], Attributes.this);
526            list.add(attr);
527        }
528        return Collections.unmodifiableList(list);
529    }
530
531    /**
532     * Retrieves a filtered view of attributes that are HTML5 custom data attributes; that is, attributes with keys
533     * starting with {@code data-}.
534     * @return map of custom data attributes.
535     */
536    public Map<String, String> dataset() {
537        return new Dataset(this);
538    }
539
540    /**
541     Get the HTML representation of these attributes.
542     @return HTML
543     */
544    public String html() {
545        StringBuilder sb = StringUtil.borrowBuilder();
546        html(QuietAppendable.wrap(sb), new Document.OutputSettings()); // output settings a bit funky, but this html() seldom used
547        return StringUtil.releaseBuilder(sb);
548    }
549
550    final void html(final QuietAppendable accum, final Document.OutputSettings out) {
551        final int sz = size;
552        final Syntax syntax = out.syntax();
553        @Nullable Set<String> usedKeys = null; // avoid allocation for normal serialization
554        for (int i = 0; i < sz; i++) {
555            String key = keys[i];
556            assert key != null;
557            if (isInternalKey(key))
558                continue;
559            String validated = Attribute.getValidKey(key, syntax);
560            if (!validated.equals(key)) {
561                if (usedKeys == null)
562                    usedKeys = collectSourceKeys(syntax); // valid source names win regardless of attribute order
563                // preserve repaired attributes without outputting duplicate names
564                while (!usedKeys.add(comparisonKey(validated, syntax)))
565                    validated = StringUtil.concat('_', validated);
566            }
567            Attribute.htmlNoValidate(validated, (String) vals[i], accum.append(' '), out);
568        }
569    }
570
571    /** Collects source keys so repaired names can be made unique without changing valid names. */
572    private Set<String> collectSourceKeys(Syntax syntax) {
573        Set<String> sourceKeys = new HashSet<>(size);
574        for (int i = 0; i < size; i++) {
575            String key = keys[i];
576            assert key != null;
577            if (!isInternalKey(key))
578                sourceKeys.add(comparisonKey(key, syntax));
579        }
580        return sourceKeys;
581    }
582
583    /** Normalizes a key for the output syntax's case sensitivity. */
584    private static String comparisonKey(String key, Syntax syntax) {
585        return syntax == xml ? key : asciiLowerCase(key);
586    }
587
588    /** Compares keys with the requested case sensitivity. */
589    private static boolean keysEqual(String first, String second, boolean caseSensitive) {
590        return caseSensitive ? first.equals(second) : equalsIgnoreAsciiCase(first, second);
591    }
592
593    @Override
594    public String toString() {
595        return html();
596    }
597
598    /**
599     Checks if these attributes have the same names and values as another set, ignoring attribute order. Internal
600     attributes, such as source ranges and user data, are excluded.
601
602     @param o attributes to compare with
603     @return if both sets of attributes have the same content
604     */
605    @Override
606    public boolean equals(@Nullable Object o) {
607        if (this == o) return true;
608        if (o == null || getClass() != o.getClass()) return false;
609
610        Attributes that = (Attributes) o;
611        if (size() != that.size()) return false;
612        for (int i = 0; i < size; i++) {
613            String key = keys[i];
614            assert key != null;
615            if (isInternalKey(key)) continue;
616
617            int here = 0, there = 0;
618            for (int j = 0; j < size; j++) {
619                if (key.equals(keys[j]) && Objects.equals(vals[i], vals[j])) here++;
620            }
621            for (int j = 0; j < that.size; j++) {
622                if (key.equals(that.keys[j]) && Objects.equals(vals[i], that.vals[j])) there++;
623            }
624            if (here != there) return false;
625        }
626        return true;
627    }
628
629    /**
630     Calculates an order-independent hash code from attribute names and values, excluding internal attributes.
631
632     @return calculated hashcode
633     */
634    @Override
635    public int hashCode() {
636        int result = 0;
637        for (int i = 0; i < size; i++) {
638            String key = keys[i];
639            assert key != null;
640            if (!isInternalKey(key))
641                result += 31 * key.hashCode() + Objects.hashCode(vals[i]);
642        }
643        return result;
644    }
645
646    @Override
647    @SuppressWarnings("unchecked")
648    public Attributes clone() {
649        Attributes clone;
650        try {
651            clone = (Attributes) super.clone();
652        } catch (CloneNotSupportedException e) {
653            throw new RuntimeException(e);
654        }
655        clone.size = size;
656        clone.keys = Arrays.copyOf(keys, size);
657        clone.vals = Arrays.copyOf(vals, size);
658
659        // make a copy of the user data map. (Contents are shallow).
660        int i = indexOfKey(SharedConstants.UserDataKey);
661        if (i != NotFound) {
662            clone.vals[i] = new HashMap<>((Map<String, Object>) vals[i]);
663        }
664
665        // make a copy of the range spans, if present.
666        i = indexOfKey(SharedConstants.RangeSpansKey);
667        if (i != NotFound) {
668            clone.vals[i] = ((Range.Spans) vals[i]).copy();
669        }
670
671        return clone;
672    }
673
674    /**
675     * Internal method. Lowercases all (non-internal) keys.
676     */
677    public void normalize() {
678        for (int i = 0; i < size; i++) {
679            assert keys[i] != null;
680            String key = keys[i];
681            assert key != null;
682            if (!isInternalKey(key))
683                keys[i] = asciiLowerCase(key);
684        }
685    }
686
687    /**
688     * Internal method. Removes duplicate attribute by name. Settings for case sensitivity of key names.
689     * @param settings case sensitivity
690     * @return number of removed dupes
691     */
692    public int deduplicate(ParseSettings settings) {
693        if (size == 0) return 0;
694        boolean preserve = settings.preserveAttributeCase();
695        int dupes = 0;
696        for (int i = 0; i < size; i++) {
697            String keyI = keys[i];
698            assert keyI != null;
699            for (int j = i + 1; j < size; j++) {
700                if (keysEqual(keyI, keys[j], preserve)) {
701                    dupes++;
702                    remove(j);
703                    j--;
704                }
705            }
706        }
707        return dupes;
708    }
709
710    private static class Dataset extends AbstractMap<String, String> {
711        private final Attributes attributes;
712
713        private Dataset(Attributes attributes) {
714            this.attributes = attributes;
715        }
716
717        @Override
718        public Set<Entry<String, String>> entrySet() {
719            return new EntrySet();
720        }
721
722        @Override
723        public String put(String key, String value) {
724            String dataKey = dataKey(key);
725            String oldValue = attributes.hasKey(dataKey) ? attributes.get(dataKey) : null;
726            attributes.put(dataKey, value);
727            return oldValue;
728        }
729
730        private class EntrySet extends AbstractSet<Map.Entry<String, String>> {
731
732            @Override
733            public Iterator<Map.Entry<String, String>> iterator() {
734                return new DatasetIterator();
735            }
736
737            @Override
738            public int size() {
739                int count = 0;
740                Iterator<Entry<String, String>> iter = new DatasetIterator();
741                while (iter.hasNext())
742                    count++;
743                return count;
744            }
745        }
746
747        private class DatasetIterator implements Iterator<Map.Entry<String, String>> {
748            private final Iterator<Attribute> attrIter = attributes.iterator();
749            private Attribute attr;
750            @Override public boolean hasNext() {
751                while (attrIter.hasNext()) {
752                    attr = attrIter.next();
753                    if (attr.isDataAttribute()) return true;
754                }
755                return false;
756            }
757
758            @Override public Entry<String, String> next() {
759                return new Attribute(attr.getKey().substring(dataPrefix.length()), attr.getValue());
760            }
761
762            @Override public void remove() {
763                attributes.remove(attr.getKey());
764            }
765        }
766    }
767
768    private static String dataKey(String key) {
769        return dataPrefix + key;
770    }
771
772    static String internalKey(String key) {
773        return InternalPrefix + key;
774    }
775
776    static boolean isInternalKey(String key) {
777        return key.length() > 1 && key.charAt(0) == InternalPrefix;
778    }
779}