Add attributes to start tags

This commit is contained in:
Dimitry Ivanov 2018-08-17 15:58:25 +03:00
parent bf8ff03b1c
commit be484765da
5 changed files with 147 additions and 86 deletions

View File

@ -4,6 +4,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* @see Inline * @see Inline
@ -32,6 +33,9 @@ public interface HtmlTag {
*/ */
boolean isEmpty(); boolean isEmpty();
@NonNull
Map<String, String> attributes();
/** /**
* Represents <em>really</em> inline HTML tags (unline commonmark definitions) * Represents <em>really</em> inline HTML tags (unline commonmark definitions)
*/ */

View File

@ -5,6 +5,7 @@ import android.support.annotation.Nullable;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
abstract class HtmlTagImpl implements HtmlTag { abstract class HtmlTagImpl implements HtmlTag {
@ -12,11 +13,13 @@ abstract class HtmlTagImpl implements HtmlTag {
final String name; final String name;
final int start; final int start;
final Map<String, String> attributes;
int end = NO_VALUE; int end = NO_VALUE;
protected HtmlTagImpl(@NonNull String name, int start) { protected HtmlTagImpl(@NonNull String name, int start, @NonNull Map<String, String> attributes) {
this.name = name; this.name = name;
this.start = start; this.start = start;
this.attributes = attributes;
} }
@NonNull @NonNull
@ -40,16 +43,23 @@ abstract class HtmlTagImpl implements HtmlTag {
return start == end; return start == end;
} }
@NonNull
@Override
public Map<String, String> attributes() {
return attributes;
}
boolean isClosed() { boolean isClosed() {
return end > NO_VALUE; return end > NO_VALUE;
} }
abstract void closeAt(int end); abstract void closeAt(int end);
static class InlineImpl extends HtmlTagImpl implements Inline { static class InlineImpl extends HtmlTagImpl implements Inline {
InlineImpl(@NonNull String name, int start) { InlineImpl(@NonNull String name, int start, @NonNull Map<String, String> attributes) {
super(name, start); super(name, start, attributes);
} }
@Override @Override
@ -65,6 +75,7 @@ abstract class HtmlTagImpl implements HtmlTag {
"name='" + name + '\'' + "name='" + name + '\'' +
", start=" + start + ", start=" + start +
", end=" + end + ", end=" + end +
", attributes=" + attributes +
'}'; '}';
} }
} }
@ -74,20 +85,28 @@ abstract class HtmlTagImpl implements HtmlTag {
@NonNull @NonNull
static BlockImpl root() { static BlockImpl root() {
//noinspection ConstantConditions //noinspection ConstantConditions
return new BlockImpl("", 0, null); return new BlockImpl("", 0, null, null);
} }
@NonNull @NonNull
static BlockImpl create(@NonNull String name, int start, @NonNull BlockImpl parent) { static BlockImpl create(
return new BlockImpl(name, start, parent); @NonNull String name,
int start,
@NonNull Map<String, String> attributes,
@NonNull BlockImpl parent) {
return new BlockImpl(name, start, attributes, parent);
} }
final BlockImpl parent; final BlockImpl parent;
List<BlockImpl> children; List<BlockImpl> children;
@SuppressWarnings("NullableProblems") @SuppressWarnings("NullableProblems")
BlockImpl(@NonNull String name, int start, @NonNull BlockImpl parent) { BlockImpl(
super(name, start); @NonNull String name,
int start,
@NonNull Map<String, String> attributes,
@NonNull BlockImpl parent) {
super(name, start, attributes);
this.parent = parent; this.parent = parent;
} }
@ -127,12 +146,24 @@ abstract class HtmlTagImpl implements HtmlTag {
return (List<Block>) (List<? extends Block>) children; return (List<Block>) (List<? extends Block>) children;
} }
@NonNull
@Override
public Map<String, String> attributes() {
//noinspection ConstantConditions
if (attributes == null) {
throw new IllegalStateException("#attributes() getter was called on the root node " +
"which should not be exposed outside internal usage");
}
return attributes;
}
@Override @Override
public String toString() { public String toString() {
return "BlockImpl{" + return "BlockImpl{" +
"name='" + name + '\'' + "name='" + name + '\'' +
", start=" + start + ", start=" + start +
", end=" + end + ", end=" + end +
", attributes=" + attributes +
", parent=" + (parent != null ? parent.name : null) + ", parent=" + (parent != null ? parent.name : null) +
", children=" + children + ", children=" + children +
'}'; '}';

View File

@ -7,14 +7,19 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set; import java.util.Set;
import ru.noties.markwon.html.HtmlTag.Block; import ru.noties.markwon.html.HtmlTag.Block;
import ru.noties.markwon.html.HtmlTag.Inline; import ru.noties.markwon.html.HtmlTag.Inline;
import ru.noties.markwon.html.HtmlTagImpl.BlockImpl; import ru.noties.markwon.html.HtmlTagImpl.BlockImpl;
import ru.noties.markwon.html.HtmlTagImpl.InlineImpl; import ru.noties.markwon.html.HtmlTagImpl.InlineImpl;
import ru.noties.markwon.html.jsoup.nodes.Attribute;
import ru.noties.markwon.html.jsoup.nodes.Attributes;
import ru.noties.markwon.html.jsoup.parser.CharacterReader; import ru.noties.markwon.html.jsoup.parser.CharacterReader;
import ru.noties.markwon.html.jsoup.parser.ParseErrorList; import ru.noties.markwon.html.jsoup.parser.ParseErrorList;
import ru.noties.markwon.html.jsoup.parser.Token; import ru.noties.markwon.html.jsoup.parser.Token;
@ -209,7 +214,7 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
final String name = startTag.normalName; final String name = startTag.normalName;
final InlineImpl inline = new InlineImpl(name, output.length()); final InlineImpl inline = new InlineImpl(name, output.length(), extractAttributes(startTag));
if (isVoidTag(name) if (isVoidTag(name)
|| startTag.selfClosing) { || startTag.selfClosing) {
@ -271,7 +276,7 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
final int start = output.length(); final int start = output.length();
final BlockImpl block = BlockImpl.create(name, start, currentBlock); final BlockImpl block = BlockImpl.create(name, start, extractAttributes(startTag), currentBlock);
final boolean isVoid = isVoidTag(name) || startTag.selfClosing; final boolean isVoid = isVoidTag(name) || startTag.selfClosing;
if (isVoid) { if (isVoid) {
@ -396,4 +401,25 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
append(output, "\n"); append(output, "\n");
} }
} }
@NonNull
protected static Map<String, String> extractAttributes(@NonNull Token.StartTag startTag) {
Map<String, String> map;
final Attributes attributes = startTag.attributes;
final int size = attributes.size();
if (size > 0) {
map = new HashMap<>(size);
for (Attribute attribute : attributes) {
map.put(attribute.getKey().toLowerCase(Locale.US), attribute.getValue());
}
map = Collections.unmodifiableMap(map);
} else {
map = Collections.emptyMap();
}
return map;
}
} }

View File

@ -137,13 +137,13 @@ public class Attribute implements Map.Entry<String, String>, Cloneable {
// return new Attribute(unencodedKey, value, null); // parent will get set when Put // return new Attribute(unencodedKey, value, null); // parent will get set when Put
// } // }
protected boolean isDataAttribute() { // protected boolean isDataAttribute() {
return isDataAttribute(key); // return isDataAttribute(key);
} // }
//
protected static boolean isDataAttribute(String key) { // protected static boolean isDataAttribute(String key) {
return key.startsWith(Attributes.dataPrefix) && key.length() > Attributes.dataPrefix.length(); // return key.startsWith(Attributes.dataPrefix) && key.length() > Attributes.dataPrefix.length();
} // }
// /** // /**
// * Collapsible if it's a boolean attribute and value is empty or same as name // * Collapsible if it's a boolean attribute and value is empty or same as name

View File

@ -28,7 +28,7 @@ import static ru.noties.markwon.html.jsoup.helper.Normalizer.lowerCase;
* @author Jonathan Hedley, jonathan@hedley.net * @author Jonathan Hedley, jonathan@hedley.net
*/ */
public class Attributes implements Iterable<Attribute>, Cloneable { public class Attributes implements Iterable<Attribute>, Cloneable {
protected static final String dataPrefix = "data-"; // protected static final String dataPrefix = "data-";
private static final int InitialCapacity = 4; // todo - analyze Alexa 1MM sites, determine best setting private static final int InitialCapacity = 4; // todo - analyze Alexa 1MM sites, determine best setting
// manages the key/val arrays // manages the key/val arrays
@ -282,14 +282,14 @@ public class Attributes implements Iterable<Attribute>, Cloneable {
return Collections.unmodifiableList(list); return Collections.unmodifiableList(list);
} }
/** // /**
* Retrieves a filtered view of attributes that are HTML5 custom data attributes; that is, attributes with keys // * Retrieves a filtered view of attributes that are HTML5 custom data attributes; that is, attributes with keys
* starting with {@code data-}. // * starting with {@code data-}.
* @return map of custom data attributes. // * @return map of custom data attributes.
*/ // */
public Map<String, String> dataset() { // public Map<String, String> dataset() {
return new Dataset(this); // return new Dataset(this);
} // }
// /** // /**
// Get the HTML representation of these attributes. // Get the HTML representation of these attributes.
@ -380,65 +380,65 @@ public class Attributes implements Iterable<Attribute>, Cloneable {
} }
} }
private static class Dataset extends AbstractMap<String, String> { // private static class Dataset extends AbstractMap<String, String> {
private final Attributes attributes; // private final Attributes attributes;
//
// private Dataset(Attributes attributes) {
// this.attributes = attributes;
// }
//
// @Override
// public Set<Entry<String, String>> entrySet() {
// return new EntrySet();
// }
//
// @Override
// public String put(String key, String value) {
// String dataKey = dataKey(key);
// String oldValue = attributes.hasKey(dataKey) ? attributes.get(dataKey) : null;
// attributes.put(dataKey, value);
// return oldValue;
// }
//
// private class EntrySet extends AbstractSet<Map.Entry<String, String>> {
//
// @Override
// public Iterator<Map.Entry<String, String>> iterator() {
// return new DatasetIterator();
// }
//
// @Override
// public int size() {
// int count = 0;
// Iterator iter = new DatasetIterator();
// while (iter.hasNext())
// count++;
// return count;
// }
// }
//
// private class DatasetIterator implements Iterator<Map.Entry<String, String>> {
// private Iterator<Attribute> attrIter = attributes.iterator();
// private Attribute attr;
// public boolean hasNext() {
// while (attrIter.hasNext()) {
// attr = attrIter.next();
// if (attr.isDataAttribute()) return true;
// }
// return false;
// }
//
// public Entry<String, String> next() {
// return new Attribute(attr.getKey().substring(dataPrefix.length()), attr.getValue());
// }
//
// public void remove() {
// attributes.remove(attr.getKey());
// }
// }
// }
private Dataset(Attributes attributes) { // private static String dataKey(String key) {
this.attributes = attributes; // return dataPrefix + key;
} // }
@Override
public Set<Entry<String, String>> entrySet() {
return new EntrySet();
}
@Override
public String put(String key, String value) {
String dataKey = dataKey(key);
String oldValue = attributes.hasKey(dataKey) ? attributes.get(dataKey) : null;
attributes.put(dataKey, value);
return oldValue;
}
private class EntrySet extends AbstractSet<Map.Entry<String, String>> {
@Override
public Iterator<Map.Entry<String, String>> iterator() {
return new DatasetIterator();
}
@Override
public int size() {
int count = 0;
Iterator iter = new DatasetIterator();
while (iter.hasNext())
count++;
return count;
}
}
private class DatasetIterator implements Iterator<Map.Entry<String, String>> {
private Iterator<Attribute> attrIter = attributes.iterator();
private Attribute attr;
public boolean hasNext() {
while (attrIter.hasNext()) {
attr = attrIter.next();
if (attr.isDataAttribute()) return true;
}
return false;
}
public Entry<String, String> next() {
return new Attribute(attr.getKey().substring(dataPrefix.length()), attr.getValue());
}
public void remove() {
attributes.remove(attr.getKey());
}
}
}
private static String dataKey(String key) {
return dataPrefix + key;
}
} }